Bug 1076764 - Added notifications for Loop contacts import. r=Standard8
authorNicolas Perriault <nperriault@mozilla.com>
Fri, 16 Jan 2015 12:26:25 +0100
changeset 224315 4cd191478d41ac99778e1e991b0e93e3bf4e7d89
parent 224314 0d7bbd6633c3a709f0433395329c884d9c970302
child 224316 b1424a861ccab26af2f6591c715b6d51a23f24bd
push id54190
push userkwierso@gmail.com
push dateSat, 17 Jan 2015 02:06:29 +0000
treeherdermozilla-inbound@369a8f14ccf8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1076764
milestone38.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 1076764 - Added notifications for Loop contacts import. r=Standard8
browser/components/loop/content/js/contacts.js
browser/components/loop/content/js/contacts.jsx
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/content/shared/css/common.css
browser/components/loop/content/shared/js/models.js
browser/components/loop/test/desktop-local/contacts_test.js
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
browser/locales/en-US/chrome/browser/loop/loop.properties
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -258,16 +258,21 @@ loop.contacts = (function(_, mozL10n) {
   });
 
   const ContactsList = React.createClass({displayName: "ContactsList",
     mixins: [
       React.addons.LinkedStateMixin,
       loop.shared.mixins.WindowCloseMixin
     ],
 
+    propTypes: {
+      notifications: React.PropTypes.instanceOf(
+        loop.shared.models.NotificationCollection).isRequired
+    },
+
     /**
      * Contacts collection object
      */
     contacts: null,
 
     /**
      * User profile
      */
@@ -384,20 +389,24 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleImportButtonClick: function() {
       this.setState({ importBusy: true });
       navigator.mozLoop.startImport({
         service: "google"
       }, (err, stats) => {
         this.setState({ importBusy: false });
-        // TODO: bug 1076764 - proper error and success reporting.
         if (err) {
-          throw err;
+          console.error("Contact import error", err);
+          this.props.notifications.errorL10n("import_contacts_failure_message");
+          return;
         }
+        this.props.notifications.successL10n("import_contacts_success_message", {
+          total: stats.total
+        });
       });
     },
 
     handleAddContactButtonClick: function() {
       this.props.startForm("contacts_add");
     },
 
     handleContactAction: function(contact, actionName) {
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -258,16 +258,21 @@ loop.contacts = (function(_, mozL10n) {
   });
 
   const ContactsList = React.createClass({
     mixins: [
       React.addons.LinkedStateMixin,
       loop.shared.mixins.WindowCloseMixin
     ],
 
+    propTypes: {
+      notifications: React.PropTypes.instanceOf(
+        loop.shared.models.NotificationCollection).isRequired
+    },
+
     /**
      * Contacts collection object
      */
     contacts: null,
 
     /**
      * User profile
      */
@@ -384,20 +389,24 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleImportButtonClick: function() {
       this.setState({ importBusy: true });
       navigator.mozLoop.startImport({
         service: "google"
       }, (err, stats) => {
         this.setState({ importBusy: false });
-        // TODO: bug 1076764 - proper error and success reporting.
         if (err) {
-          throw err;
+          console.error("Contact import error", err);
+          this.props.notifications.errorL10n("import_contacts_failure_message");
+          return;
         }
+        this.props.notifications.successL10n("import_contacts_success_message", {
+          total: stats.total
+        });
       });
     },
 
     handleAddContactButtonClick: function() {
       this.props.startForm("contacts_add");
     },
 
     handleContactAction: function(contact, actionName) {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -984,17 +984,18 @@ loop.panel = (function(_, mozL10n) {
         React.createElement("div", null, 
           React.createElement(NotificationListView, {notifications: this.props.notifications, 
                                 clearOnDocumentHidden: true}), 
           React.createElement(TabView, {ref: "tabView", selectedTab: this.props.selectedTab, 
             buttonsHidden: hideButtons}, 
             this._renderRoomsOrCallTab(), 
             React.createElement(Tab, {name: "contacts"}, 
               React.createElement(ContactsList, {selectTab: this.selectTab, 
-                            startForm: this.startForm})
+                            startForm: this.startForm, 
+                            notifications: this.props.notifications})
             ), 
             React.createElement(Tab, {name: "contacts_add", hidden: true}, 
               React.createElement(ContactDetailsForm, {ref: "contacts_add", mode: "add", 
                                   selectTab: this.selectTab})
             ), 
             React.createElement(Tab, {name: "contacts_edit", hidden: true}, 
               React.createElement(ContactDetailsForm, {ref: "contacts_edit", mode: "edit", 
                                   selectTab: this.selectTab})
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -984,17 +984,18 @@ loop.panel = (function(_, mozL10n) {
         <div>
           <NotificationListView notifications={this.props.notifications}
                                 clearOnDocumentHidden={true} />
           <TabView ref="tabView" selectedTab={this.props.selectedTab}
             buttonsHidden={hideButtons}>
             {this._renderRoomsOrCallTab()}
             <Tab name="contacts">
               <ContactsList selectTab={this.selectTab}
-                            startForm={this.startForm} />
+                            startForm={this.startForm}
+                            notifications={this.props.notifications} />
             </Tab>
             <Tab name="contacts_add" hidden={true}>
               <ContactDetailsForm ref="contacts_add" mode="add"
                                   selectTab={this.selectTab} />
             </Tab>
             <Tab name="contacts_edit" hidden={true}>
               <ContactDetailsForm ref="contacts_edit" mode="edit"
                                   selectTab={this.selectTab} />
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -258,16 +258,22 @@ p {
   color: #fff;
 }
 
 .alert-warning {
   background: #fcf8e3;
   border: 1px solid #fbeed5;
 }
 
+.alert-success {
+  background: #5BC0A4;
+  border: 1px solid #5BC0A4;
+  color: #fff;
+}
+
 .notificationContainer > .details-error {
   background: #fbebeb;
   color: #d74345
 }
 
 .notificationContainer > .details-error > .detailsButton {
   float: right;
   -moz-margin-start: 1em; /* Match .detailsBar padding */
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -417,16 +417,37 @@ loop.shared.models = (function(l10n) {
      *
      * @param  {String} messageId L10n message id
      * @param  {Object} [l10nProps] An object with variables to be interpolated
      *                  into the translation. All members' values must be
      *                  strings or numbers.
      */
     errorL10n: function(messageId, l10nProps) {
       this.error(l10n.get(messageId, l10nProps));
+    },
+
+    /**
+     * Adds a success notification to the stack and renders it.
+     *
+     * @return {String} message
+     */
+    success: function(message) {
+      this.add({level: "success", message: message});
+    },
+
+    /**
+     * Adds a l10n success notification to the stack and renders it.
+     *
+     * @param  {String} messageId L10n message id
+     * @param  {Object} [l10nProps] An object with variables to be interpolated
+     *                  into the translation. All members' values must be
+     *                  strings or numbers.
+     */
+    successL10n: function(messageId, l10nProps) {
+      this.success(l10n.get(messageId, l10nProps));
     }
   });
 
   return {
     ConversationModel: ConversationModel,
     NotificationCollection: NotificationCollection,
     NotificationModel: NotificationModel
   };
--- a/browser/components/loop/test/desktop-local/contacts_test.js
+++ b/browser/components/loop/test/desktop-local/contacts_test.js
@@ -11,16 +11,17 @@ var TestUtils = React.addons.TestUtils;
 describe("loop.contacts", function() {
   "use strict";
 
   var fakeAddContactButtonText = "Fake Add Contact";
   var fakeEditContactButtonText = "Fake Edit Contact";
   var fakeDoneButtonText = "Fake Done";
   var sandbox;
   var fakeWindow;
+  var notifications;
 
   beforeEach(function(done) {
     sandbox = sinon.sandbox.create();
     navigator.mozLoop = {
       getStrings: function(entityName) {
         var textContentValue = "fakeText";
         if (entityName == "add_contact_button") {
           textContentValue = fakeAddContactButtonText;
@@ -54,18 +55,21 @@ describe("loop.contacts", function() {
 
     beforeEach(function() {
       navigator.mozLoop.calls = {
         startDirectCall: sandbox.stub(),
         clearCallInProgress: sandbox.stub()
       };
       navigator.mozLoop.contacts = {getAll: sandbox.stub()};
 
+      notifications = new loop.shared.models.NotificationCollection();
       listView = TestUtils.renderIntoDocument(
-        React.createElement(loop.contacts.ContactsList));
+        React.createElement(loop.contacts.ContactsList, {
+          notifications: notifications
+        }));
     });
 
     afterEach(function() {
       listView = null;
       delete navigator.mozLoop.calls;
       delete navigator.mozLoop.contacts;
     });
 
@@ -79,16 +83,44 @@ describe("loop.contacts", function() {
 
       it("should call window.close when called with 'audio-call' action",
         function() {
           listView.handleContactAction({}, "audio-call");
 
           sinon.assert.calledOnce(fakeWindow.close);
         });
     });
+
+    describe("#handleImportButtonClick", function() {
+      it("should notify the end user from a succesful import", function() {
+        sandbox.stub(notifications, "successL10n");
+        navigator.mozLoop.startImport = function(opts, cb) {
+          cb(null, {total: 42});
+        };
+
+        listView.handleImportButtonClick();
+
+        sinon.assert.calledWithExactly(
+          notifications.successL10n,
+          "import_contacts_success_message",
+          {total: 42});
+      });
+
+      it("should notify the end user from any encountered error", function() {
+        sandbox.stub(notifications, "errorL10n");
+        navigator.mozLoop.startImport = function(opts, cb) {
+          cb(new Error("fake error"));
+        };
+
+        listView.handleImportButtonClick();
+
+        sinon.assert.calledWithExactly(notifications.errorL10n,
+                                       "import_contacts_failure_message");
+      });
+    });
   });
 
   describe("ContactDetailsForm", function() {
     describe("#render", function() {
       describe("add mode", function() {
         var view;
 
         beforeEach(function() {
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -263,16 +263,32 @@
             ), 
             React.createElement(Example, {summary: "Room list tab", dashed: "true", style: {width: "332px"}}, 
               React.createElement(PanelView, {client: mockClient, notifications: notifications, 
                          userProfile: {email: "test@example.com"}, 
                          mozLoop: mockMozLoopRooms, 
                          dispatcher: dispatcher, 
                          roomStore: roomStore, 
                          selectedTab: "rooms"})
+            ), 
+            React.createElement(Example, {summary: "Contact import success", dashed: "true", style: {width: "332px"}}, 
+              React.createElement(PanelView, {notifications: new loop.shared.models.NotificationCollection([{level: "success", message: "Import success"}]), 
+                         userProfile: {email: "test@example.com"}, 
+                         mozLoop: mockMozLoopRooms, 
+                         dispatcher: dispatcher, 
+                         roomStore: roomStore, 
+                         selectedTab: "contacts"})
+            ), 
+            React.createElement(Example, {summary: "Contact import error", dashed: "true", style: {width: "332px"}}, 
+              React.createElement(PanelView, {notifications: new loop.shared.models.NotificationCollection([{level: "error", message: "Import error"}]), 
+                         userProfile: {email: "test@example.com"}, 
+                         mozLoop: mockMozLoopRooms, 
+                         dispatcher: dispatcher, 
+                         roomStore: roomStore, 
+                         selectedTab: "contacts"})
             )
           ), 
 
           React.createElement(Section, {name: "IncomingCallView"}, 
             React.createElement(Example, {summary: "Default / incoming video call", dashed: "true", style: {width: "260px", height: "254px"}}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(IncomingCallView, {model: mockConversationModel, 
                                   video: true})
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -264,16 +264,32 @@
             <Example summary="Room list tab" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={notifications}
                          userProfile={{email: "test@example.com"}}
                          mozLoop={mockMozLoopRooms}
                          dispatcher={dispatcher}
                          roomStore={roomStore}
                          selectedTab="rooms" />
             </Example>
+            <Example summary="Contact import success" dashed="true" style={{width: "332px"}}>
+              <PanelView notifications={new loop.shared.models.NotificationCollection([{level: "success", message: "Import success"}])}
+                         userProfile={{email: "test@example.com"}}
+                         mozLoop={mockMozLoopRooms}
+                         dispatcher={dispatcher}
+                         roomStore={roomStore}
+                         selectedTab="contacts" />
+            </Example>
+            <Example summary="Contact import error" dashed="true" style={{width: "332px"}}>
+              <PanelView notifications={new loop.shared.models.NotificationCollection([{level: "error", message: "Import error"}])}
+                         userProfile={{email: "test@example.com"}}
+                         mozLoop={mockMozLoopRooms}
+                         dispatcher={dispatcher}
+                         roomStore={roomStore}
+                         selectedTab="contacts" />
+            </Example>
           </Section>
 
           <Section name="IncomingCallView">
             <Example summary="Default / incoming video call" dashed="true" style={{width: "260px", height: "254px"}}>
               <div className="fx-embedded">
                 <IncomingCallView model={mockConversationModel}
                                   video={true} />
               </div>
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -115,16 +115,23 @@ valid_email_text_description=Please ente
 ## LOCALIZATION NOTE (add_or_import_contact_title): This is the subtitle of the panel
 ## at https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
 add_or_import_contact_title=Add or Import Contact
 ## LOCALIZATION NOTE (import_contacts_button, importing_contacts_progress_button):
 ## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
 ## for where these appear on the UI
 import_contacts_button=Import
 importing_contacts_progress_button=Importing…
+import_contacts_failure_message=Some contacts could not be imported. Please try again.
+## LOCALIZATION NOTE(import_contacts_success_message): Success notification message
+## when user's contacts have been successfully imported.
+## Semicolon-separated list of plural forms. See:
+## http://developer.mozilla.org/en/docs/Localization_and_Plurals
+## In this item, don't translate the part between {{..}}
+import_contacts_success_message={{total}} contact was successfully imported.;{{total}} contacts were successfully imported.
 ## LOCALIZATION NOTE(sync_contacts_button): This button is displayed in place of
 ## importing_contacts_button once contacts have been imported once.
 sync_contacts_button=Sync Contacts
 
 ## LOCALIZATION NOTE(import_failed_description simple): Displayed when an import of
 ## contacts fails. This is displayed in the error field here:
 ## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error
 import_failed_description_simple=Sorry, contact import failed