From 43959e4205ef6fc2c24ccf38e8c1d7f71377fac1 Mon Sep 17 00:00:00 2001
From: Paul Kaplan <pkaplan@media.mit.edu>
Date: Wed, 5 Dec 2018 10:46:22 -0500
Subject: [PATCH] Add "SaveStatus" component to control showing "Save Now" and
 save alerts

---
 src/components/menu-bar/menu-bar.jsx      |  4 +-
 src/components/menu-bar/save-status.css   |  4 ++
 src/components/menu-bar/save-status.jsx   | 61 ++++++++++++++++++
 test/unit/containers/save-status.test.jsx | 76 +++++++++++++++++++++++
 4 files changed, 143 insertions(+), 2 deletions(-)
 create mode 100644 src/components/menu-bar/save-status.css
 create mode 100644 src/components/menu-bar/save-status.jsx
 create mode 100644 test/unit/containers/save-status.test.jsx

diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx
index a9ebe0c5b..878e6a7b4 100644
--- a/src/components/menu-bar/menu-bar.jsx
+++ b/src/components/menu-bar/menu-bar.jsx
@@ -12,7 +12,7 @@ import ShareButton from './share-button.jsx';
 import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
 import Divider from '../divider/divider.jsx';
 import LanguageSelector from '../../containers/language-selector.jsx';
-import InlineMessages from '../../containers/inline-messages.jsx';
+import SaveStatus from './save-status.jsx';
 import SBFileUploader from '../../containers/sb-file-uploader.jsx';
 import ProjectWatcher from '../../containers/project-watcher.jsx';
 import MenuBarMenu from './menu-bar-menu.jsx';
@@ -535,7 +535,7 @@ class MenuBar extends React.Component {
                 logged in, and whether a session is available to log in with */}
                 <div className={styles.accountInfoGroup}>
                     <div className={styles.menuBarItem}>
-                        <InlineMessages />
+                        <SaveStatus />
                     </div>
                     {this.props.sessionExists ? (
                         this.props.username ? (
diff --git a/src/components/menu-bar/save-status.css b/src/components/menu-bar/save-status.css
new file mode 100644
index 000000000..d191d5d23
--- /dev/null
+++ b/src/components/menu-bar/save-status.css
@@ -0,0 +1,4 @@
+.save-now {
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    cursor: pointer;
+}
diff --git a/src/components/menu-bar/save-status.jsx b/src/components/menu-bar/save-status.jsx
new file mode 100644
index 000000000..9f83f6274
--- /dev/null
+++ b/src/components/menu-bar/save-status.jsx
@@ -0,0 +1,61 @@
+import {connect} from 'react-redux';
+import {FormattedMessage} from 'react-intl';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import InlineMessages from '../../containers/inline-messages.jsx';
+
+import {
+    manualUpdateProject
+} from '../../reducers/project-state';
+
+import {
+    filterInlineAlerts
+} from '../../reducers/alerts';
+
+import styles from './save-status.css';
+
+// Wrapper for inline messages in the nav bar, which are all related to saving.
+// Show any inline messages if present, else show the "Save Now" button if the
+// project has changed.
+// We decided to not use an inline message for "Save Now" because it is a reflection
+// of the project state, rather than an event.
+const SaveStatus = ({
+    alertsList,
+    projectChanged,
+    onClickSave
+}) => (
+    filterInlineAlerts(alertsList).length > 0 ? (
+        <InlineMessages />
+    ) : projectChanged && (
+        <div
+            className={styles.saveNow}
+            onClick={onClickSave}
+        >
+            <FormattedMessage
+                defaultMessage="Save Now"
+                description="Title bar link for saving now"
+                id="gui.menuBar.saveNowLink"
+            />
+        </div>
+    ));
+
+SaveStatus.propTypes = {
+    alertsList: PropTypes.arrayOf(PropTypes.object),
+    onClickSave: PropTypes.func,
+    projectChanged: PropTypes.bool
+};
+
+const mapStateToProps = state => ({
+    alertsList: state.scratchGui.alerts.alertsList,
+    projectChanged: state.scratchGui.projectChanged
+});
+
+const mapDispatchToProps = dispatch => ({
+    onClickSave: () => dispatch(manualUpdateProject())
+});
+
+export default connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(SaveStatus);
diff --git a/test/unit/containers/save-status.test.jsx b/test/unit/containers/save-status.test.jsx
new file mode 100644
index 000000000..c426864d4
--- /dev/null
+++ b/test/unit/containers/save-status.test.jsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import {Provider} from 'react-redux';
+import configureStore from 'redux-mock-store';
+import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
+import SaveStatus from '../../../src/components/menu-bar/save-status.jsx';
+import InlineMessages from '../../../src/containers/inline-messages.jsx';
+import {AlertTypes} from '../../../src/lib/alerts/index.jsx';
+
+// Stub the manualUpdateProject action creator for later testing
+jest.mock('../../../src/reducers/project-state', () => ({
+    manualUpdateProject: jest.fn(() => ({type: 'stubbed'}))
+}));
+
+describe('SaveStatus container', () => {
+    const mockStore = configureStore();
+
+    test('if there are inline messages, they are shown instead of save now', () => {
+        const store = mockStore({
+            scratchGui: {
+                projectChanged: true,
+                alerts: {
+                    alertsList: [
+                        {alertId: 'saveSuccess', alertType: AlertTypes.INLINE}
+                    ]
+                }
+            }
+        });
+        const wrapper = mountWithIntl(
+            <Provider store={store}>
+                <SaveStatus />
+            </Provider>
+        );
+        expect(wrapper.find(InlineMessages).exists()).toBe(true);
+        expect(wrapper.contains('Save Now')).not.toBe(true);
+    });
+
+    test('save now is shown if there are project changes and no inline messages', () => {
+        const store = mockStore({
+            scratchGui: {
+                projectChanged: true,
+                alerts: {
+                    alertsList: []
+                }
+            }
+        });
+        const wrapper = mountWithIntl(
+            <Provider store={store}>
+                <SaveStatus />
+            </Provider>
+        );
+        expect(wrapper.find(InlineMessages).exists()).not.toBe(true);
+        expect(wrapper.contains('Save Now')).toBe(true);
+
+        // Clicking save now should dispatch the manualUpdateProject action (stubbed above)
+        wrapper.find('[children="Save Now"]').simulate('click');
+        expect(store.getActions()[0].type).toEqual('stubbed');
+    });
+
+    test('neither is shown if there are no project changes or inline messages', () => {
+        const store = mockStore({
+            scratchGui: {
+                projectChanged: false,
+                alerts: {
+                    alertsList: []
+                }
+            }
+        });
+        const wrapper = mountWithIntl(
+            <Provider store={store}>
+                <SaveStatus />
+            </Provider>
+        );
+        expect(wrapper.find(InlineMessages).exists()).not.toBe(true);
+        expect(wrapper.contains('Save Now')).not.toBe(true);
+    });
+});
-- 
GitLab