Bug 1549814 - Add 'New Login' button to create new saved logins. r?MattN draft
authorpulselistener
Sat, 18 May 2019 06:49:23 +0000
changeset 2008574 125909675145ef08262d8459821b1221087cadf4
parent 2008573 7b25b65910f86ee3f7534cd2aca748c90fcb9fbf
child 2008575 e1fb67d904f884b8220b870ad7f057a621c0a4bc
push id363923
push userreviewbot
push dateSat, 18 May 2019 06:49:52 +0000
treeherdertry@6eb4ec2cb49f [default view] [failures only]
reviewersMattN
bugs1549814
milestone68.0a1
Bug 1549814 - Add 'New Login' button to create new saved logins. r?MattN Differential Revision: https://phabricator.services.mozilla.com/D31713 Differential Diff: PHID-DIFF-tl2jd4vufqzl6vgfcxbv
browser/components/BrowserGlue.jsm
browser/components/aboutlogins/AboutLoginsChild.jsm
browser/components/aboutlogins/AboutLoginsParent.jsm
browser/components/aboutlogins/content/aboutLogins.css
browser/components/aboutlogins/content/aboutLogins.ftl
browser/components/aboutlogins/content/aboutLogins.html
browser/components/aboutlogins/content/aboutLogins.js
browser/components/aboutlogins/content/components/login-filter.css
browser/components/aboutlogins/content/components/login-item.css
browser/components/aboutlogins/content/components/login-item.js
browser/components/aboutlogins/content/components/login-list.js
browser/components/aboutlogins/content/components/modal-input.css
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -30,16 +30,17 @@ let ACTORS = {
 };
 
 let LEGACY_ACTORS = {
   AboutLogins: {
     child: {
       matches: ["about:logins"],
       module: "resource:///actors/AboutLoginsChild.jsm",
       events: {
+        "AboutLoginsCreateLogin": {wantUntrusted: true},
         "AboutLoginsDeleteLogin": {wantUntrusted: true},
         "AboutLoginsOpenSite": {wantUntrusted: true},
         "AboutLoginsUpdateLogin": {wantUntrusted: true},
         "AboutLoginsInit": {wantUntrusted: true},
       },
       messages: [
         "AboutLogins:AllLogins",
         "AboutLogins:LoginAdded",
@@ -541,16 +542,17 @@ const listeners = {
     "AsyncPrefs:ResetPref": ["AsyncPrefs"],
     // PLEASE KEEP THIS LIST IN SYNC WITH THE LISTENERS ADDED IN AsyncPrefs.init
 
     "webrtc:UpdateGlobalIndicators": ["webrtcUI"],
     "webrtc:UpdatingIndicators": ["webrtcUI"],
   },
 
   mm: {
+    "AboutLogins:CreateLogin": ["AboutLoginsParent"],
     "AboutLogins:DeleteLogin": ["AboutLoginsParent"],
     "AboutLogins:OpenSite": ["AboutLoginsParent"],
     "AboutLogins:Subscribe": ["AboutLoginsParent"],
     "AboutLogins:UpdateLogin": ["AboutLoginsParent"],
     "Content:Click": ["ContentClick"],
     "ContentSearch": ["ContentSearch"],
     "FormValidation:ShowPopup": ["FormValidationHandler"],
     "FormValidation:HidePopup": ["FormValidationHandler"],
--- a/browser/components/aboutlogins/AboutLoginsChild.jsm
+++ b/browser/components/aboutlogins/AboutLoginsChild.jsm
@@ -26,16 +26,20 @@ class AboutLoginsChild extends ActorChil
             return LoginHelper.doLoginsMatch(loginA, loginB, {});
           },
         };
         waivedContent.AboutLoginsUtils = Cu.cloneInto(AboutLoginsUtils, waivedContent, {
           cloneFunctions: true,
         });
         break;
       }
+      case "AboutLoginsCreateLogin": {
+        this.mm.sendAsyncMessage("AboutLogins:CreateLogin", {login: event.detail});
+        break;
+      }
       case "AboutLoginsDeleteLogin": {
         this.mm.sendAsyncMessage("AboutLogins:DeleteLogin", {login: event.detail});
         break;
       }
       case "AboutLoginsOpenSite": {
         this.mm.sendAsyncMessage("AboutLogins:OpenSite", {login: event.detail});
         break;
       }
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -65,16 +65,26 @@ var AboutLoginsParent = {
   receiveMessage(message) {
     // Only respond to messages sent from about:logins.
     if (message.target.remoteType != EXPECTED_ABOUTLOGINS_REMOTE_TYPE ||
         message.target.contentPrincipal.originNoSuffix != ABOUT_LOGINS_ORIGIN) {
       return;
     }
 
     switch (message.name) {
+      case "AboutLogins:CreateLogin": {
+        let newLogin = message.data.login;
+        Object.assign(newLogin, {
+          formSubmitURL: "",
+          usernameField: "",
+          passwordField: "",
+        });
+        Services.logins.addLogin(LoginHelper.vanillaObjectToLogin(newLogin));
+        break;
+      }
       case "AboutLogins:DeleteLogin": {
         let login = LoginHelper.vanillaObjectToLogin(message.data.login);
         Services.logins.removeLogin(login);
         break;
       }
       case "AboutLogins:OpenSite": {
         let guid = message.data.login.guid;
         let logins = LoginHelper.searchLoginsWithObject({guid});
--- a/browser/components/aboutlogins/content/aboutLogins.css
+++ b/browser/components/aboutlogins/content/aboutLogins.css
@@ -12,32 +12,39 @@ body {
 }
 
 header {
   display: flex;
   grid-area: header;
   align-items: center;
   background-color: var(--in-content-box-background);
   border-bottom: 1px solid var(--in-content-box-border-color);
+  padding: 0 18px;
 }
 
 login-filter {
   flex: auto;
   align-self: center;
 }
 
 login-list {
   grid-area: logins;
+  overflow: hidden auto;
 }
 
 login-item {
   grid-area: login;
   max-width: 800px;
 }
 
 #branding-logo {
   height: 32px;
+  margin-inline-end: 18px;
+}
+
+#create-login-button {
   margin-inline-start: 18px;
+  margin-inline-end: 0;
 }
 
 :root:not(.official-branding) #branding-logo {
   display: none;
 }
--- a/browser/components/aboutlogins/content/aboutLogins.ftl
+++ b/browser/components/aboutlogins/content/aboutLogins.ftl
@@ -5,38 +5,53 @@
 ### This file is not in a locales directory to prevent it from
 ### being translated as the feature is still in heavy development
 ### and strings are likely to change often.
 
 ### Fluent isn't translating elements in the shadow DOM so the translated strings
 ### need to be applied to the composed node where they can be moved to the proper
 ### descendant after translation.
 
-about-logins-page-title = Login Manager
+about-logins-page-title = Logins & Passwords
+
+create-login-button = New Login
 
 login-filter =
   .placeholder = Search Logins
 
 login-list =
   .count =
     { $count ->
         [one] { $count } entry
        *[other] { $count } entries
     }
 
 login-item =
   .cancel-button = Cancel
+  .copied-password-button = ✓ Copied!
+  .copied-username-button = ✓ Copied!
+  .copy-password-button = Copy
+  .copy-username-button = Copy
   .delete-button = Delete
   .edit-button = Edit
   .hostname-label = Website Address
   .modal-input-reveal-checkbox-hide = Hide password
   .modal-input-reveal-checkbox-show = Show password
-  .copied-password-button = ✓ Copied!
-  .copied-username-button = ✓ Copied!
-  .copy-password-button = Copy
-  .copy-username-button = Copy
+  .new-login-title = New Entry
   .open-site-button = Launch
   .password-label = Password
   .save-changes-button = Save Changes
-  .time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
-  .time-changed = Last changed: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
-  .time-used = Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
+  .time-created =
+    { $timeCreated ->
+        [0] Created:
+       *[other] Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
+    }
+  .time-changed =
+    { $timeChanged ->
+        [0] Last modified:
+       *[other] Last modified: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
+    }
+  .time-used =
+    { $timeUsed ->
+        [0] Last used:
+       *[other] Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
+    }
   .username-label = Username
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -20,32 +20,34 @@
     <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
     <link rel="stylesheet" href="chrome://browser/content/aboutlogins/aboutLogins.css">
   </head>
   <body>
     <header>
       <img id="branding-logo" src="chrome://branding/content/aboutlogins.svg" alt=""/>
       <login-filter data-l10n-id="login-filter"
                     data-l10n-attrs="placeholder"></login-filter>
+      <button id="create-login-button" data-l10n-id="create-login-button"></button>
     </header>
     <login-list data-l10n-id="login-list"
                 data-l10n-attrs="count"
                 data-l10n-args='{"count": 0}'></login-list>
     <login-item data-l10n-id="login-item"
                 data-l10n-args='{"timeCreated": 0, "timeChanged": 0, "timeUsed": 0}'
                 data-l10n-attrs="cancel-button,
                                  copy-password-button,
                                  copy-username-button,
                                  copied-password-button,
                                  copied-username-button,
                                  delete-button,
                                  edit-button,
                                  hostname-label,
                                  modal-input-reveal-checkbox-hide,
                                  modal-input-reveal-checkbox-show,
+                                 new-login-title,
                                  open-site-button,
                                  password-label,
                                  save-changes-button,
                                  time-created,
                                  time-changed,
                                  time-used,
                                  username-label"></login-item>
 
@@ -72,17 +74,18 @@
       <div class="header">
         <h2 class="title"></h2>
         <button class="edit-button"></button>
         <button class="delete-button"></button>
       </div>
       <div class="detail-row">
         <label>
           <span class="hostname-label field-label"></span>
-          <span class="hostname"/>
+          <span class="hostname-saved-value"></span>
+          <input type="text" name="hostname"/>
         </label>
         <button class="open-site-button"></button>
       </div>
       <div class="detail-row">
         <label>
           <span class="username-label field-label"></span>
           <modal-input name="username"/>
         </label>
--- a/browser/components/aboutlogins/content/aboutLogins.js
+++ b/browser/components/aboutlogins/content/aboutLogins.js
@@ -2,28 +2,35 @@
  * 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/. */
 
 let gElements = {};
 
 document.addEventListener("DOMContentLoaded", () => {
   gElements.loginList = document.querySelector("login-list");
   gElements.loginItem = document.querySelector("login-item");
+  gElements.newLoginButton = document.querySelector("#create-login-button");
+
+  gElements.newLoginButton.addEventListener("click", () => {
+    gElements.loginItem.setLogin({});
+    gElements.loginList.clearSelection();
+  });
 
   document.dispatchEvent(new CustomEvent("AboutLoginsInit", {bubbles: true}));
 }, {once: true});
 
 window.addEventListener("AboutLoginsChromeToContent", event => {
   switch (event.detail.messageType) {
     case "AllLogins": {
       gElements.loginList.setLogins(event.detail.value);
       break;
     }
     case "LoginAdded": {
       gElements.loginList.loginAdded(event.detail.value);
+      gElements.loginItem.loginAdded(event.detail.value);
       break;
     }
     case "LoginModified": {
       gElements.loginList.loginModified(event.detail.value);
       gElements.loginItem.loginModified(event.detail.value);
       break;
     }
     case "LoginRemoved": {
--- a/browser/components/aboutlogins/content/components/login-filter.css
+++ b/browser/components/aboutlogins/content/components/login-filter.css
@@ -2,11 +2,10 @@
  * 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/. */
 
 :host {
   display: flex;
 }
 
 input {
-  margin: 18px;
   flex: auto;
 }
--- a/browser/components/aboutlogins/content/components/login-item.css
+++ b/browser/components/aboutlogins/content/components/login-item.css
@@ -3,16 +3,20 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 :host {
   padding-top: 36px;
   padding-left: 40px;
   padding-right: 40px;
 }
 
+:host([isNewLogin]) .hostname-saved-value,
+:host(:not([isNewLogin])) input[name="hostname"],
+:host([isNewLogin]) copy-to-clipboard-button,
+:host([isNewLogin]) .open-site-button,
 :host(:not([editing])) .save-changes-button,
 :host(:not([editing])) .cancel-button {
   display: none;
 }
 
 .header {
   display: flex;
   border-bottom: 1px solid var(--in-content-box-border-color);
--- a/browser/components/aboutlogins/content/components/login-item.js
+++ b/browser/components/aboutlogins/content/components/login-item.js
@@ -52,16 +52,17 @@ class LoginItem extends ReflectedFluentE
       "copied-username-button",
       "copy-password-button",
       "copy-username-button",
       "delete-button",
       "edit-button",
       "hostname-label",
       "modal-input-reveal-checkbox-hide",
       "modal-input-reveal-checkbox-show",
+      "new-login-title",
       "open-site-button",
       "password-label",
       "save-changes-button",
       "time-created",
       "time-changed",
       "time-used",
       "username-label",
     ];
@@ -92,31 +93,41 @@ class LoginItem extends ReflectedFluentE
                        .setAttribute("reveal-checkbox-hide", this.getAttribute(attrName));
         break;
       }
       case "modal-input-reveal-checkbox-show": {
         this.shadowRoot.querySelector("modal-input[name='password']")
                        .setAttribute("reveal-checkbox-show", this.getAttribute(attrName));
         break;
       }
+      case "new-login-title": {
+        let title = this.shadowRoot.querySelector(".title");
+        title.setAttribute(attrName, this.getAttribute(attrName));
+        if (!this._login.title) {
+          title.textContent = this.getAttribute(attrName);
+        }
+        break;
+      }
       default:
         return false;
     }
     return true;
   }
 
   render() {
     let l10nArgs = {
-      timeCreated: this._login.timeCreated || "",
-      timeChanged: this._login.timePasswordChanged || "",
-      timeUsed: this._login.timeLastUsed || "",
+      timeCreated: this._login.timeCreated || 0,
+      timeChanged: this._login.timePasswordChanged || 0,
+      timeUsed: this._login.timeLastUsed || 0,
     };
     document.l10n.setAttributes(this, "login-item", l10nArgs);
-    this.shadowRoot.querySelector(".title").textContent = this._login.title || "";
-    this.shadowRoot.querySelector(".hostname").textContent = this._login.hostname || "";
+
+    let title = this.shadowRoot.querySelector(".title");
+    title.textContent = this._login.title || title.getAttribute("new-login-title");
+    this.shadowRoot.querySelector(".hostname-saved-value").textContent = this._login.hostname || "";
     this.shadowRoot.querySelector("modal-input[name='username']").setAttribute("value", this._login.username || "");
     this.shadowRoot.querySelector("modal-input[name='password']").setAttribute("value", this._login.password || "");
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "AboutLoginsLoginSelected": {
         this.setLogin(event.detail);
@@ -148,61 +159,85 @@ class LoginItem extends ReflectedFluentE
         if (event.target.classList.contains("open-site-button")) {
           document.dispatchEvent(new CustomEvent("AboutLoginsOpenSite", {
             bubbles: true,
             detail: this._login,
           }));
           return;
         }
         if (event.target.classList.contains("save-changes-button")) {
-          let loginUpdates = {
-            guid: this._login.guid,
-          };
-          let formUsername = this.shadowRoot.querySelector("modal-input[name='username']").value.trim();
-          if (formUsername != this._login.username) {
-            loginUpdates.username = formUsername;
+          let loginUpdates = this._loginFromForm();
+          if (this._login.guid) {
+            loginUpdates.guid = this._login.guid;
+            document.dispatchEvent(new CustomEvent("AboutLoginsUpdateLogin", {
+              bubbles: true,
+              detail: loginUpdates,
+            }));
+          } else {
+            document.dispatchEvent(new CustomEvent("AboutLoginsCreateLogin", {
+              bubbles: true,
+              detail: loginUpdates,
+            }));
           }
-          let formPassword = this.shadowRoot.querySelector("modal-input[name='password']").value.trim();
-          if (formPassword != this._login.password) {
-            loginUpdates.password = formPassword;
-          }
-          document.dispatchEvent(new CustomEvent("AboutLoginsUpdateLogin", {
-            bubbles: true,
-            detail: loginUpdates,
-          }));
         }
         break;
       }
     }
   }
 
   setLogin(login) {
     this._login = login;
+    this.toggleAttribute("isNewLogin", !login.guid);
+    this.toggleEditing(!login.guid);
+    this.render();
+  }
+
+  loginAdded(login) {
+    if (this._login.guid ||
+        !window.AboutLoginsUtils.doLoginsMatch(login, this._loginFromForm())) {
+      return;
+    }
+
+    this.toggleEditing(false);
+    this._login = login;
     this.render();
   }
 
   loginModified(login) {
-    if (login.guid != this._login.guid) {
+    if (this._login.guid != login.guid) {
       return;
     }
 
     this._login = login;
-    this.toggleEditing(false);
     this.render();
   }
 
   loginRemoved(login) {
     if (login.guid != this._login.guid) {
       return;
     }
     this._login = {};
     this.render();
   }
 
   toggleEditing(force) {
     let shouldEdit = force !== undefined ? force : !this.hasAttribute("editing");
+
+    if (!shouldEdit) {
+      this.removeAttribute("isNewLogin");
+    }
+
     this.shadowRoot.querySelector(".edit-button").disabled = shouldEdit;
     this.shadowRoot.querySelectorAll("modal-input")
                    .forEach(el => el.toggleAttribute("editing", shouldEdit));
     this.toggleAttribute("editing", shouldEdit);
   }
+
+  _loginFromForm() {
+    return {
+      username: this.shadowRoot.querySelector("modal-input[name='username']").value.trim(),
+      password: this.shadowRoot.querySelector("modal-input[name='password']").value.trim(),
+      hostname: this.hasAttribute("isNewLogin") ? this.shadowRoot.querySelector("input[name='hostname']").value.trim()
+                                                : this.shadowRoot.querySelector(".hostname-saved-value").textContent,
+    };
+  }
 }
 customElements.define("login-item", LoginItem);
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -78,16 +78,24 @@ class LoginList extends ReflectedFluentE
   static get reflectedFluentIDs() {
     return ["count"];
   }
 
   static get observedAttributes() {
     return this.reflectedFluentIDs;
   }
 
+  clearSelection() {
+    if (!this._selectedItem) {
+      return;
+    }
+    this._selectedItem.classList.remove("selected");
+    this._selectedItem = null;
+  }
+
   setLogins(logins) {
     this._logins = logins;
     this.render();
   }
 
   loginAdded(login) {
     this._logins.push(login);
     let list = this.shadowRoot.querySelector("ol");
--- a/browser/components/aboutlogins/content/components/modal-input.css
+++ b/browser/components/aboutlogins/content/components/modal-input.css
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 :host {
   --reveal-button-opacity: .8;
   --reveal-button-opacity-hover: .6;
   --reveal-button-opacity-active: 1;
 
   display: flex;
+  align-items: center;
 }
 
 :host([editing]) .locked-value,
 :host(:not([editing])) .unlocked-value {
   display: none;
 }
 
 :host(:not([type="password"])) .reveal-checkbox {