Bug 1038246 - Desktop client needs the ability to edit a contact locally. r=mikedeboer
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Tue, 30 Sep 2014 13:10:23 +0100
changeset 231240 8d456f170b1e2f0bb822c2fbfc4a4f1d5354c105
parent 231239 95aefa6ce37fa4309806be1ccabe4048185c2efb
child 231241 6d875de038d472d7abad345474ad04bb7ba7c60a
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer
bugs1038246
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1038246 - Desktop client needs the ability to edit a contact locally. r=mikedeboer
browser/components/loop/content/js/contacts.js
browser/components/loop/content/js/contacts.jsx
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -15,16 +15,17 @@ loop.contacts = (function(_, mozL10n) {
   const ButtonGroup = loop.shared.views.ButtonGroup;
 
   // Number of contacts to add to the list at the same time.
   const CONTACTS_CHUNK_SIZE = 100;
 
   const ContactDropdown = React.createClass({displayName: 'ContactDropdown',
     propTypes: {
       handleAction: React.PropTypes.func.isRequired,
+      canEdit: React.PropTypes.bool
     },
 
     getInitialState: function () {
       return {
         openDirUp: false,
       };
     },
 
@@ -47,44 +48,46 @@ loop.contacts = (function(_, mozL10n) {
       }
     },
 
     onItemClick: function(event) {
       this.props.handleAction(event.currentTarget.dataset.action);
     },
 
     render: function() {
-      let dropdownMenu = React.addons.classSet({
-        "dropdown-menu": true,
-        "dropdown-menu-up": this.state.openDirUp,
-      });
+      var cx = React.addons.classSet;
 
       return (
-        React.DOM.ul({className: dropdownMenu}, 
-          React.DOM.li({className: "dropdown-menu-item disabled", 
+        React.DOM.ul({className: cx({ "dropdown-menu": true,
+                            "dropdown-menu-up": this.state.openDirUp })}, 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true,
+                              "disabled": true }), 
               onClick: this.onItemClick, 'data-action': "video-call"}, 
             React.DOM.i({className: "icon icon-video-call"}), 
             mozL10n.get("video_call_menu_button")
           ), 
-          React.DOM.li({className: "dropdown-menu-item disabled", 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true,
+                              "disabled": true }), 
               onClick: this.onItemClick, 'data-action': "audio-call"}, 
             React.DOM.i({className: "icon icon-audio-call"}), 
             mozL10n.get("audio_call_menu_button")
           ), 
-          React.DOM.li({className: "dropdown-menu-item", 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true,
+                              "disabled": !this.props.canEdit }), 
               onClick: this.onItemClick, 'data-action': "edit"}, 
             React.DOM.i({className: "icon icon-edit"}), 
             mozL10n.get("edit_contact_menu_button")
           ), 
-          React.DOM.li({className: "dropdown-menu-item disabled", 
+          React.DOM.li({className: "dropdown-menu-item", 
               onClick: this.onItemClick, 'data-action': "block"}, 
             React.DOM.i({className: "icon icon-block"}), 
             mozL10n.get("block_contact_menu_button")
           ), 
-          React.DOM.li({className: "dropdown-menu-item disabled", 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true,
+                              "disabled": !this.props.canEdit }), 
               onClick: this.onItemClick, 'data-action': "delete"}, 
             React.DOM.i({className: "icon icon-delete"}), 
             mozL10n.get("remove_contact_menu_button")
           )
         )
       );
     }
   });
@@ -116,19 +119,18 @@ loop.contacts = (function(_, mozL10n) {
       this.setState({showMenu: false});
     },
 
     componentWillUnmount: function() {
       document.body.removeEventListener("click", this._onBodyClick);
     },
 
     handleAction: function(actionName) {
-      console.error("Actions not implemented: " + actionName);
       if (this.props.handleContactAction) {
-        this.props.handleContactAction(this.props.key, actionName);
+        this.props.handleContactAction(this.props.contact, actionName);
       }
     },
 
     getContactNames: function() {
       // The model currently does not enforce a name to be present, but we're
       // going to assume it is awaiting more advanced validation of required fields
       // by the model. (See bug 1069918)
       // NOTE: this method of finding a firstname and lastname is not i18n-proof.
@@ -149,16 +151,22 @@ loop.contacts = (function(_, mozL10n) {
           email = address;
           return true;
         }
         return false;
       });
       return email;
     },
 
+    canEdit: function() {
+      // We cannot modify imported contacts.  For the moment, the check for
+      // determining whether the contact is imported is based on its category.
+      return this.props.contact.category[0] != "google";
+    },
+
     render: function() {
       let names = this.getContactNames();
       let email = this.getPreferredEmail();
       let cx = React.addons.classSet;
       let contactCSSClass = cx({
         contact: true,
         blocked: this.props.contact.blocked
       });
@@ -177,17 +185,18 @@ loop.contacts = (function(_, mozL10n) {
           ), 
           React.DOM.div({className: "icons"}, 
             React.DOM.i({className: "icon icon-video", 
                onClick: this.handleAction.bind(null, "video-call")}), 
             React.DOM.i({className: "icon icon-caret-down", 
                onClick: this.showDropdownMenu})
           ), 
           this.state.showMenu
-            ? ContactDropdown({handleAction: this.handleAction})
+            ? ContactDropdown({handleAction: this.handleAction, 
+                               canEdit: this.canEdit()})
             : null
           
         )
       );
     }
   });
 
   const ContactsList = React.createClass({displayName: 'ContactsList',
@@ -257,29 +266,41 @@ loop.contacts = (function(_, mozL10n) {
 
     handleImportButtonClick: function() {
     },
 
     handleAddContactButtonClick: function() {
       this.props.startForm("contacts_add");
     },
 
+    handleContactAction: function(contact, actionName) {
+      switch (actionName) {
+        case "edit":
+          this.props.startForm("contacts_edit", contact);
+          break;
+        default:
+          console.error("Unrecognized action: " + actionName);
+          break;
+      }
+    },
+
     sortContacts: function(contact1, contact2) {
       let comp = contact1.name[0].localeCompare(contact2.name[0]);
       if (comp !== 0) {
         return comp;
       }
       // If names are equal, compare against unique ids to make sure we have
       // consistent ordering.
       return contact1._guid - contact2._guid;
     },
 
     render: function() {
       let viewForItem = item => {
-        return ContactDetail({key: item._guid, contact: item})
+        return ContactDetail({key: item._guid, contact: item, 
+                              handleContactAction: this.handleContactAction})
       };
 
       let shownContacts = _.groupBy(this.state.contacts, function(contact) {
         return contact.blocked ? "blocked" : "available";
       });
 
       // Buttons are temporarily hidden using "style".
       return (
@@ -322,17 +343,21 @@ loop.contacts = (function(_, mozL10n) {
         pristine: true,
         name: "",
         email: "",
       };
     },
 
     initForm: function(contact) {
       let state = this.getInitialState();
-      state.contact = contact || null;
+      if (contact) {
+        state.contact = contact;
+        state.name = contact.name[0];
+        state.email = contact.email[0].value;
+      }
       this.setState(state);
     },
 
     handleAcceptButtonClick: function() {
       // Allow validity error indicators to be displayed.
       this.setState({
         pristine: false,
       });
@@ -343,16 +368,23 @@ loop.contacts = (function(_, mozL10n) {
       }
 
       this.props.selectTab("contacts");
 
       let contactsAPI = navigator.mozLoop.contacts;
 
       switch (this.props.mode) {
         case "edit":
+          this.state.contact.name[0] = this.state.name.trim();
+          this.state.contact.email[0].value = this.state.email.trim();
+          contactsAPI.update(this.state.contact, err => {
+            if (err) {
+              throw err;
+            }
+          });
           this.setState({
             contact: null,
           });
           break;
         case "add":
           contactsAPI.add({
             id: navigator.mozLoop.generateUUID(),
             name: [this.state.name.trim()],
@@ -374,31 +406,35 @@ loop.contacts = (function(_, mozL10n) {
     handleCancelButtonClick: function() {
       this.props.selectTab("contacts");
     },
 
     render: function() {
       let cx = React.addons.classSet;
       return (
         React.DOM.div({className: "content-area contact-form"}, 
-          React.DOM.header(null, mozL10n.get("add_contact_button")), 
+          React.DOM.header(null, this.props.mode == "add"
+                   ? mozL10n.get("add_contact_button")
+                   : mozL10n.get("edit_contact_title")), 
           React.DOM.label(null, mozL10n.get("edit_contact_name_label")), 
           React.DOM.input({ref: "name", required: true, pattern: "\\s*\\S.*", 
                  className: cx({pristine: this.state.pristine}), 
                  valueLink: this.linkState("name")}), 
           React.DOM.label(null, mozL10n.get("edit_contact_email_label")), 
           React.DOM.input({ref: "email", required: true, type: "email", 
                  className: cx({pristine: this.state.pristine}), 
                  valueLink: this.linkState("email")}), 
           ButtonGroup(null, 
             Button({additionalClass: "button-cancel", 
                     caption: mozL10n.get("cancel_button"), 
                     onClick: this.handleCancelButtonClick}), 
             Button({additionalClass: "button-accept", 
-                    caption: mozL10n.get("add_contact_button"), 
+                    caption: this.props.mode == "add"
+                             ? mozL10n.get("add_contact_button")
+                             : mozL10n.get("edit_contact_done_button"), 
                     onClick: this.handleAcceptButtonClick})
           )
         )
       );
     }
   });
 
   return {
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -15,16 +15,17 @@ loop.contacts = (function(_, mozL10n) {
   const ButtonGroup = loop.shared.views.ButtonGroup;
 
   // Number of contacts to add to the list at the same time.
   const CONTACTS_CHUNK_SIZE = 100;
 
   const ContactDropdown = React.createClass({
     propTypes: {
       handleAction: React.PropTypes.func.isRequired,
+      canEdit: React.PropTypes.bool
     },
 
     getInitialState: function () {
       return {
         openDirUp: false,
       };
     },
 
@@ -47,44 +48,46 @@ loop.contacts = (function(_, mozL10n) {
       }
     },
 
     onItemClick: function(event) {
       this.props.handleAction(event.currentTarget.dataset.action);
     },
 
     render: function() {
-      let dropdownMenu = React.addons.classSet({
-        "dropdown-menu": true,
-        "dropdown-menu-up": this.state.openDirUp,
-      });
+      var cx = React.addons.classSet;
 
       return (
-        <ul className={dropdownMenu}>
-          <li className="dropdown-menu-item disabled"
+        <ul className={cx({ "dropdown-menu": true,
+                            "dropdown-menu-up": this.state.openDirUp })}>
+          <li className={cx({ "dropdown-menu-item": true,
+                              "disabled": true })}
               onClick={this.onItemClick} data-action="video-call">
             <i className="icon icon-video-call" />
             {mozL10n.get("video_call_menu_button")}
           </li>
-          <li className="dropdown-menu-item disabled"
+          <li className={cx({ "dropdown-menu-item": true,
+                              "disabled": true })}
               onClick={this.onItemClick} data-action="audio-call">
             <i className="icon icon-audio-call" />
             {mozL10n.get("audio_call_menu_button")}
           </li>
-          <li className="dropdown-menu-item"
+          <li className={cx({ "dropdown-menu-item": true,
+                              "disabled": !this.props.canEdit })}
               onClick={this.onItemClick} data-action="edit">
             <i className="icon icon-edit" />
             {mozL10n.get("edit_contact_menu_button")}
           </li>
-          <li className="dropdown-menu-item disabled"
+          <li className="dropdown-menu-item"
               onClick={this.onItemClick} data-action="block">
             <i className="icon icon-block" />
             {mozL10n.get("block_contact_menu_button")}
           </li>
-          <li className="dropdown-menu-item disabled"
+          <li className={cx({ "dropdown-menu-item": true,
+                              "disabled": !this.props.canEdit })}
               onClick={this.onItemClick} data-action="delete">
             <i className="icon icon-delete" />
             {mozL10n.get("remove_contact_menu_button")}
           </li>
         </ul>
       );
     }
   });
@@ -116,19 +119,18 @@ loop.contacts = (function(_, mozL10n) {
       this.setState({showMenu: false});
     },
 
     componentWillUnmount: function() {
       document.body.removeEventListener("click", this._onBodyClick);
     },
 
     handleAction: function(actionName) {
-      console.error("Actions not implemented: " + actionName);
       if (this.props.handleContactAction) {
-        this.props.handleContactAction(this.props.key, actionName);
+        this.props.handleContactAction(this.props.contact, actionName);
       }
     },
 
     getContactNames: function() {
       // The model currently does not enforce a name to be present, but we're
       // going to assume it is awaiting more advanced validation of required fields
       // by the model. (See bug 1069918)
       // NOTE: this method of finding a firstname and lastname is not i18n-proof.
@@ -149,16 +151,22 @@ loop.contacts = (function(_, mozL10n) {
           email = address;
           return true;
         }
         return false;
       });
       return email;
     },
 
+    canEdit: function() {
+      // We cannot modify imported contacts.  For the moment, the check for
+      // determining whether the contact is imported is based on its category.
+      return this.props.contact.category[0] != "google";
+    },
+
     render: function() {
       let names = this.getContactNames();
       let email = this.getPreferredEmail();
       let cx = React.addons.classSet;
       let contactCSSClass = cx({
         contact: true,
         blocked: this.props.contact.blocked
       });
@@ -177,17 +185,18 @@ loop.contacts = (function(_, mozL10n) {
           </div>
           <div className="icons">
             <i className="icon icon-video"
                onClick={this.handleAction.bind(null, "video-call")} />
             <i className="icon icon-caret-down"
                onClick={this.showDropdownMenu} />
           </div>
           {this.state.showMenu
-            ? <ContactDropdown handleAction={this.handleAction} />
+            ? <ContactDropdown handleAction={this.handleAction}
+                               canEdit={this.canEdit()} />
             : null
           }
         </li>
       );
     }
   });
 
   const ContactsList = React.createClass({
@@ -257,29 +266,41 @@ loop.contacts = (function(_, mozL10n) {
 
     handleImportButtonClick: function() {
     },
 
     handleAddContactButtonClick: function() {
       this.props.startForm("contacts_add");
     },
 
+    handleContactAction: function(contact, actionName) {
+      switch (actionName) {
+        case "edit":
+          this.props.startForm("contacts_edit", contact);
+          break;
+        default:
+          console.error("Unrecognized action: " + actionName);
+          break;
+      }
+    },
+
     sortContacts: function(contact1, contact2) {
       let comp = contact1.name[0].localeCompare(contact2.name[0]);
       if (comp !== 0) {
         return comp;
       }
       // If names are equal, compare against unique ids to make sure we have
       // consistent ordering.
       return contact1._guid - contact2._guid;
     },
 
     render: function() {
       let viewForItem = item => {
-        return <ContactDetail key={item._guid} contact={item} />
+        return <ContactDetail key={item._guid} contact={item}
+                              handleContactAction={this.handleContactAction} />
       };
 
       let shownContacts = _.groupBy(this.state.contacts, function(contact) {
         return contact.blocked ? "blocked" : "available";
       });
 
       // Buttons are temporarily hidden using "style".
       return (
@@ -322,17 +343,21 @@ loop.contacts = (function(_, mozL10n) {
         pristine: true,
         name: "",
         email: "",
       };
     },
 
     initForm: function(contact) {
       let state = this.getInitialState();
-      state.contact = contact || null;
+      if (contact) {
+        state.contact = contact;
+        state.name = contact.name[0];
+        state.email = contact.email[0].value;
+      }
       this.setState(state);
     },
 
     handleAcceptButtonClick: function() {
       // Allow validity error indicators to be displayed.
       this.setState({
         pristine: false,
       });
@@ -343,16 +368,23 @@ loop.contacts = (function(_, mozL10n) {
       }
 
       this.props.selectTab("contacts");
 
       let contactsAPI = navigator.mozLoop.contacts;
 
       switch (this.props.mode) {
         case "edit":
+          this.state.contact.name[0] = this.state.name.trim();
+          this.state.contact.email[0].value = this.state.email.trim();
+          contactsAPI.update(this.state.contact, err => {
+            if (err) {
+              throw err;
+            }
+          });
           this.setState({
             contact: null,
           });
           break;
         case "add":
           contactsAPI.add({
             id: navigator.mozLoop.generateUUID(),
             name: [this.state.name.trim()],
@@ -374,31 +406,35 @@ loop.contacts = (function(_, mozL10n) {
     handleCancelButtonClick: function() {
       this.props.selectTab("contacts");
     },
 
     render: function() {
       let cx = React.addons.classSet;
       return (
         <div className="content-area contact-form">
-          <header>{mozL10n.get("add_contact_button")}</header>
+          <header>{this.props.mode == "add"
+                   ? mozL10n.get("add_contact_button")
+                   : mozL10n.get("edit_contact_title")}</header>
           <label>{mozL10n.get("edit_contact_name_label")}</label>
           <input ref="name" required pattern="\s*\S.*"
                  className={cx({pristine: this.state.pristine})}
                  valueLink={this.linkState("name")} />
           <label>{mozL10n.get("edit_contact_email_label")}</label>
           <input ref="email" required type="email"
                  className={cx({pristine: this.state.pristine})}
                  valueLink={this.linkState("email")} />
           <ButtonGroup>
             <Button additionalClass="button-cancel"
                     caption={mozL10n.get("cancel_button")}
                     onClick={this.handleCancelButtonClick} />
             <Button additionalClass="button-accept"
-                    caption={mozL10n.get("add_contact_button")}
+                    caption={this.props.mode == "add"
+                             ? mozL10n.get("add_contact_button")
+                             : mozL10n.get("edit_contact_done_button")}
                     onClick={this.handleAcceptButtonClick} />
           </ButtonGroup>
         </div>
       );
     }
   });
 
   return {