From 722f332c16bb959dc690f12396aacd840d71ac6f Mon Sep 17 00:00:00 2001 From: Jared Wein Date: Tue, 11 Nov 2014 18:40:54 -0500 Subject: [PATCH 1/7] Bug 1081322 - Add a phone number field to the Add/Edit Contact view. r=dmose --HG-- extra : rebase_source : cbd5fbf8f1b4d26690c614ebb02d16e577fe9c3a --- .../components/loop/content/js/contacts.js | 90 ++++++++-- .../components/loop/content/js/contacts.jsx | 90 ++++++++-- .../loop/test/desktop-local/contacts_test.js | 166 ++++++++++++++++++ .../loop/test/desktop-local/index.html | 1 + 4 files changed, 323 insertions(+), 24 deletions(-) create mode 100644 browser/components/loop/test/desktop-local/contacts_test.js diff --git a/browser/components/loop/content/js/contacts.js b/browser/components/loop/content/js/contacts.js index dc5a97f5528..de1a6ea28a9 100644 --- a/browser/components/loop/content/js/contacts.js +++ b/browser/components/loop/content/js/contacts.js @@ -33,12 +33,55 @@ loop.contacts = (function(_, mozL10n) { }; }; - let getPreferredEmail = function(contact) { - // A contact may not contain email addresses, but only a phone number. - if (!contact.email || contact.email.length == 0) { + /** Used to retrieve the preferred email or phone number + * for the contact. Both fields are optional. + * @param {object} contact + * The contact object to get the field from. + * @param {string} field + * The field that should be read out of the contact object. + * @returns {object} An object with a 'value' property that hold a string value. + */ + let getPreferred = function(contact, field) { + if (!contact[field] || !contact[field].length) { return { value: "" }; } - return contact.email.find(e => e.pref) || contact.email[0]; + return contact[field].find(e => e.pref) || contact[field][0]; + }; + + /** Used to set the preferred email or phone number + * for the contact. Both fields are optional. + * @param {object} contact + * The contact object to get the field from. + * @param {object} data + * An object that has a 'field' property. If the 'optional' field + * is defined and true, then the field will only be set if the + * 'value' is not empty or if the previous value was defined. + */ + let setPreferred = function(contact, data) { + if (data.optional) { + // Don't clear the field if it doesn't exist. + if (!data.value && + (!contact[data.field] || !contact[data.field].length)) { + return; + } + } + + if (!contact[data.field]) { + contact[data.field] = []; + } + + if (!contact[data.field].length) { + contact[data.field][0] = {"value": data.value}; + return; + } + // Set the value in the preferred tuple and return. + for (let i in contact[data.field]) { + if (contact[data.field][i].pref) { + contact[data.field][i].value = data.value; + return; + } + } + contact[data.field][0].value = data.value; }; const ContactDropdown = React.createClass({displayName: 'ContactDropdown', @@ -159,10 +202,12 @@ loop.contacts = (function(_, mozL10n) { shouldComponentUpdate: function(nextProps, nextState) { let currContact = this.props.contact; let nextContact = nextProps.contact; + let currContactEmail = getPreferred(currContact, "email").value; + let nextContactEmail = getPreferred(nextContact, "email").value; return ( currContact.name[0] !== nextContact.name[0] || currContact.blocked !== nextContact.blocked || - getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value || + currContactEmail !== nextContactEmail || nextState.showMenu !== this.state.showMenu ); }, @@ -181,7 +226,7 @@ loop.contacts = (function(_, mozL10n) { render: function() { let names = getContactNames(this.props.contact); - let email = getPreferredEmail(this.props.contact); + let email = getPreferred(this.props.contact, "email"); let cx = React.addons.classSet; let contactCSSClass = cx({ contact: true, @@ -435,7 +480,7 @@ loop.contacts = (function(_, mozL10n) { if (filter) { let filterFn = contact => { return contact.name[0].toLocaleLowerCase().contains(filter) || - getPreferredEmail(contact).value.toLocaleLowerCase().contains(filter); + getPreferred(contact, "email").value.toLocaleLowerCase().contains(filter); }; if (shownContacts.available) { shownContacts.available = shownContacts.available.filter(filterFn); @@ -497,6 +542,7 @@ loop.contacts = (function(_, mozL10n) { pristine: true, name: "", email: "", + tel: "", }; }, @@ -505,7 +551,8 @@ loop.contacts = (function(_, mozL10n) { if (contact) { state.contact = contact; state.name = contact.name[0]; - state.email = contact.email[0].value; + state.email = getPreferred(contact, "email").value; + state.tel = getPreferred(contact, "tel").value; } this.setState(state); }, @@ -528,7 +575,10 @@ loop.contacts = (function(_, mozL10n) { 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(); + setPreferred(this.state.contact, + {field: "email", value: this.state.email.trim()}); + setPreferred(this.state.contact, + {field: "tel", value: this.state.tel.trim(), optional: true}); contactsAPI.update(this.state.contact, err => { if (err) { throw err; @@ -539,7 +589,7 @@ loop.contacts = (function(_, mozL10n) { }); break; case "add": - contactsAPI.add({ + var contact = { id: navigator.mozLoop.generateUUID(), name: [this.state.name.trim()], email: [{ @@ -548,7 +598,16 @@ loop.contacts = (function(_, mozL10n) { value: this.state.email.trim() }], category: ["local"] - }, err => { + }; + var tel = this.state.tel.trim(); + if (!!tel) { + contact["tel"] = [{ + pref: true, + type: ["fxos"], + value: tel + }]; + } + contactsAPI.add(contact, err => { if (err) { throw err; } @@ -563,19 +622,24 @@ loop.contacts = (function(_, mozL10n) { render: function() { let cx = React.addons.classSet; + // XXX do we need the ref="" attributes below? return ( React.DOM.div({className: "content-area contact-form"}, 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.*", + React.DOM.input({ref: "name", required: true, pattern: "\\s*\\S.*", type: "text", 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")}), + React.DOM.label(null, mozL10n.get("new_contact_phone_placeholder")), + React.DOM.input({ref: "tel", type: "tel", + className: cx({pristine: this.state.pristine}), + valueLink: this.linkState("tel")}), ButtonGroup(null, Button({additionalClass: "button-cancel", caption: mozL10n.get("cancel_button"), @@ -594,5 +658,7 @@ loop.contacts = (function(_, mozL10n) { return { ContactsList: ContactsList, ContactDetailsForm: ContactDetailsForm, + _getPreferred: getPreferred, + _setPreferred: setPreferred, }; })(_, document.mozL10n); diff --git a/browser/components/loop/content/js/contacts.jsx b/browser/components/loop/content/js/contacts.jsx index dad51b4eb25..1480dccbc76 100644 --- a/browser/components/loop/content/js/contacts.jsx +++ b/browser/components/loop/content/js/contacts.jsx @@ -33,12 +33,55 @@ loop.contacts = (function(_, mozL10n) { }; }; - let getPreferredEmail = function(contact) { - // A contact may not contain email addresses, but only a phone number. - if (!contact.email || contact.email.length == 0) { + /** Used to retrieve the preferred email or phone number + * for the contact. Both fields are optional. + * @param {object} contact + * The contact object to get the field from. + * @param {string} field + * The field that should be read out of the contact object. + * @returns {object} An object with a 'value' property that hold a string value. + */ + let getPreferred = function(contact, field) { + if (!contact[field] || !contact[field].length) { return { value: "" }; } - return contact.email.find(e => e.pref) || contact.email[0]; + return contact[field].find(e => e.pref) || contact[field][0]; + }; + + /** Used to set the preferred email or phone number + * for the contact. Both fields are optional. + * @param {object} contact + * The contact object to get the field from. + * @param {object} data + * An object that has a 'field' property. If the 'optional' field + * is defined and true, then the field will only be set if the + * 'value' is not empty or if the previous value was defined. + */ + let setPreferred = function(contact, data) { + if (data.optional) { + // Don't clear the field if it doesn't exist. + if (!data.value && + (!contact[data.field] || !contact[data.field].length)) { + return; + } + } + + if (!contact[data.field]) { + contact[data.field] = []; + } + + if (!contact[data.field].length) { + contact[data.field][0] = {"value": data.value}; + return; + } + // Set the value in the preferred tuple and return. + for (let i in contact[data.field]) { + if (contact[data.field][i].pref) { + contact[data.field][i].value = data.value; + return; + } + } + contact[data.field][0].value = data.value; }; const ContactDropdown = React.createClass({ @@ -159,10 +202,12 @@ loop.contacts = (function(_, mozL10n) { shouldComponentUpdate: function(nextProps, nextState) { let currContact = this.props.contact; let nextContact = nextProps.contact; + let currContactEmail = getPreferred(currContact, "email").value; + let nextContactEmail = getPreferred(nextContact, "email").value; return ( currContact.name[0] !== nextContact.name[0] || currContact.blocked !== nextContact.blocked || - getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value || + currContactEmail !== nextContactEmail || nextState.showMenu !== this.state.showMenu ); }, @@ -181,7 +226,7 @@ loop.contacts = (function(_, mozL10n) { render: function() { let names = getContactNames(this.props.contact); - let email = getPreferredEmail(this.props.contact); + let email = getPreferred(this.props.contact, "email"); let cx = React.addons.classSet; let contactCSSClass = cx({ contact: true, @@ -435,7 +480,7 @@ loop.contacts = (function(_, mozL10n) { if (filter) { let filterFn = contact => { return contact.name[0].toLocaleLowerCase().contains(filter) || - getPreferredEmail(contact).value.toLocaleLowerCase().contains(filter); + getPreferred(contact, "email").value.toLocaleLowerCase().contains(filter); }; if (shownContacts.available) { shownContacts.available = shownContacts.available.filter(filterFn); @@ -497,6 +542,7 @@ loop.contacts = (function(_, mozL10n) { pristine: true, name: "", email: "", + tel: "", }; }, @@ -505,7 +551,8 @@ loop.contacts = (function(_, mozL10n) { if (contact) { state.contact = contact; state.name = contact.name[0]; - state.email = contact.email[0].value; + state.email = getPreferred(contact, "email").value; + state.tel = getPreferred(contact, "tel").value; } this.setState(state); }, @@ -528,7 +575,10 @@ loop.contacts = (function(_, mozL10n) { 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(); + setPreferred(this.state.contact, + {field: "email", value: this.state.email.trim()}); + setPreferred(this.state.contact, + {field: "tel", value: this.state.tel.trim(), optional: true}); contactsAPI.update(this.state.contact, err => { if (err) { throw err; @@ -539,7 +589,7 @@ loop.contacts = (function(_, mozL10n) { }); break; case "add": - contactsAPI.add({ + var contact = { id: navigator.mozLoop.generateUUID(), name: [this.state.name.trim()], email: [{ @@ -548,7 +598,16 @@ loop.contacts = (function(_, mozL10n) { value: this.state.email.trim() }], category: ["local"] - }, err => { + }; + var tel = this.state.tel.trim(); + if (!!tel) { + contact["tel"] = [{ + pref: true, + type: ["fxos"], + value: tel + }]; + } + contactsAPI.add(contact, err => { if (err) { throw err; } @@ -563,19 +622,24 @@ loop.contacts = (function(_, mozL10n) { render: function() { let cx = React.addons.classSet; + // XXX do we need the ref="" attributes below? return (
{this.props.mode == "add" ? mozL10n.get("add_contact_button") : mozL10n.get("edit_contact_title")}
- + +