From 29c96ac14dbcd7ce960bf0591240a1919790f45b Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Mon, 31 Aug 2020 16:40:10 -0400 Subject: [PATCH] [MM-27526] Onboarding Flow for End Users (#6296) * [MM-26468][MM-26483] Progress Bar Common Component/Sidebar Next Steps Component (#5865) * [MM-26483] Progress bar common component and PoC in the sidebar * Styling for the sidebar element and some tests * Added translations * Snapshot update * Style changes * Use same border design as channels * [MM-26465] Background and general layout for cloud onboarding (#5823) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * Lint fix * Update copy * PR feedback Co-authored-by: Mattermod * [MM-26480] Card/Accordion Common Component (#5861) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * WIP * Initial card style * Collapse functionality (no animation) * WIP * Rest of accordion common component and some animation * Lint, type and test fixes * Updated snapshot * Reduce nesting * Merge'd * PR feedback * Fix box-shadow on collapsed state * [MM-26470] Base Next Step Wizard Controller and Styling (#5893) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * WIP * Initial card style * Collapse functionality (no animation) * WIP * Rest of accordion common component and some animation * Lint, type and test fixes * Updated snapshot * Reduce nesting * WIP - Wiring for step wizard * Skip getting started link, hook for final page * Moved steps into its own constants file, type and test fixes * Shifted around the screen changing and added final screen placeholder * Translations and wizard navigation button styling * Pick starting step based on which are finished, button styling fixes * Allow for getting out of next steps view by switching channels * PR feedback * PR feedback * blank * Change style of complete card header to be more like the regular one * Fixed background on complete step * Merge'd * PR feedback * PR feedback * Removed translation * Fixed box shadow transition on card * Removed duplicate logic * PR feedback * PR feedback * Fixed hover state on completed cards * re-add margin on complete state * [MM-26466] Close Next Steps Modal and functionality (#5995) * Hooked up the sidebar next steps bar and some fixes * Integration of state into app for next steps view, close next steps view modal preliminary * Styling and help arrow for modal * Missed a translation * PR feedback * Center the next steps modal * PR feedback * Translation fix * Screen transitions for loading screen and final screen * Transition screen * Fixed APNG issue * Style for desktop and mobile * Fixed styling * More fixes * Functionality and test fixes * Dev PR feedback * UX PR feedback * [MM-27164] Picture Selector Common Component (#5973) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * WIP * Initial card style * Collapse functionality (no animation) * WIP * Rest of accordion common component and some animation * Lint, type and test fixes * Updated snapshot * Reduce nesting * WIP - Wiring for step wizard * Skip getting started link, hook for final page * Moved steps into its own constants file, type and test fixes * Shifted around the screen changing and added final screen placeholder * Translations and wizard navigation button styling * Pick starting step based on which are finished, button styling fixes * Allow for getting out of next steps view by switching channels * PR feedback * PR feedback * blank * Change style of complete card header to be more like the regular one * Fixed background on complete step * Merge'd * PR feedback * PR feedback * Removed translation * Fixed box shadow transition on card * Removed duplicate logic * WIP * Functional component that works * Styling and a couple tweaks * A few tests * Snapshots * Type and i18n fixes * PR feedback and test fixes * Added button hover states * PR feedback * Blur select button on select image * Blur on click, not on select image * Update components/picture_selector.tsx Co-authored-by: Nev Angelova Co-authored-by: Nev Angelova * [MM-26482] Textbox Common Component for Cloud Onboarding (#5904) * WIP * WIP * [MM-26465] Background and general layout for cloud onboarding * Mobile view, lint and type fixes, added a test file for later use * More test fixes * UX feedback * Replaced dumb comment with useful one * Turn off graphic at 1020px * WIP * Initial card style * Collapse functionality (no animation) * WIP * Rest of accordion common component and some animation * Lint, type and test fixes * Updated snapshot * Reduce nesting * WIP - Wiring for step wizard * Skip getting started link, hook for final page * Moved steps into its own constants file, type and test fixes * Shifted around the screen changing and added final screen placeholder * Translations and wizard navigation button styling * Pick starting step based on which are finished, button styling fixes * Allow for getting out of next steps view by switching channels * PR feedback * WIP * [MM-26472] Textbox Common Component for Cloud Onboarding * Specific styling for the Cloud Onboarding components * Added info component and some other styling * Fixed the error styling * Fixed most of the shifting in the textbox * Lint fix * PR feedback * blank * PR feedback * Change style of complete card header to be more like the regular one * Fixed background on complete step * Merge'd * PR feedback * PR feedback * Removed translation * PR feedback * Use box shadow instead of border for changing text input * Improved CSS from Asaad * Removed inner border when focused/error state * Removed unnecessary commented code * Merge'd * Switch to proper BEM * Adding a card for opening and setting preferences * Adding files * [MM-26469] Complete Profile Step (#6077) * [MM-26469] Complete Profile Step * Lint fix * [MM-26473] Tips and Next Steps screen (#6020) * Screen transitions for loading screen and final screen * Transition screen * Fixed APNG issue * Style for desktop and mobile * Fixed styling * More fixes * Functionality and test fixes * Dev PR feedback * UX PR feedback * UX feedback * Merge'd Co-authored-by: Mattermod * [MM-26471] Team Profile Setup step (#6083) * [MM-26471] Team Profile Setup step * Translation fix * PR feedback * Fixed an issue with an older version of the styles * Removed commented code * [MM-26472] Invite Members step (#6143) * WIP * WIP * [MM-26742] Functionality for Invite Members step, and most of styling * Beginning of common component, currently for use only in the Invite Members step * Cleanup * Couple small style fixes * Fixed the invite link at the bottom * Lint and test fixes * PR feedback * Fixed button width for copy link and fixed test * [MM-27908] Updated Cloud Logo (#6190) * [MM-27908] Updated Cloud Logo * Remove extra prop * Type fix * [MM-27907] Fixed input focus colour and fixed placeholder focus on multi_input (#6182) * [MM-27907] Fixed input focus colour and fixed placeholder focus on multi_input * PR feedback * [MM-27159] Button States for Cloud Onboarding (#6189) * [MM-27159] Button States * PR feedback * [MM-27305] Card Component Refactor for smoothness (#6172) * Refactor Card component to be more responsive and robust * Cleanup and a couple other bug fixes * [MM-27974] Unit Tests for Cloud Onboarding (#6208) * [MM-27974] Unit Tests for Cloud Onboarding * Translation fix * [MM-27923] Turn on Cloud Onboarding for Cloud-only instances (#6225) * Flow working, pre cleanup, pre merge of Devin's work * Code clean up * Adding tests for my added components * Put package-lock back to master * Fix bug that always redirects you to the next steps page * Change function name to avoid confusion * Code clean up, and this dang package-lock * Fix one of the test issues * Fix other tests * fix alignment of body text in text_card_with_action component * Fix a few issues in PR review * pushing to get help * Fixes and updating tests * Fix package-lock * Fix package-lock * Remove package-lock changes * Increase transition time from 1 second to 3 seconds * Some fixes for UX * add 2 tests to be sure that step filter logic is working based on user roles * Fix tests * Fix test Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Co-authored-by: Mattermod Co-authored-by: Devin Binnie Co-authored-by: Nev Angelova --- .../next_steps_view.test.tsx.snap | 1 + components/next_steps_view/index.ts | 6 +- .../next_steps_view/next_steps_tips.tsx | 62 ++++++++++++- .../next_steps_view/next_steps_view.scss | 2 +- .../next_steps_view/next_steps_view.test.tsx | 51 +++++------ .../next_steps_view/next_steps_view.tsx | 53 ++++++++---- components/next_steps_view/steps.test.tsx | 73 +++++++++++++++- components/next_steps_view/steps.ts | 70 +++++++++++++-- .../enable_notifications_step.test.tsx.snap | 11 +++ .../enable_notifications_step.test.tsx | 23 +++++ .../enable_notifications_step.tsx | 43 ++++++++++ .../setup_preferences_step.test.tsx.snap | 86 +++++++++++++++++++ .../setup_preferences_step.test.tsx | 34 ++++++++ .../setup_preferences_step.tsx | 43 ++++++++++ .../text_card_with_action.test.tsx.snap | 31 +++++++ .../text_card_with_action.scss | 12 +++ .../text_card_with_action.test.tsx | 22 +++++ .../text_card_with_action.tsx | 36 ++++++++ .../sidebar/sidebar_next_steps/index.ts | 6 +- .../sidebar_next_steps/sidebar_next_steps.tsx | 8 +- .../modal/user_settings_modal.tsx | 2 + i18n/en.json | 10 +++ package-lock.json | 11 ++- utils/constants.jsx | 2 + 24 files changed, 639 insertions(+), 59 deletions(-) create mode 100644 components/next_steps_view/steps/enable_notifications_step/__snapshots__/enable_notifications_step.test.tsx.snap create mode 100644 components/next_steps_view/steps/enable_notifications_step/enable_notifications_step.test.tsx create mode 100644 components/next_steps_view/steps/enable_notifications_step/enable_notifications_step.tsx create mode 100644 components/next_steps_view/steps/setup_preferences_step/__snapshots__/setup_preferences_step.test.tsx.snap create mode 100644 components/next_steps_view/steps/setup_preferences_step/setup_preferences_step.test.tsx create mode 100644 components/next_steps_view/steps/setup_preferences_step/setup_preferences_step.tsx create mode 100644 components/next_steps_view/steps/text_card_with_action/__snapshots__/text_card_with_action.test.tsx.snap create mode 100644 components/next_steps_view/steps/text_card_with_action/text_card_with_action.scss create mode 100644 components/next_steps_view/steps/text_card_with_action/text_card_with_action.test.tsx create mode 100644 components/next_steps_view/steps/text_card_with_action/text_card_with_action.tsx diff --git a/components/next_steps_view/__snapshots__/next_steps_view.test.tsx.snap b/components/next_steps_view/__snapshots__/next_steps_view.test.tsx.snap index bcb0b8d2e82c..cdcbcb11dd92 100644 --- a/components/next_steps_view/__snapshots__/next_steps_view.test.tsx.snap +++ b/components/next_steps_view/__snapshots__/next_steps_view.test.tsx.snap @@ -106,6 +106,7 @@ exports[`components/next_steps_view should match snapshot 1`] = ` diff --git a/components/next_steps_view/index.ts b/components/next_steps_view/index.ts index a8ff2e93a1f3..e31983418cb2 100644 --- a/components/next_steps_view/index.ts +++ b/components/next_steps_view/index.ts @@ -6,12 +6,14 @@ import {bindActionCreators, Dispatch} from 'redux'; import {savePreferences} from 'mattermost-redux/actions/preferences'; import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; import {setShowNextStepsView} from 'actions/views/next_steps'; import {GlobalState} from 'types/store'; import {Preferences} from 'utils/constants'; +import {getSteps} from './steps'; + import NextStepsView from './next_steps_view'; function makeMapStateToProps() { @@ -20,7 +22,9 @@ function makeMapStateToProps() { return (state: GlobalState) => { return { currentUser: getCurrentUser(state), + isAdmin: isCurrentUserSystemAdmin(state), preferences: getCategory(state, Preferences.RECOMMENDED_NEXT_STEPS), + steps: getSteps(state), }; }; } diff --git a/components/next_steps_view/next_steps_tips.tsx b/components/next_steps_view/next_steps_tips.tsx index 8b2463e1bdbc..2b8880e1b5a3 100644 --- a/components/next_steps_view/next_steps_tips.tsx +++ b/components/next_steps_view/next_steps_tips.tsx @@ -18,6 +18,9 @@ import * as UserAgent from 'utils/user_agent'; import {ModalIdentifiers} from 'utils/constants'; import * as Utils from 'utils/utils'; +import TeamMembersModal from 'components/team_members_modal'; +import {toggleShortcutsModal} from 'actions/global_actions.jsx'; + const seeAllApps = () => { window.open('https://mattermost.com/download/#mattermostApps', '_blank'); }; @@ -73,13 +76,17 @@ const openAuthPage = (page: string) => { browserHistory.push(`/admin_console/authentication/${page}`); }; -export default function NextStepsTips(props: {showFinalScreen: boolean; animating: boolean; stopAnimating: () => void}) { +export default function NextStepsTips(props: { showFinalScreen: boolean; animating: boolean; stopAnimating: () => void; isAdmin: boolean}) { const dispatch = useDispatch(); const openPluginMarketplace = openModal({modalId: ModalIdentifiers.PLUGIN_MARKETPLACE, dialogType: MarketplaceModal}); const openMoreChannels = openModal({modalId: ModalIdentifiers.MORE_CHANNELS, dialogType: MoreChannels}); + const openViewMembersModal = openModal({ + modalId: ModalIdentifiers.TEAM_MEMBERS, + dialogType: TeamMembersModal, + }); let nonMobileTips; - if (!Utils.isMobile()) { + if (!Utils.isMobile() && props.isAdmin) { nonMobileTips = ( <> @@ -146,6 +153,57 @@ export default function NextStepsTips(props: {showFinalScreen: boolean; animatin ); + } else if (!Utils.isMobile() && !props.isAdmin) { + nonMobileTips = ( + <> + +
+

+ +

+ + +
+
+ +
+

+ +

+ + +
+
+ + ); } let downloadSection; diff --git a/components/next_steps_view/next_steps_view.scss b/components/next_steps_view/next_steps_view.scss index 5f3fbd086bc4..a7956edc4cab 100644 --- a/components/next_steps_view/next_steps_view.scss +++ b/components/next_steps_view/next_steps_view.scss @@ -183,7 +183,7 @@ display: flex; border: none; background: none; - padding: 12px; + padding: 13px; width: 100%; > span { diff --git a/components/next_steps_view/next_steps_view.test.tsx b/components/next_steps_view/next_steps_view.test.tsx index 5a1bedff9804..1b6557d99ab7 100644 --- a/components/next_steps_view/next_steps_view.test.tsx +++ b/components/next_steps_view/next_steps_view.test.tsx @@ -7,30 +7,31 @@ import {shallow, ShallowWrapper} from 'enzyme'; import NextStepsView from 'components/next_steps_view/next_steps_view'; import {TestHelper} from 'utils/test_helper'; -jest.mock('components/next_steps_view/steps', () => ({ - Steps: [ - { - id: 'step_1', - title: 'Step_1', - component: jest.fn(), - }, - { - id: 'step_2', - title: 'Step_2', - component: jest.fn(), - }, - { - id: 'step_3', - title: 'Step_3', - component: jest.fn(), - }, - ], -})); - describe('components/next_steps_view', () => { const baseProps = { + steps: [ + { + id: 'step_1', + roles: [], + title: 'Step_1', + component: jest.fn(), + }, + { + id: 'step_2', + title: 'Step_2', + roles: [], + component: jest.fn(), + }, + { + id: 'step_3', + title: 'Step_3', + roles: [], + component: jest.fn(), + }, + ], currentUser: TestHelper.getUserMock(), preferences: [], + isAdmin: true, actions: { setShowNextStepsView: jest.fn(), savePreferences: jest.fn(), @@ -74,14 +75,14 @@ describe('components/next_steps_view', () => { expect(setExpanded).toBeCalledWith('step_2'); }); - test('should go to final screen when last step is marked complete', () => { + test('should go to first incomplete step when last step is marked complete', () => { const wrapper: ShallowWrapper = shallow( , ); - wrapper.instance().transitionToFinalScreen = jest.fn(); - wrapper.instance().nextStep(jest.fn(), 'step_3'); - expect(wrapper.instance().transitionToFinalScreen).toBeCalled(); + const setExpanded = jest.fn(); + wrapper.instance().nextStep(setExpanded, 'step_3'); + expect(setExpanded).toBeCalledWith('step_1'); }); test('should cascade through all steps when all marked complete', () => { @@ -108,6 +109,7 @@ describe('components/next_steps_view', () => { }, ], }; + jest.useFakeTimers(); const wrapper: ShallowWrapper = shallow( , @@ -115,6 +117,7 @@ describe('components/next_steps_view', () => { wrapper.instance().transitionToFinalScreen = jest.fn(); wrapper.instance().nextStep(jest.fn(), 'step_1'); + jest.runOnlyPendingTimers(); expect(wrapper.instance().transitionToFinalScreen).toBeCalled(); }); }); diff --git a/components/next_steps_view/next_steps_view.tsx b/components/next_steps_view/next_steps_view.tsx index 91098fa9677e..a10554a28a1d 100644 --- a/components/next_steps_view/next_steps_view.tsx +++ b/components/next_steps_view/next_steps_view.tsx @@ -14,7 +14,7 @@ import Card from 'components/card/card'; import loadingIcon from 'images/spinner-48x48-blue.apng'; import {Preferences} from 'utils/constants'; -import {Steps, StepType} from './steps'; +import {StepType} from './steps'; import './next_steps_view.scss'; import NextStepsTips from './next_steps_tips'; import OnboardingBgSvg from './images/onboarding-bg-svg'; @@ -22,11 +22,13 @@ import GettingStartedSvg from './images/getting-started-svg'; import CloudLogoSvg from './images/cloud-logo-svg'; import OnboardingSuccessSvg from './images/onboarding-success-svg'; -const TRANSITION_SCREEN_TIMEOUT = 1000; +const TRANSITION_SCREEN_TIMEOUT = 3000; type Props = { currentUser: UserProfile; preferences: PreferenceType[]; + isAdmin: boolean; + steps: StepType[]; actions: { savePreferences: (userId: string, preferences: PreferenceType[]) => void; setShowNextStepsView: (show: boolean) => void; @@ -51,13 +53,21 @@ export default class NextStepsView extends React.PureComponent { } getStartingStep = () => { - for (let i = 0; i < Steps.length; i++) { - if (!this.isStepComplete(Steps[i].id)) { - return Steps[i].id; + for (let i = 0; i < this.props.steps.length; i++) { + if (!this.isStepComplete(this.props.steps[i].id)) { + return this.props.steps[i].id; } } + return this.props.steps[0].id; + } - return Steps[0].id; + getIncompleteStep = () => { + for (let i = 0; i < this.props.steps.length; i++) { + if (!this.isStepComplete(this.props.steps[i].id)) { + return this.props.steps[i].id; + } + } + return null; } onSkip = (setExpanded: (expandedKey: string) => void) => { @@ -67,8 +77,8 @@ export default class NextStepsView extends React.PureComponent { } onFinish = (setExpanded: (expandedKey: string) => void) => { - return (id: string) => { - this.props.actions.savePreferences(this.props.currentUser.id, [{ + return async (id: string) => { + await this.props.actions.savePreferences(this.props.currentUser.id, [{ category: Preferences.RECOMMENDED_NEXT_STEPS, user_id: this.props.currentUser.id, name: id, @@ -100,13 +110,23 @@ export default class NextStepsView extends React.PureComponent { } nextStep = (setExpanded: (expandedKey: string) => void, id: string) => { - const currentIndex = Steps.findIndex((step) => step.id === id); - if ((currentIndex + 1) > (Steps.length - 1)) { - this.transitionToFinalScreen(); - } else if (this.isStepComplete(Steps[currentIndex + 1].id)) { - this.nextStep(setExpanded, Steps[currentIndex + 1].id); + const currentIndex = this.props.steps.findIndex((step) => step.id === id); + if (currentIndex + 1 > this.props.steps.length - 1) { + // Check if previous steps were skipped before moving on + const incompleteStep = this.getIncompleteStep(); + if (incompleteStep === null) { + // Collapse all accordion tiles + setExpanded(''); + setTimeout(() => { + this.transitionToFinalScreen(); + }, 300); + } else { + setExpanded(incompleteStep); + } + } else if (this.isStepComplete(this.props.steps[currentIndex + 1].id)) { + this.nextStep(setExpanded, this.props.steps[currentIndex + 1].id); } else { - setExpanded(Steps[currentIndex + 1].id); + setExpanded(this.props.steps[currentIndex + 1].id); } } @@ -186,7 +206,7 @@ export default class NextStepsView extends React.PureComponent { } renderMainBody = () => { - const renderedSteps = Steps.map(this.renderStep); + const renderedSteps = this.props.steps.map(this.renderStep); return (
{
- + {(setExpanded, expandedKey) => { return ( <> @@ -258,6 +278,7 @@ export default class NextStepsView extends React.PureComponent { showFinalScreen={this.state.showFinalScreen} animating={this.state.animating} stopAnimating={this.stopAnimating} + isAdmin={this.props.isAdmin} /> ); diff --git a/components/next_steps_view/steps.test.tsx b/components/next_steps_view/steps.test.tsx index 6fa6411e984b..5d8dc8356778 100644 --- a/components/next_steps_view/steps.test.tsx +++ b/components/next_steps_view/steps.test.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {showNextSteps} from './steps'; +import {showNextSteps, getSteps} from './steps'; describe('components/next_steps_view/steps', () => { test('should not show next steps if not cloud', () => { @@ -15,6 +15,12 @@ describe('components/next_steps_view/steps', () => { preferences: { myPreferences: {}, }, + users: { + currentUserId: 'current_user_id', + profiles: { + current_user_id: {roles: 'system_role'}, + }, + }, }, }; @@ -49,6 +55,12 @@ describe('components/next_steps_view/steps', () => { 'recommended_next_steps--hide': {name: 'hide', value: 'true'}, }, }, + users: { + currentUserId: 'current_user_id', + profiles: { + current_user_id: {roles: 'system_role'}, + }, + }, }, }; @@ -65,9 +77,24 @@ describe('components/next_steps_view/steps', () => { }, preferences: { myPreferences: { - 'recommended_next_steps--complete_profile': {name: 'complete_profile', value: 'true'}, - 'recommended_next_steps--team_setup': {name: 'team_setup', value: 'true'}, - 'recommended_next_steps--invite_members': {name: 'invite_members', value: 'true'}, + 'recommended_next_steps--complete_profile': { + name: 'complete_profile', + value: 'true', + }, + 'recommended_next_steps--team_setup': { + name: 'team_setup', + value: 'true', + }, + 'recommended_next_steps--invite_members': { + name: 'invite_members', + value: 'true', + }, + }, + }, + users: { + currentUserId: 'current_user_id', + profiles: { + current_user_id: {roles: 'system_role'}, }, }, }, @@ -75,4 +102,42 @@ describe('components/next_steps_view/steps', () => { expect(showNextSteps(state as any)).toBe(false); }); + + test('should only show admin steps for admin users', () => { + const state = { + entities: { + general: { + license: { + Cloud: 'true', + }, + }, + users: { + currentUserId: 'current_user_id', + profiles: { + current_user_id: {roles: 'system_admin system_user'}, + }, + }, + }, + }; + expect(getSteps(state as any)).toHaveLength(3); + }); + + test('should only show non-admin steps for non-admin users', () => { + const state = { + entities: { + general: { + license: { + Cloud: 'true', + }, + }, + users: { + currentUserId: 'current_user_id', + profiles: { + current_user_id: {roles: 'system_user'}, + }, + }, + }, + }; + expect(getSteps(state as any)).toHaveLength(3); + }); }); diff --git a/components/next_steps_view/steps.ts b/components/next_steps_view/steps.ts index 4da9084dfa49..5edc38ec8984 100644 --- a/components/next_steps_view/steps.ts +++ b/components/next_steps_view/steps.ts @@ -6,13 +6,18 @@ import {getLicense} from 'mattermost-redux/selectors/entities/general'; import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; import {UserProfile} from 'mattermost-redux/types/users'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {isEqual} from 'lodash'; + import {GlobalState} from 'types/store'; import {RecommendedNextSteps, Preferences} from 'utils/constants'; import {localizeMessage} from 'utils/utils'; import CompleteProfileStep from './steps/complete_profile_step'; +import SetupPreferencesStep from './steps/setup_preferences_step/setup_preferences_step'; import InviteMembersStep from './steps/invite_members_step'; import TeamProfileStep from './steps/team_profile_step'; +import EnableNotificationsStep from './steps/enable_notifications_step/enable_notifications_step'; export type StepComponentProps = { id: string; @@ -20,31 +25,81 @@ export type StepComponentProps = { onSkip: (id: string) => void; onFinish: (id: string) => void; } - export type StepType = { id: string; title: string; component: React.ComponentType; -} + + // An array of all roles a user must have in order to see the step e.g. admins are both system_admin and system_user + // so you would require ['system_admin','system_user'] to match. + // to show step for all roles, leave the roles array blank. + roles: Array; +}; export const Steps: StepType[] = [ { id: RecommendedNextSteps.COMPLETE_PROFILE, - title: localizeMessage('next_steps_view.titles.completeProfile', 'Complete your profile'), + title: localizeMessage( + 'next_steps_view.titles.completeProfile', + 'Complete your profile' + ), component: CompleteProfileStep, + roles: [], }, { id: RecommendedNextSteps.TEAM_SETUP, - title: localizeMessage('next_steps_view.titles.teamSetup', 'Name your team'), + title: localizeMessage( + 'next_steps_view.titles.teamSetup', + 'Name your team' + ), + roles: ['system_admin', 'system_user'], component: TeamProfileStep, }, { id: RecommendedNextSteps.INVITE_MEMBERS, - title: localizeMessage('next_steps_view.titles.inviteMembers', 'Invite members to the team'), + title: localizeMessage( + 'next_steps_view.titles.inviteMembers', + 'Invite members to the team' + ), + roles: ['system_admin', 'system_user'], component: InviteMembersStep, }, + { + id: RecommendedNextSteps.NOTIFICATION_SETUP, + title: localizeMessage( + 'next_steps_view.notificationSetup.setNotifications', + 'Turn on notifications' + ), + roles: ['system_user'], + component: EnableNotificationsStep, + }, + { + id: RecommendedNextSteps.PREFERENCES_SETUP, + title: localizeMessage( + 'next_steps_view.titles.preferenceSetup', + 'Set your preferences' + ), + roles: ['system_user'], + component: SetupPreferencesStep, + }, ]; +// Filter the steps shown by checking if our user has any of the required roles for that step +export function isStepForUser(step: StepType, roles: string): boolean { + const userRoles = roles.split(' '); + return ( + isEqual(userRoles.sort(), step.roles.sort()) || + step.roles.length === 0 + ); +} + +export const getSteps = createSelector( + (state: GlobalState) => getCurrentUser(state), + (currentUser) => { + return Steps.filter((step) => isStepForUser(step, currentUser.roles)); + } +); + const getCategory = makeGetCategory(); export const showNextSteps = createSelector( (state: GlobalState) => getCategory(state, Preferences.RECOMMENDED_NEXT_STEPS), @@ -65,8 +120,9 @@ export const showNextSteps = createSelector( export const nextStepsNotFinished = createSelector( (state: GlobalState) => getCategory(state, Preferences.RECOMMENDED_NEXT_STEPS), - (stepPreferences) => { - const checkPref = (step: StepType) => stepPreferences.some((pref) => pref.name === step.id && pref.value === 'true'); + (state: GlobalState) => getCurrentUser(state), + (stepPreferences, currentUser) => { + const checkPref = (step: StepType) => stepPreferences.some((pref) => (pref.name === step.id && pref.value === 'true') || !isStepForUser(step, currentUser.roles)); return !Steps.every(checkPref); } ); diff --git a/components/next_steps_view/steps/enable_notifications_step/__snapshots__/enable_notifications_step.test.tsx.snap b/components/next_steps_view/steps/enable_notifications_step/__snapshots__/enable_notifications_step.test.tsx.snap new file mode 100644 index 000000000000..84269a6d15b5 --- /dev/null +++ b/components/next_steps_view/steps/enable_notifications_step/__snapshots__/enable_notifications_step.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/next_steps_view/steps/setup_preferences_step should match snapshot 1`] = ` + +`; diff --git a/components/next_steps_view/steps/enable_notifications_step/enable_notifications_step.test.tsx b/components/next_steps_view/steps/enable_notifications_step/enable_notifications_step.test.tsx new file mode 100644 index 000000000000..56d5ba1bd34b --- /dev/null +++ b/components/next_steps_view/steps/enable_notifications_step/enable_notifications_step.test.tsx @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; + +import {shallow} from 'enzyme'; + +import {TestHelper} from 'utils/test_helper'; + +import EnableNotificationsStep from './enable_notifications_step'; + +describe('components/next_steps_view/steps/setup_preferences_step', () => { + const props = { + id: 'test', + currentUser: TestHelper.getUserMock(), + onSkip: () => {}, + onFinish: () => {}, + }; + + test('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/components/next_steps_view/steps/enable_notifications_step/enable_notifications_step.tsx b/components/next_steps_view/steps/enable_notifications_step/enable_notifications_step.tsx new file mode 100644 index 000000000000..a976c7fbc3ef --- /dev/null +++ b/components/next_steps_view/steps/enable_notifications_step/enable_notifications_step.tsx @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import * as Utils from 'utils/utils.jsx'; +import {showNotification} from 'utils/notifications'; +import {StepComponentProps} from '../../steps'; + +import TextCardWithAction from '../text_card_with_action/text_card_with_action'; + +export default function EnableNotificationsStep(props: StepComponentProps) { + const onFinish = async () => { + await showNotification({ + title: Utils.localizeMessage( + 'next_steps_view.notificationSetup.notficationsEnabledTitle', + 'Notifications Enabled!' + ), + body: Utils.localizeMessage( + 'next_steps_view.notificationSetup.notficationsEnabledBody', + 'This is how notifications from Mattermost will appear' + ), + requireInteraction: false, + silent: false, + onClick: () => {}, + }); + props.onFinish(props.id); + }; + + return ( + + ); +} diff --git a/components/next_steps_view/steps/setup_preferences_step/__snapshots__/setup_preferences_step.test.tsx.snap b/components/next_steps_view/steps/setup_preferences_step/__snapshots__/setup_preferences_step.test.tsx.snap new file mode 100644 index 000000000000..ec7e871e11fc --- /dev/null +++ b/components/next_steps_view/steps/setup_preferences_step/__snapshots__/setup_preferences_step.test.tsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/next_steps_view/steps/setup_preferences_step should match snapshot 1`] = ` + + + +`; diff --git a/components/next_steps_view/steps/setup_preferences_step/setup_preferences_step.test.tsx b/components/next_steps_view/steps/setup_preferences_step/setup_preferences_step.test.tsx new file mode 100644 index 000000000000..b56d508e0122 --- /dev/null +++ b/components/next_steps_view/steps/setup_preferences_step/setup_preferences_step.test.tsx @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; + +import {shallow} from 'enzyme'; + +import configureStore from 'redux-mock-store'; + +import {Provider} from 'react-redux'; + +import {TestHelper} from 'utils/test_helper'; + +import SetupPreferencesStep from './setup_preferences_step'; + +describe('components/next_steps_view/steps/setup_preferences_step', () => { + const initialState = {}; + const mockStore = configureStore(); + const props = { + id: 'test', + currentUser: TestHelper.getUserMock(), + onSkip: () => { }, + onFinish: () => {} + }; + + test('should match snapshot', () => { + const store = mockStore(initialState); + const wrapper = shallow( + + + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/components/next_steps_view/steps/setup_preferences_step/setup_preferences_step.tsx b/components/next_steps_view/steps/setup_preferences_step/setup_preferences_step.tsx new file mode 100644 index 000000000000..6aa0b1434a6c --- /dev/null +++ b/components/next_steps_view/steps/setup_preferences_step/setup_preferences_step.tsx @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {useDispatch} from 'react-redux'; + +import {ModalIdentifiers} from 'utils/constants'; +import UserSettingsModal from '../../../user_settings/modal'; +import {StepComponentProps} from '../../steps'; +import TextCardWithAction from '../text_card_with_action/text_card_with_action'; +import {openModal} from 'actions/views/modals'; + +export default function SetupPreferencesStep(props: StepComponentProps) { + const dispatch = useDispatch(); + + const onFinish = () => { + props.onFinish(props.id); + }; + + const onClick = () => { + dispatch(openModal({ + modalId: ModalIdentifiers.USER_SETTINGS, + dialogType: UserSettingsModal, + dialogProps: { + onExit: onFinish, + } + })); + }; + + return ( + <> + + + ); +} + diff --git a/components/next_steps_view/steps/text_card_with_action/__snapshots__/text_card_with_action.test.tsx.snap b/components/next_steps_view/steps/text_card_with_action/__snapshots__/text_card_with_action.test.tsx.snap new file mode 100644 index 000000000000..108a6f1e2ca1 --- /dev/null +++ b/components/next_steps_view/steps/text_card_with_action/__snapshots__/text_card_with_action.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/next_steps_view/steps/setup_preferences_step should match snapshot 1`] = ` +
+
+ +
+
+ +
+
+`; diff --git a/components/next_steps_view/steps/text_card_with_action/text_card_with_action.scss b/components/next_steps_view/steps/text_card_with_action/text_card_with_action.scss new file mode 100644 index 000000000000..a61eb35dd0fc --- /dev/null +++ b/components/next_steps_view/steps/text_card_with_action/text_card_with_action.scss @@ -0,0 +1,12 @@ +.TextCardWithAction { + width: 100%; + .card-body { + max-height: 40px; + max-width: 1440px; + margin: 12px 46px 16px 40px; + } + + .card-action-button { + margin-bottom: 16px; + } +} diff --git a/components/next_steps_view/steps/text_card_with_action/text_card_with_action.test.tsx b/components/next_steps_view/steps/text_card_with_action/text_card_with_action.test.tsx new file mode 100644 index 000000000000..57a266ea6782 --- /dev/null +++ b/components/next_steps_view/steps/text_card_with_action/text_card_with_action.test.tsx @@ -0,0 +1,22 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; + +import {shallow} from 'enzyme'; + +import TextCardWithAction from './text_card_with_action'; + +describe('components/next_steps_view/steps/setup_preferences_step', () => { + const props = { + cardBodyMessageId: 'test-id', + cardBodyDefaultMessage: 'This is a test card body message', + buttonMessageId: 'test-button-id', + buttonDefaultMessage: 'this is test button text', + onClick: () => {}, + }; + + test('should match snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/components/next_steps_view/steps/text_card_with_action/text_card_with_action.tsx b/components/next_steps_view/steps/text_card_with_action/text_card_with_action.tsx new file mode 100644 index 000000000000..aaadabfea93b --- /dev/null +++ b/components/next_steps_view/steps/text_card_with_action/text_card_with_action.tsx @@ -0,0 +1,36 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import './text_card_with_action.scss'; + +export default function TextCardWithAction(props: { + cardBodyMessageId: string; + cardBodyDefaultMessage: string; + buttonMessageId: string; + buttonDefaultMessage: string; + onClick: () => void; +}) { + return ( +
+
+ +
+
+ +
+
+ ); +} diff --git a/components/sidebar/sidebar_next_steps/index.ts b/components/sidebar/sidebar_next_steps/index.ts index 097bf7df232f..93853abab8f0 100644 --- a/components/sidebar/sidebar_next_steps/index.ts +++ b/components/sidebar/sidebar_next_steps/index.ts @@ -6,7 +6,9 @@ import {Dispatch, bindActionCreators} from 'redux'; import {savePreferences} from 'mattermost-redux/actions/preferences'; import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUserId, getCurrentUser} from 'mattermost-redux/selectors/entities/users'; + +import {getSteps} from '../../next_steps_view/steps'; import {openModal, closeModal} from 'actions/views/modals'; import {setShowNextStepsView} from 'actions/views/next_steps'; @@ -21,7 +23,9 @@ function makeMapStateToProps() { return (state: GlobalState) => ({ active: state.views.nextSteps.show, + steps: getSteps(state), showNextSteps: showNextSteps(state), + currentUser: getCurrentUser(state), currentUserId: getCurrentUserId(state), preferences: getCategory(state, Preferences.RECOMMENDED_NEXT_STEPS), }); diff --git a/components/sidebar/sidebar_next_steps/sidebar_next_steps.tsx b/components/sidebar/sidebar_next_steps/sidebar_next_steps.tsx index 5b302bd5fb17..1e46a3efc31b 100644 --- a/components/sidebar/sidebar_next_steps/sidebar_next_steps.tsx +++ b/components/sidebar/sidebar_next_steps/sidebar_next_steps.tsx @@ -8,7 +8,10 @@ import classNames from 'classnames'; import {PreferenceType} from 'mattermost-redux/types/preferences'; import FormattedMarkdownMessage from 'components/formatted_markdown_message.jsx'; -import {Steps} from 'components/next_steps_view/steps'; +import { + StepType, +} from 'components/next_steps_view/steps'; + import ProgressBar from 'components/progress_bar'; import {ModalIdentifiers, RecommendedNextSteps, Preferences} from 'utils/constants'; import {localizeMessage} from 'utils/utils'; @@ -22,6 +25,7 @@ type Props = { showNextSteps: boolean; currentUserId: string; preferences: PreferenceType[]; + steps: StepType[]; actions: { savePreferences: (userId: string, preferences: PreferenceType[]) => void; openModal: (modalData: {modalId: string; dialogType: any; dialogProps?: any}) => void; @@ -85,7 +89,7 @@ export default class SidebarNextSteps extends React.PureComponent return null; } - const total = Steps.length; + const total = this.props.steps.length; const complete = this.props.preferences.filter((pref) => pref.name !== RecommendedNextSteps.HIDE && pref.value === 'true').length; let header = ( diff --git a/components/user_settings/modal/user_settings_modal.tsx b/components/user_settings/modal/user_settings_modal.tsx index 3bdbd7140dcb..9d0b0ee0a5cd 100644 --- a/components/user_settings/modal/user_settings_modal.tsx +++ b/components/user_settings/modal/user_settings_modal.tsx @@ -71,6 +71,7 @@ const holders = defineMessages({ type Props = { currentUser: UserProfile; onHide: () => void; + onExit: () => void; intl: IntlShape; actions: { sendVerificationEmail: (email: string) => Promise<{ @@ -172,6 +173,7 @@ class UserSettingsModal extends React.PureComponent { active_section: '', }); this.props.onHide(); + this.props.onExit(); } // Called to hide the settings pane when on mobile diff --git a/i18n/en.json b/i18n/en.json index 02911a4b88d5..e3ea4097c3f5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3130,6 +3130,9 @@ "next_steps_view.invite_members_step.youCanInviteUpTo": "You can invite up to 10 team members using a space or comma between addresses", "next_steps_view.mobile_tips_message": "To configure your workspace, continue on a desktop computer.", "next_steps_view.nicelyDone": "Nicely done! You’re all set.", + "next_steps_view.notificationSetup.notficationsEnabledBody": "This is how notifications from Mattermost will appear", + "next_steps_view.notificationSetup.notficationsEnabledTitle": "Notifications Enabled!", + "next_steps_view.notificationSetup.setNotifications": "Turn on notifications", "next_steps_view.oneMoment": "One moment", "next_steps_view.otherAreasToExplore": "A few other areas to explore", "next_steps_view.seeAllTheApps": "See all the apps", @@ -3143,7 +3146,10 @@ "next_steps_view.team_profile_step.teamName": "Team name", "next_steps_view.tips.addPlugins": "Add plugins to Mattermost", "next_steps_view.tips.addPlugins.button": "Add plugins", + "next_steps_view.tips.addPlugins.buttons": "See shortcuts", "next_steps_view.tips.addPlugins.text": "Visit the Plugins Marketplace to install and configure plugins.", + "next_steps_view.tips.addPlugins.texts": "Work more efficiently with Keyboard Shortcuts in Mattermost.", + "next_steps_view.tips.addPluginss": "Learn Keyboard Shortcuts", "next_steps_view.tips.auth.ldap": "AD/LDAP", "next_steps_view.tips.auth.menuAriaLabel": "Configure Authentication Menu", "next_steps_view.tips.auth.oauth": "OAuth", @@ -3151,15 +3157,19 @@ "next_steps_view.tips.configureLogin": "Configure your login method", "next_steps_view.tips.configureLogin.button": "Configure", "next_steps_view.tips.configureLogin.text": "Set up OAuth, SAML or AD/LDAP authentication.", + "next_steps_view.tips.configureLogin.texts": "Browse or search through the team members directory", + "next_steps_view.tips.configureLogins": "See who else is here", "next_steps_view.tips.downloadForDefault": "Download Mattermost", "next_steps_view.tips.downloadForMac": "Download Mattermost for Mac", "next_steps_view.tips.downloadForWindows": "Download Mattermost for Windows", "next_steps_view.tips.exploreChannels": "Explore channels", "next_steps_view.tips.exploreChannels.button": "Browse channels", "next_steps_view.tips.exploreChannels.text": "See the channels in your workspace or create a new channel.", + "next_steps_view.tips.viewMembers": "View team members", "next_steps_view.tipsAndNextSteps": "Tips & Next Steps", "next_steps_view.titles.completeProfile": "Complete your profile", "next_steps_view.titles.inviteMembers": "Invite members to the team", + "next_steps_view.titles.preferenceSetup": "Set up your preferences", "next_steps_view.titles.teamSetup": "Name your team", "next_steps_view.welcomeToMattermost": "Welcome to Mattermost", "no_results.channel_search.subtitle": "Check the spelling or try another search.", diff --git a/package-lock.json b/package-lock.json index db978d01e3e1..e7608fd01c8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13314,7 +13314,10 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/full-icu/-/full-icu-1.3.1.tgz", "integrity": "sha512-VMtK//85QJomhk3cXOCksNwOYaw1KWnYTS37GYGgyf7A3ajdBoPGhaJuJWAH2S2kq8GZeXkdKn+3Mfmgy11cVw==", - "dev": true + "dev": true, + "requires": { + "icu4c-data": "0.64.2" + } }, "function-bind": { "version": "1.1.1", @@ -14326,6 +14329,12 @@ "postcss": "^7.0.14" } }, + "icu4c-data": { + "version": "0.64.2", + "resolved": "https://registry.npmjs.org/icu4c-data/-/icu4c-data-0.64.2.tgz", + "integrity": "sha512-BPuTfkRTkplmK1pNrqgyOLJ0qB2UcQ12EotVLwiWh4ErtZR1tEYoRZk/LBLmlDfK5v574/lQYLB4jT9vApBiBQ==", + "dev": true + }, "identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", diff --git a/utils/constants.jsx b/utils/constants.jsx index bae5e287e0a6..339686960f40 100644 --- a/utils/constants.jsx +++ b/utils/constants.jsx @@ -386,6 +386,8 @@ export const RecommendedNextSteps = { COMPLETE_PROFILE: 'complete_profile', TEAM_SETUP: 'team_setup', INVITE_MEMBERS: 'invite_members', + PREFERENCES_SETUP: 'preferences_setup', + NOTIFICATION_SETUP: 'notification_setup', HIDE: 'hide', };