diff --git a/mail/components/addrbook/test/browser/browser.ini b/mail/components/addrbook/test/browser/browser.ini index 9ccc5efef7..b01e251236 100644 --- a/mail/components/addrbook/test/browser/browser.ini +++ b/mail/components/addrbook/test/browser/browser.ini @@ -10,6 +10,7 @@ subsuite = thunderbird tags = addrbook [browser_cardDAV_init.js] +[browser_cardDAV_properties.js] [browser_cardDAV_sync.js] [browser_contact_tree.js] [browser_directory_tree.js] diff --git a/mail/components/addrbook/test/browser/browser_cardDAV_properties.js b/mail/components/addrbook/test/browser/browser_cardDAV_properties.js new file mode 100644 index 0000000000..4a4fe1f00c --- /dev/null +++ b/mail/components/addrbook/test/browser/browser_cardDAV_properties.js @@ -0,0 +1,246 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests CardDAV properties dialog. + */ + +const { CardDAVDirectory } = ChromeUtils.import( + "resource:///modules/CardDAVDirectory.jsm" +); +const { CardDAVServer } = ChromeUtils.import( + "resource://testing-common/CardDAVServer.jsm" +); + +add_task(async () => { + const INTERVAL_PREF = "ldap_2.servers.props.carddav.syncinterval"; + const TOKEN_PREF = "ldap_2.servers.props.carddav.token"; + const TOKEN_VALUE = "http://mochi.test/sync/0"; + const URL_PREF = "ldap_2.servers.props.carddav.url"; + const URL_VALUE = "https://mochi.test/carddav/test"; + + let dirPrefId = MailServices.ab.newAddressBook( + "props", + undefined, + Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE + ); + Assert.equal(dirPrefId, "ldap_2.servers.props"); + Assert.equal([...MailServices.ab.directories].length, 3); + + let directory = MailServices.ab.getDirectoryFromId(dirPrefId); + let davDirectory = CardDAVDirectory.forFile(directory.fileName); + registerCleanupFunction(async () => { + let removePromise = promiseDirectoryRemoved(); + MailServices.ab.deleteAddressBook(directory.URI); + await removePromise; + + Assert.equal(davDirectory._syncTimer, null, "sync timer cleaned up"); + }); + Assert.equal(directory.dirType, Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE); + + Services.prefs.setIntPref(INTERVAL_PREF, 0); + Services.prefs.setStringPref(TOKEN_PREF, TOKEN_VALUE); + Services.prefs.setStringPref(URL_PREF, URL_VALUE); + + Assert.ok(davDirectory); + Assert.equal(davDirectory._serverURL, URL_VALUE); + Assert.equal(davDirectory._syncToken, TOKEN_VALUE); + Assert.equal(davDirectory._syncTimer, null, "no sync scheduled"); + + let abWindow = await openAddressBookWindow(); + let abDocument = abWindow.document; + registerCleanupFunction(async () => { + await closeAddressBookWindow(); + Services.prefs.clearUserPref("mail.addr_book.view.startupURI"); + }); + + // This test becomes unreliable if we don't pause for a moment. + await new Promise(resolve => abWindow.setTimeout(resolve, 500)); + + openDirectory(directory); + + Assert.equal(abWindow.gDirectoryTreeView.rowCount, 4); + Assert.equal(abWindow.gDirectoryTreeView.getIndexForId(directory.URI), 2); + Assert.equal(abWindow.gDirTree.currentIndex, 2); + + let menu = abDocument.getElementById("dirTreeContext"); + let menuItem = abDocument.getElementById("dirTreeContext-properties"); + + let subtest = async function(expectedValues, newValues, buttonAction) { + let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown"); + mailTestUtils.treeClick(EventUtils, abWindow, abWindow.gDirTree, 2, 0, { + type: "mousedown", + button: 2, + }); + mailTestUtils.treeClick(EventUtils, abWindow, abWindow.gDirTree, 2, 0, { + type: "contextmenu", + }); + mailTestUtils.treeClick(EventUtils, abWindow, abWindow.gDirTree, 2, 0, { + type: "mouseup", + button: 2, + }); + await shownPromise; + + Assert.equal(abWindow.gDirTree.currentIndex, 2); + + let dialogPromise = BrowserTestUtils.promiseAlertDialog( + undefined, + "chrome://messenger/content/addressbook/abCardDAVProperties.xhtml", + async dialogWindow => { + let dialogDocument = dialogWindow.document; + + let nameInput = dialogDocument.getElementById("carddav-name"); + Assert.equal(nameInput.value, expectedValues.name); + if ("name" in newValues) { + nameInput.value = newValues.name; + } + + let urlInput = dialogDocument.getElementById("carddav-url"); + Assert.equal(urlInput.value, expectedValues.url); + if ("url" in newValues) { + urlInput.value = newValues.url; + } + + let refreshActiveInput = dialogDocument.getElementById( + "carddav-refreshActive" + ); + let refreshIntervalInput = dialogDocument.getElementById( + "carddav-refreshInterval" + ); + + Assert.equal(refreshActiveInput.checked, expectedValues.refreshActive); + Assert.equal( + refreshIntervalInput.disabled, + !expectedValues.refreshActive + ); + if ( + "refreshActive" in newValues && + newValues.refreshActive != expectedValues.refreshActive + ) { + EventUtils.synthesizeMouseAtCenter( + refreshActiveInput, + {}, + dialogWindow + ); + Assert.equal(refreshIntervalInput.disabled, !newValues.refreshActive); + } + + Assert.equal( + refreshIntervalInput.value, + expectedValues.refreshInterval + ); + if ("refreshInterval" in newValues) { + refreshIntervalInput.value = newValues.refreshInterval; + } + + dialogDocument + .querySelector("dialog") + .getButton(buttonAction) + .click(); + } + ); + EventUtils.synthesizeMouseAtCenter(menuItem, {}, abWindow); + await dialogPromise; + + await new Promise(resolve => abWindow.setTimeout(resolve)); + }; + + info("Open the dialog and cancel it. Nothing should change."); + await subtest( + { + name: "props", + url: URL_VALUE, + refreshActive: false, + refreshInterval: 30, + }, + {}, + "cancel" + ); + + Assert.equal(davDirectory.dirName, "props"); + Assert.equal(davDirectory._serverURL, URL_VALUE); + Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 0); + Assert.equal(davDirectory._syncTimer, null, "no sync scheduled"); + + info("Open the dialog and accept it. Nothing should change."); + await subtest( + { + name: "props", + url: URL_VALUE, + refreshActive: false, + refreshInterval: 30, + }, + {}, + "accept" + ); + + Assert.equal(davDirectory.dirName, "props"); + Assert.equal(davDirectory._serverURL, URL_VALUE); + Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 0); + Assert.equal(davDirectory._syncTimer, null, "no sync scheduled"); + + info("Open the dialog and change the values."); + await subtest( + { + name: "props", + url: URL_VALUE, + refreshActive: false, + refreshInterval: 30, + }, + { + name: "CardDAV Properties Test", + refreshActive: true, + refreshInterval: 30, + }, + "accept" + ); + + Assert.equal(davDirectory.dirName, "CardDAV Properties Test"); + Assert.equal(davDirectory._serverURL, URL_VALUE); + Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 30); + Assert.notEqual(davDirectory._syncTimer, null, "sync scheduled"); + let currentSyncTimer = davDirectory._syncTimer; + + info("Open the dialog and accept it. Nothing should change."); + await subtest( + { + name: "CardDAV Properties Test", + url: URL_VALUE, + refreshActive: true, + refreshInterval: 30, + }, + {}, + "accept" + ); + + Assert.equal(davDirectory.dirName, "CardDAV Properties Test"); + Assert.equal(davDirectory._serverURL, URL_VALUE); + Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 30); + Assert.equal( + davDirectory._syncTimer, + currentSyncTimer, + "same sync scheduled" + ); + + info("Open the dialog and change the interval."); + await subtest( + { + name: "CardDAV Properties Test", + url: URL_VALUE, + refreshActive: true, + refreshInterval: 30, + }, + { refreshInterval: 60 }, + "accept" + ); + + Assert.equal(davDirectory.dirName, "CardDAV Properties Test"); + Assert.equal(davDirectory._serverURL, URL_VALUE); + Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 60); + Assert.greater( + davDirectory._syncTimer, + currentSyncTimer, + "new sync scheduled" + ); +}); diff --git a/mail/locales/en-US/messenger/addressbook/abCardDAVProperties.ftl b/mail/locales/en-US/messenger/addressbook/abCardDAVProperties.ftl new file mode 100644 index 0000000000..9ff9ea2331 --- /dev/null +++ b/mail/locales/en-US/messenger/addressbook/abCardDAVProperties.ftl @@ -0,0 +1,27 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at http://mozilla.org/MPL/2.0/. + +carddav-url-label = + .value = CardDAV URL: + .accesskey = V + +carddav-refreshinterval-label = + .label = Synchronize: + .accesskey = S + +# Variables: +# $minutes (integer) - Number of minutes between address book synchronizations +carddav-refreshinterval-minutes-value = + .label = { $minutes -> + [one] every minute + *[other] every { $minutes } minutes + } + +# Variables: +# $hours (integer) - Number of hours between address book synchronizations +carddav-refreshinterval-hours-value = + .label = { $hours -> + [one] every hour + *[other] every { $hours } hours + } diff --git a/mail/themes/shared/jar.inc.mn b/mail/themes/shared/jar.inc.mn index cef7f51cab..473d07506e 100644 --- a/mail/themes/shared/jar.inc.mn +++ b/mail/themes/shared/jar.inc.mn @@ -207,6 +207,7 @@ skin/classic/messenger/shared/accountManage.css (../shared/mail/accountManage.css) skin/classic/messenger/shared/accountProvisioner.css (../shared/mail/accountProvisioner.css) skin/classic/messenger/shared/addressbook.css (../shared/mail/addressbook.css) + skin/classic/messenger/shared/cardDAV.css (../shared/mail/cardDAV.css) skin/classic/messenger/shared/cardDialog.css (../shared/mail/cardDialog.css) skin/classic/messenger/shared/chat.css (../../components/im/themes/chat.css) skin/classic/messenger/shared/compacttheme.css (../shared/mail/compacttheme.css) diff --git a/mailnews/addrbook/content/abCardDAVDialog.css b/mail/themes/shared/mail/cardDAV.css similarity index 78% rename from mailnews/addrbook/content/abCardDAVDialog.css rename to mail/themes/shared/mail/cardDAV.css index 1d865bb8b7..c3b98ac74f 100644 --- a/mailnews/addrbook/content/abCardDAVDialog.css +++ b/mail/themes/shared/mail/cardDAV.css @@ -20,22 +20,6 @@ width: 30em; } -th { - font-weight: normal; - text-align: start; - width: 10%; -} - -.input-container { - display: flex; - align-items: center; - flex-wrap: nowrap; -} - -.input-container > * { - flex: 1; -} - #carddav-provider { height: 1.8em; } @@ -85,3 +69,33 @@ th { background-size: 16px 16px; } } + +#carddav-properties-table { + display: grid; + grid-template-columns: min-content auto; + align-items: baseline; +} + +.input-container { + display: flex; +} + +.input-container > * { + flex: 1; +} + +#carddav-refreshActive-cell { + /* This shouldn't be necessary, but there's no good combination of checkbox + * and label that play nicely with the align-items: baseline above. */ + align-self: center; +} + +#carddav-refreshInterval-cell { + display: flex; + align-items: baseline; +} + +#carddav-refreshInterval { + flex: 1; + margin: 2px 4px; +} diff --git a/mailnews/addrbook/content/abAddressBookNameDialog.js b/mailnews/addrbook/content/abAddressBookNameDialog.js index a2df4129fc..daa8ebba29 100644 --- a/mailnews/addrbook/content/abAddressBookNameDialog.js +++ b/mailnews/addrbook/content/abAddressBookNameDialog.js @@ -64,7 +64,10 @@ function abNameOKButton(event) { let newDirName = gNameInput.value.trim(); // Do not allow an already existing name. - if (MailServices.ab.directoryNameExists(newDirName)) { + if ( + MailServices.ab.directoryNameExists(newDirName) && + (!gDirectory || newDirName != gDirectory.dirName) + ) { const kAlertTitle = document .getElementById("bundle_addressBook") .getString("duplicateNameTitle"); diff --git a/mailnews/addrbook/content/abCardDAVDialog.xhtml b/mailnews/addrbook/content/abCardDAVDialog.xhtml index 4117dff9be..538e324882 100644 --- a/mailnews/addrbook/content/abCardDAVDialog.xhtml +++ b/mailnews/addrbook/content/abCardDAVDialog.xhtml @@ -4,8 +4,8 @@ - file, you can obtain one at http://mozilla.org/MPL/2.0/. --> - - + + - - - - - - - - - - - + + + + + + + + { + gStringBundle = Services.strings.createBundle( + "chrome://messenger/locale/addressbook/addressBook.properties" + ); + document.title = gStringBundle.formatStringFromName( + "addressBookTitleEdit", + [gDirectory.dirName] + ); + + gNameInput = document.getElementById("carddav-name"); + gNameInput.value = gDirectory.dirName; + gNameInput.addEventListener("input", () => { + gAcceptButton.disabled = gNameInput.value.trim() == ""; + }); + + gURLInput = document.getElementById("carddav-url"); + gURLInput.value = gDirectory.getStringValue("carddav.url", ""); + + gRefreshActiveInput = document.getElementById("carddav-refreshActive"); + gRefreshActiveInput.addEventListener( + "command", + () => (gRefreshMenulist.disabled = !gRefreshActiveInput.checked) + ); + + gRefreshMenulist = document.getElementById("carddav-refreshInterval"); + initRefreshInterval(); + + gAcceptButton = document.querySelector("dialog").getButton("accept"); + }, + { once: true } +); + +window.addEventListener("dialogaccept", event => { + let newDirName = gNameInput.value.trim(); + let newSyncInterval = gRefreshActiveInput.checked + ? gRefreshMenulist.value + : 0; + + if (newDirName != gDirectory.dirName) { + // Do not allow an already existing name. + if (MailServices.ab.directoryNameExists(newDirName)) { + let alertTitle = gStringBundle.GetStringFromName("duplicateNameTitle"); + let alertText = gStringBundle.formatStringFromName("duplicateNameText", [ + newDirName, + ]); + Services.prompt.alert(window, alertTitle, alertText); + event.preventDefault(); + return; + } + + gDirectory.dirName = newDirName; + } + + if (newSyncInterval != gDirectory.getIntValue("carddav.syncinterval", -1)) { + gDirectory.setIntValue("carddav.syncinterval", newSyncInterval); + } +}); + +function initRefreshInterval() { + function createMenuItem(minutes) { + let menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("value", minutes); + menuitem.setAttribute("data-l10n-attrs", "label"); + if (minutes < 60) { + document.l10n.setAttributes( + menuitem, + "carddav-refreshinterval-minutes-value", + { + minutes, + } + ); + } else { + document.l10n.setAttributes( + menuitem, + "carddav-refreshinterval-hours-value", + { + hours: minutes / 60, + } + ); + } + + gRefreshMenulist.menupopup.appendChild(menuitem); + if (refreshInterval == minutes) { + gRefreshMenulist.value = minutes; + foundValue = true; + } + + return menuitem; + } + + let refreshInterval = gDirectory.getIntValue("carddav.syncinterval", 30); + if (refreshInterval === null) { + refreshInterval = 30; + } + + let foundValue = false; + + for (let min of [1, 5, 15, 30, 60, 120, 240, 360, 720, 1440]) { + createMenuItem(min); + } + + if (refreshInterval == 0) { + gRefreshMenulist.value = 30; // The default. + gRefreshMenulist.disabled = true; + foundValue = true; + } else { + gRefreshActiveInput.checked = true; + } + + if (!foundValue) { + // Special menuitem in case the user changed the value in the config editor. + createMenuItem(refreshInterval); + } +} diff --git a/mailnews/addrbook/content/abCardDAVProperties.xhtml b/mailnews/addrbook/content/abCardDAVProperties.xhtml new file mode 100644 index 0000000000..c06465593c --- /dev/null +++ b/mailnews/addrbook/content/abCardDAVProperties.xhtml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + +