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 225554 05c0e2b99f47f05ce78ad1056d77127da80db8b7
parent 225553 e4bc85cfd6e7467bcf764fd4c5d9f20e45d94de4
child 225555 c47ec0044f8c4d978f61f7518c503a48b3d60f2d
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer
bugs1038246
milestone34.0a2
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 (
@@ -319,17 +340,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,
       });
@@ -340,16 +365,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()],
@@ -371,31 +403,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 (
@@ -319,17 +340,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,
       });
@@ -340,16 +365,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()],
@@ -371,31 +403,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 {