Bug 1511943 - Make CloudFile preferences respond to provider registration; r=Fallen a=jorgk
authorGeoff Lankow <geoff@darktrojan.net>
Thu, 13 Dec 2018 20:08:45 +1300
changeset 33878 6f2dc49da9821e7a8716a5ea032402863bb26520
parent 33877 514f894df2cb5915e059f1ef896abd5a481aef65
child 33879 89aae8487741a3d1371d4e2a47ff353d14399674
push id388
push userclokep@gmail.com
push dateMon, 28 Jan 2019 20:54:56 +0000
reviewersFallen, jorgk
bugs1511943
Bug 1511943 - Make CloudFile preferences respond to provider registration; r=Fallen a=jorgk
mail/base/content/messenger.xul
mail/components/cloudfile/cloudFileAccounts.js
mail/components/preferences/applications.js
mail/test/mozmill/cloudfile/html/settings-with-link.xhtml
mail/test/mozmill/cloudfile/test-cloudfile-manager.js
--- a/mail/base/content/messenger.xul
+++ b/mail/base/content/messenger.xul
@@ -764,17 +764,18 @@
       </vbox>
     </vbox>
     <vbox id="preferencesTab" collapsed="true">
       <vbox flex="1">
         <notificationbox flex="1">
           <browser id="preferencesbrowser"
                    type="content"
                    flex="1"
-                   disablehistory="true"/>
+                   disablehistory="true"
+                   onclick="specialTabs.defaultClickHandler(event);"/>
         </notificationbox>
         <findbar browserid="preferencesbrowser"/>
       </vbox>
     </vbox>
   </hbox>
   <panel id="customizeToolbarSheetPopup" noautohide="true">
     <iframe id="customizeToolbarSheetIFrame"
             style="&dialog.dimensions;"
--- a/mail/components/cloudfile/cloudFileAccounts.js
+++ b/mail/components/cloudfile/cloudFileAccounts.js
@@ -98,30 +98,32 @@ var cloudFileAccounts = new class extend
     }
 
     if (this._providers.has(type)) {
       throw new Error(`Cloudfile provider ${type} is already registered`);
     } else if (hasXPCOM) {
       throw new Error(`Cloudfile provider ${type} is already registered as an XPCOM component`);
     }
     this._providers.set(aProvider.type, aProvider);
+    this.emit("providerRegistered", aProvider);
   }
 
   /**
    * Unregister a cloudfile provider. This function will only unregister those providers registered
    * through #registerProvider. XPCOM providers cannot be unregistered here.
    *
    * @param {String} aType                  The provider type to unregister
    */
   unregisterProvider(aType) {
     if (!this._providers.has(aType)) {
       throw new Error(`Cloudfile provider ${aType} is not registered`);
     }
 
     this._providers.delete(aType);
+    this.emit("providerUnregistered", aType);
   }
 
   getProviderForType(aType) {
     if (this._providers.has(aType)) {
       return this._providers.get(aType);
     }
 
     try {
--- a/mail/components/preferences/applications.js
+++ b/mail/components/preferences/applications.js
@@ -480,21 +480,61 @@ var gCloudFileTab = {
       this._list.selectedIndex = 0;
       this._removeAccountButton.disabled = false;
     }
 
     window.addEventListener("unload", this, {capture: false, once: true});
 
     this.updateThreshold();
 
+    this._onProviderRegistered = this._onProviderRegistered.bind(this);
+    this._onProviderUnregistered = this._onProviderUnregistered.bind(this);
+    cloudFileAccounts.on("providerRegistered", this._onProviderRegistered);
+    cloudFileAccounts.on("providerUnregistered", this._onProviderUnregistered);
+
     this._initialized = true;
   },
 
   destroy() {
     // Remove any controllers or observers here.
+    cloudFileAccounts.off("providerRegistered", this._onProviderRegistered);
+    cloudFileAccounts.off("providerUnregistered", this._onProviderUnregistered);
+  },
+
+  _onProviderRegistered(event, provider) {
+    let accounts = cloudFileAccounts.getAccountsForType(provider.type);
+    accounts.sort(this._sortAccounts);
+
+    // Always add newly-enabled accounts to the end of the list, this makes
+    // it clearer to users what's happening.
+    for (let account of accounts) {
+      let item = this.makeRichListItemForAccount(account);
+      this._list.appendChild(item);
+      if (!(account.accountKey in this._accountCache)) {
+        let accountInfo = {
+          account,
+          listItem: item,
+          result: Cr.NS_OK,
+        };
+        this._accountCache[account.accountKey] = accountInfo;
+        this._mapResultToState(item, accountInfo.result);
+      }
+    }
+  },
+
+  _onProviderUnregistered(event, type) {
+    for (let item of this._list.children) {
+      // If the provider is unregistered, getAccount returns null.
+      if (!cloudFileAccounts.getAccount(item.value)) {
+        if (item.hasAttribute("selected")) {
+          this._settingsDeck.selectedPanel = this._defaultPanel;
+        }
+        item.remove();
+      }
+    }
   },
 
   makeRichListItemForAccount(aAccount) {
     let rli = document.createElement("richlistitem");
     rli.value = aAccount.accountKey;
     rli.setAttribute("value", aAccount.accountKey);
     rli.setAttribute("class", "cloudfileAccount");
     rli.setAttribute("state", "waiting-to-connect");
@@ -523,35 +563,35 @@ var gCloudFileTab = {
   },
 
   clearEntries() {
     // Clear the list of entries.
     while (this._list.hasChildNodes())
       this._list.lastChild.remove();
   },
 
+  // Sort the accounts by displayName.
+  _sortAccounts(a, b) {
+    let aName = cloudFileAccounts.getDisplayName(a.accountKey)
+                                 .toLowerCase();
+    let bName = cloudFileAccounts.getDisplayName(b.accountKey)
+                                 .toLowerCase();
+
+    if (aName < bName)
+      return -1;
+    if (aName > bName)
+      return 1;
+    return 0;
+  },
+
   rebuildView() {
     this.clearEntries();
     let accounts = cloudFileAccounts.accounts;
 
-    // Sort the accounts by displayName.
-    function sortAccounts(a, b) {
-      let aName = cloudFileAccounts.getDisplayName(a.accountKey)
-                                   .toLowerCase();
-      let bName = cloudFileAccounts.getDisplayName(b.accountKey)
-                                   .toLowerCase();
-
-      if (aName < bName)
-        return -1;
-      if (aName > bName)
-        return 1;
-      return 0;
-    }
-
-    accounts.sort(sortAccounts);
+    accounts.sort(this._sortAccounts);
 
     for (let account of accounts) {
       let rli = this.makeRichListItemForAccount(account);
       this._list.appendChild(rli);
       if (!(account.accountKey in this._accountCache))
         this.requestUserInfoForItem(rli, false);
     }
   },
@@ -639,27 +679,32 @@ var gCloudFileTab = {
     } else if (result == Ci.nsIMsgCloudFileProvider.authErr) {
       this._settingsDeck.selectedPanel = this._authErrorPanel;
     } else {
       Cu.reportError("Unexpected connection error.");
     }
   },
 
   _showAccountManagement(aProvider) {
-    let iframe = document.createElement("iframe");
+    let url = aProvider.managementURL;
+    if (url.startsWith("moz-extension:")) {
+      // Assumes there is only one account per provider.
+      let account = cloudFileAccounts.getAccountsForType(aProvider.type)[0];
+      url += `?accountId=${account.accountKey}`;
+    }
 
-    iframe.setAttribute("src", aProvider.managementURL);
+    let iframe = document.createElement("iframe");
     iframe.setAttribute("flex", "1");
-
-    let type = aProvider.settingsURL.startsWith("chrome:") ? "chrome" : "content";
-    iframe.setAttribute("type", type);
-
     // allows keeping dialog background color without hoops
     iframe.setAttribute("transparent", "true");
 
+    let type = url.startsWith("chrome:") ? "chrome" : "content";
+    iframe.setAttribute("type", type);
+    iframe.setAttribute("src", url);
+
     // If we have a past iframe, we replace it. Else append
     // to the wrapper.
     if (this._settings)
       this._settings.remove();
 
     this._settingsPanelWrap.appendChild(iframe);
     this._settings = iframe;
 
@@ -668,33 +713,16 @@ var gCloudFileTab = {
       try {
         iframe.contentWindow
               .wrappedJSObject
               .onLoadProvider(aProvider);
       } catch (e) {
         Cu.reportError(e);
       }
     }, {capture: false, once: true});
-
-    // When the iframe (or any subcontent) fires the DOMContentLoaded event,
-    // attach the _onClickLink handler to any anchor elements that we can find.
-    this._settings.contentWindow.addEventListener("DOMContentLoaded", function(e) {
-      let doc = e.originalTarget;
-      let links = doc.getElementsByTagName("a");
-
-      for (let link of links) {
-        link.addEventListener("click", gCloudFileTab._onClickLink);
-      }
-    }, {capture: false, once: true});
-  },
-
-  _onClickLink(aEvent) {
-    aEvent.preventDefault();
-    let href = aEvent.target.getAttribute("href");
-    openLinkExternally(href);
   },
 
   authSelected() {
     let item = this._list.selectedItem;
 
     if (!item)
       return;
 
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/cloudfile/html/settings-with-link.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/.  -->
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <body>
+    <a id="a" href="https://www.example.com/">Click me!</a>
+  </body>
+</html>
--- a/mail/test/mozmill/cloudfile/test-cloudfile-manager.js
+++ b/mail/test/mozmill/cloudfile/test-cloudfile-manager.js
@@ -7,31 +7,36 @@
  * services
  */
 
 "use strict";
 
 var MODULE_NAME = 'test-cloudfile-manager';
 
 var RELATIVE_ROOT = '../shared-modules';
-var MODULE_REQUIRES = ['folder-display-helpers',
-                       'pref-window-helpers',
+var MODULE_REQUIRES = ["folder-display-helpers",
+                       "pref-window-helpers",
                        "content-tab-helpers",
-                       'cloudfile-helpers'];
+                       "cloudfile-helpers",
+                       "window-helpers"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 var kTestAccountType = "mock";
+var kRootURL = collector.addHttpResource("../cloudfile/html", "");
+var kSettingsWithLink = kRootURL + "settings-with-link.xhtml";
 
 function setupModule(module) {
   for (let lib of MODULE_REQUIRES) {
     collector.getModule(lib).installInto(module);
   }
 
-  gMockCloudfileManager.register(kTestAccountType);
+  gMockCloudfileManager.register(kTestAccountType, {
+    managementURL: kSettingsWithLink,
+  });
 
   // Let's set up a few dummy accounts;
   create_dummy_account("someKey1", kTestAccountType,
                        "carl's Account");
   create_dummy_account("someKey2", kTestAccountType,
                        "Amber's Account");
   create_dummy_account("someKey3", kTestAccountType,
                        "alice's Account");
@@ -82,8 +87,33 @@ function test_load_accounts_and_properly
   for (let [index, expectedKey] of kExpected.entries()) {
     let item = richList.getItemAtIndex(index);
     assert_equals(expectedKey, item.value,
                   "The account list is out of order");
   }
 
   close_pref_tab(prefTab);
 }
+
+/**
+ * Tests that a link in the management pane is loaded in
+ * a browser and not in the management pane.
+ */
+function test_external_link() {
+  gMockExtProtSvcReg.register();
+
+  let prefTab = open_pref_tab("paneApplications");
+  let tabbox = content_tab_e(prefTab, "attachmentPrefs");
+  tabbox.selectedIndex = 1;
+
+  let iframe = content_tab_e(prefTab, "cloudFileSettingsWrapper").firstElementChild;
+  wait_for_frame_load(iframe, kSettingsWithLink);
+  mc.click(new elementslib.ID(iframe.contentDocument, "a"));
+
+  let targetHref = "https://www.example.com/";
+  mc.waitFor(
+    () => gMockExtProtSvc.urlLoaded(targetHref),
+    `Timed out waiting for the link ${targetHref} to be opened in the default browser.`
+  );
+  close_pref_tab(prefTab);
+
+  gMockExtProtSvcReg.unregister();
+}