diff --git a/actions/views/next_steps.ts b/actions/views/next_steps.ts new file mode 100644 index 000000000000..1115df66e16a --- /dev/null +++ b/actions/views/next_steps.ts @@ -0,0 +1,11 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ActionTypes} from 'utils/constants'; + +export function setShowNextStepsView(show: boolean) { + return { + type: ActionTypes.SET_SHOW_NEXT_STEPS_VIEW, + show, + }; +} diff --git a/components/channel_view/channel_view.jsx b/components/channel_view/channel_view.jsx index ddac5f4f4f3e..90ca7d6e3f2a 100644 --- a/components/channel_view/channel_view.jsx +++ b/components/channel_view/channel_view.jsx @@ -29,10 +29,12 @@ export default class ChannelView extends React.PureComponent { }).isRequired, showTutorial: PropTypes.bool.isRequired, showNextSteps: PropTypes.bool.isRequired, + showNextStepsEphemeral: PropTypes.bool.isRequired, channelIsArchived: PropTypes.bool.isRequired, viewArchivedChannels: PropTypes.bool.isRequired, actions: PropTypes.shape({ goToLastViewedChannel: PropTypes.func.isRequired, + setShowNextStepsView: PropTypes.func.isRequired, }), }; @@ -54,7 +56,7 @@ export default class ChannelView extends React.PureComponent { const focusedPostId = props.match.params.postid; if (props.match.url !== state.url && props.channelId !== state.channelId) { - updatedState = {deferredPostView: ChannelView.createDeferredPostView(), url: props.match.url, focusedPostId, showNextSteps: false}; + updatedState = {deferredPostView: ChannelView.createDeferredPostView(), url: props.match.url, focusedPostId}; } if (props.channelId !== state.channelId) { @@ -65,6 +67,10 @@ export default class ChannelView extends React.PureComponent { updatedState = {...updatedState, focusedPostId}; } + if (props.showNextSteps !== state.showNextSteps) { + updatedState = {...updatedState, showNextSteps: props.showNextSteps}; + } + if (Object.keys(updatedState).length) { return updatedState; } @@ -79,7 +85,6 @@ export default class ChannelView extends React.PureComponent { url: props.match.url, channelId: props.channelId, deferredPostView: ChannelView.createDeferredPostView(), - showNextSteps: props.showNextSteps, }; } @@ -91,6 +96,12 @@ export default class ChannelView extends React.PureComponent { this.props.actions.goToLastViewedChannel(); } + componentDidMount() { + if (this.props.showNextSteps) { + this.props.actions.setShowNextStepsView(true); + } + } + componentDidUpdate(prevProps) { if (prevProps.channelId !== this.props.channelId || prevProps.channelIsArchived !== this.props.channelIsArchived) { mark('ChannelView#componentDidUpdate'); @@ -114,6 +125,10 @@ export default class ChannelView extends React.PureComponent { this.props.actions.goToLastViewedChannel(); } } + + if (this.props.match.url !== prevProps.match.url && this.props.showNextStepsEphemeral) { + this.props.actions.setShowNextStepsView(false); + } } render() { @@ -126,7 +141,7 @@ export default class ChannelView extends React.PureComponent { ); } - if (this.state.showNextSteps) { + if (this.props.showNextStepsEphemeral) { return ( ); diff --git a/components/channel_view/channel_view.test.jsx b/components/channel_view/channel_view.test.jsx index 21d372eee4cd..69cd2ff735d3 100644 --- a/components/channel_view/channel_view.test.jsx +++ b/components/channel_view/channel_view.test.jsx @@ -17,10 +17,12 @@ describe('components/channel_view', () => { }, showTutorial: false, showNextSteps: false, + showNextStepsEphemeral: false, channelIsArchived: false, viewArchivedChannels: false, actions: { goToLastViewedChannel: jest.fn(), + setShowNextStepsView: jest.fn(), }, }; diff --git a/components/channel_view/index.js b/components/channel_view/index.js index 89438c8c4655..a1ff4b766bfd 100644 --- a/components/channel_view/index.js +++ b/components/channel_view/index.js @@ -16,6 +16,7 @@ import {getDirectTeammate} from 'utils/utils.jsx'; import {TutorialSteps, Preferences} from 'utils/constants'; import {goToLastViewedChannel} from 'actions/views/channel'; +import {setShowNextStepsView} from 'actions/views/next_steps'; import {showNextSteps} from 'components/next_steps_view/steps'; import ChannelView from './channel_view.jsx'; @@ -59,6 +60,7 @@ function mapStateToProps(state) { deactivatedChannel: channel ? getDeactivatedChannel(state, channel.id) : false, showTutorial: enableTutorial && tutorialStep <= TutorialSteps.INTRO_SCREENS, showNextSteps: showNextSteps(state), + showNextStepsEphemeral: state.views.nextSteps.show, channelIsArchived: channel ? channel.delete_at !== 0 : false, viewArchivedChannels, }; @@ -67,6 +69,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ + setShowNextStepsView, goToLastViewedChannel, }, dispatch), }; diff --git a/components/generic_modal.tsx b/components/generic_modal.tsx index 1a8072051a3c..4748dc5c1846 100644 --- a/components/generic_modal.tsx +++ b/components/generic_modal.tsx @@ -9,6 +9,7 @@ import {FormattedMessage} from 'react-intl'; import './generic_modal.scss'; type Props = { + className?: string; onHide: () => void; modalHeaderText: React.ReactNode; show?: boolean; @@ -109,7 +110,7 @@ export default class GenericModal extends React.PureComponent { return ( { preferences: [], skuName: '', actions: { + setShowNextStepsView: jest.fn(), savePreferences: jest.fn(), }, }; diff --git a/components/next_steps_view/next_steps_view.tsx b/components/next_steps_view/next_steps_view.tsx index 2c948d55c864..965d46f83c7b 100644 --- a/components/next_steps_view/next_steps_view.tsx +++ b/components/next_steps_view/next_steps_view.tsx @@ -21,6 +21,7 @@ type Props = { skuName: string; actions: { savePreferences: (userId: string, preferences: PreferenceType[]) => void; + setShowNextStepsView: (show: boolean) => void; }; }; diff --git a/components/next_steps_view/steps.ts b/components/next_steps_view/steps.ts index 4cfe0750ead4..2ed010f7003a 100644 --- a/components/next_steps_view/steps.ts +++ b/components/next_steps_view/steps.ts @@ -3,8 +3,8 @@ import {createSelector} from 'reselect'; import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; -import {GlobalState} from 'mattermost-redux/types/store'; +import {GlobalState} from 'types/store'; import {RecommendedNextSteps, Preferences} from 'utils/constants'; import {localizeMessage} from 'utils/utils'; @@ -44,6 +44,10 @@ const getCategory = makeGetCategory(); export const showNextSteps = createSelector( (state: GlobalState) => getCategory(state, Preferences.RECOMMENDED_NEXT_STEPS), (stepPreferences) => { + if (stepPreferences.some((pref) => pref.name === RecommendedNextSteps.HIDE && pref.value)) { + return false; + } + const checkPref = (step: StepType) => stepPreferences.some((pref) => pref.name === step.id && pref.value); return !Steps.every(checkPref); } diff --git a/components/sidebar/__snapshots__/sidebar.test.tsx.snap b/components/sidebar/__snapshots__/sidebar.test.tsx.snap index 6ab71118f516..d4dec248c321 100644 --- a/components/sidebar/__snapshots__/sidebar.test.tsx.snap +++ b/components/sidebar/__snapshots__/sidebar.test.tsx.snap @@ -33,7 +33,7 @@ exports[`components/sidebar should match snapshot 1`] = ` onDragStart={[Function]} /> - + - + - + - + ({ + active: state.views.nextSteps.show, + showNextSteps: showNextSteps(state), + currentUserId: getCurrentUserId(state), + preferences: getCategory(state, Preferences.RECOMMENDED_NEXT_STEPS), + }); +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + actions: bindActionCreators({ + savePreferences, + openModal, + closeModal, + setShowNextStepsView, + }, dispatch), + }; +} + +export default connect(makeMapStateToProps, mapDispatchToProps)(SidebarNextSteps); diff --git a/components/sidebar/sidebar_next_steps/remove_next_steps_modal.scss b/components/sidebar/sidebar_next_steps/remove_next_steps_modal.scss new file mode 100644 index 000000000000..aa6aa4fd193e --- /dev/null +++ b/components/sidebar/sidebar_next_steps/remove_next_steps_modal.scss @@ -0,0 +1,69 @@ +.RemoveNextStepsModal__helpBox { + position: fixed; + top: 16px; + left: 201px; + z-index: 1050; +} + +.RemoveNextStepsModal__helpText { + font-weight: 600; + font-size: 12px; + line-height: 16px; + text-align: center; + color: var(--center-channel-bg); + display: block; + width: 188px; + margin-left: -12px; + margin-top: 4px; +} + +.modal .GenericModal.modal-dialog.RemoveNextStepsModal { + max-width: 514px; + margin-top: calc(50vh - 133px); + + .GenericModal__header { + text-align: center; + padding-bottom: 8px; + + h1 { + font-size: 24px; + line-height: 32px; + } + } + + .GenericModal__body { + text-align: center; + color: var(--center-channel-color); + } + + .modal-footer { + text-align: center; + } + + .modal-content { + padding: 40px 36px 24px 36px; + } + + .GenericModal__button { + padding: 13px 20px; + line-height: 14px; + + & +.GenericModal__button { + margin-left: 10px; + } + } +} + +.modal-backdrop.in { + clip-path: polygon(0 0, 0 63px, 240px 63px, 240px 0, 100% 0, 100% 100%, 0 100%); +} + +.multi-teams { + & ~ div .modal-backdrop.in { + clip-path: polygon(0 0, 65px 0, 65px 63px, 305px 63px, 305px 0, 100% 0, 100% 100%, 0 100%); + } + + & ~ .RemoveNextStepsModal__helpBox { + left: 266px; + } +} diff --git a/components/sidebar/sidebar_next_steps/remove_next_steps_modal.tsx b/components/sidebar/sidebar_next_steps/remove_next_steps_modal.tsx new file mode 100644 index 000000000000..3e78620f55a2 --- /dev/null +++ b/components/sidebar/sidebar_next_steps/remove_next_steps_modal.tsx @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; + +import FormattedMarkdownMessage from 'components/formatted_markdown_message.jsx'; +import GenericModal from 'components/generic_modal'; +import closeNextStepsArrow from 'images/close_next_steps_arrow.svg'; + +import './remove_next_steps_modal.scss'; + +type Props = { + screenTitle: string; + onConfirm: () => void; + onCancel: () => void; +} + +export default function RemoveNextStepsModal(props: Props) { + const {onConfirm, onCancel, screenTitle} = props; + + return ( + <> + {ReactDOM.createPortal( +
+ + + + +
, + document.body as HTMLElement + )} + + )} + confirmButtonText={( + + )} + > + + + + ); +} diff --git a/components/sidebar/sidebar_next_steps/sidebar_next_steps.scss b/components/sidebar/sidebar_next_steps/sidebar_next_steps.scss index 9f7d06c976cc..8da96addc155 100644 --- a/components/sidebar/sidebar_next_steps/sidebar_next_steps.scss +++ b/components/sidebar/sidebar_next_steps/sidebar_next_steps.scss @@ -2,8 +2,9 @@ width: 100%; color: var(--sidebar-text); padding: 8px 16px 18px 14px; - position: absolute; - bottom: 0; + align-self: flex-end; + position: relative; + background-color: var(--sidebar-bg); &.active { background-color: var(--sidebar-text-hover-bg); @@ -39,7 +40,7 @@ margin-left: auto; font-size: 14.4px; line-height: 14px; - + align-self: center; width: 28px; height: 28px; @@ -56,7 +57,7 @@ &:hover { background-color: var(--sidebar-text-hover-bg); - i { + i { opacity: 1; } } @@ -82,4 +83,4 @@ border-radius: 4px; background-color: var(--sidebar-text); } -} \ No newline at end of file +} diff --git a/components/sidebar/sidebar_next_steps/sidebar_next_steps.tsx b/components/sidebar/sidebar_next_steps/sidebar_next_steps.tsx index 2b6f21ff44c3..f7b8bb11f06f 100644 --- a/components/sidebar/sidebar_next_steps/sidebar_next_steps.tsx +++ b/components/sidebar/sidebar_next_steps/sidebar_next_steps.tsx @@ -3,14 +3,31 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; +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 ProgressBar from 'components/progress_bar'; +import {ModalIdentifiers, RecommendedNextSteps, Preferences} from 'utils/constants'; +import {localizeMessage} from 'utils/utils'; import './sidebar_next_steps.scss'; -type Props = { +import RemoveNextStepsModal from './remove_next_steps_modal'; +type Props = { + active: boolean; + showNextSteps: boolean; + currentUserId: string; + preferences: PreferenceType[]; + actions: { + savePreferences: (userId: string, preferences: PreferenceType[]) => void; + openModal: (modalData: {modalId: string; dialogType: any; dialogProps?: any}) => void; + closeModal: (modalId: string) => void; + setShowNextStepsView: (show: boolean) => void; + }; }; type State = { @@ -27,22 +44,93 @@ export default class SidebarNextSteps extends React.PureComponent } closeNextSteps = () => { - // TODO: Using this to test the progress bar for now - this.setState({complete: (this.state.complete + 1) % 4}); + const screenTitle = this.props.showNextSteps ? + localizeMessage('sidebar_next_steps.gettingStarted', 'Getting Started') : + localizeMessage('sidebar_next_steps.tipsAndNextSteps', 'Tips & Next Steps'); + + this.props.actions.openModal({ + modalId: ModalIdentifiers.REMOVE_NEXT_STEPS_MODAL, + dialogType: RemoveNextStepsModal, + dialogProps: { + screenTitle, + onConfirm: this.onConfirmModal, + onCancel: this.onCloseModal, + } + }); + } + + onCloseModal = () => { + this.props.actions.closeModal(ModalIdentifiers.REMOVE_NEXT_STEPS_MODAL); + } + + onConfirmModal = () => { + this.props.actions.savePreferences(this.props.currentUserId, [{ + user_id: this.props.currentUserId, + category: Preferences.RECOMMENDED_NEXT_STEPS, + name: RecommendedNextSteps.HIDE, + value: 'true', + }]); + + this.props.actions.setShowNextStepsView(false); + + this.onCloseModal(); } render() { - // TODO: Temporary values - const total = 3; + if (this.props.preferences.some((pref) => pref.name === RecommendedNextSteps.HIDE && pref.value)) { + return null; + } + + if (!this.props.active && !this.props.showNextSteps) { + return null; + } + + const total = Steps.length; + const complete = this.props.preferences.filter((pref) => pref.value).length; + + let header = ( + + ); + if (!this.props.showNextSteps) { + header = ( + + ); + } + + let middleSection = ( + + ); + if (!this.props.showNextSteps) { + middleSection = ( + + ); + } return ( -
+
- + {header}
- + {middleSection}
+ {this.props.showNextSteps &&
-
+
}
); } diff --git a/i18n/en.json b/i18n/en.json index d80409c3c8f1..3b4747824aaa 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3225,6 +3225,10 @@ "remove_group_confirm_button": "Yes, Remove Group and {memberCount, plural, one {Member} other {Members}}", "remove_group_confirm_message": "{memberCount, number} {memberCount, plural, one {member} other {members}} associated to this group will be removed from the team. Are you sure you wish to remove this group and {memberCount} {memberCount, plural, one {member} other {members}}?", "remove_group_confirm_title": "Remove Group and {memberCount, number} {memberCount, plural, one {Member} other {Members}}", + "remove_next_steps_modal.confirm": "Remove", + "remove_next_steps_modal.header": "Remove {title}?", + "remove_next_steps_modal.helpText": "Access {title} any time through the Main Menu", + "remove_next_steps_modal.mainText": "This will remove this section from your sidebar, but you can access it later in the Help section of the Main Menu.", "removed_channel.channelName": "the channel", "removed_channel.from": "Removed from ", "removed_channel.okay": "Okay", @@ -3399,7 +3403,9 @@ "sidebar_left.sidebar_channel_menu.unmuteChannel": "Unmute Channel", "sidebar_left.sidebar_channel_menu.unmuteConversation": "Unmute Conversation", "sidebar_next_steps.gettingStarted": "Getting Started", + "sidebar_next_steps.otherAreasToExplore": "A few other areas to explore", "sidebar_next_steps.stepsComplete": "{complete} / {total} steps complete", + "sidebar_next_steps.tipsAndNextSteps": "Tips & Next Steps", "sidebar_right_menu.console": "System Console", "sidebar_right_menu.flagged": "Saved Posts", "sidebar_right_menu.recentMentions": "Recent Mentions", diff --git a/images/close_next_steps_arrow.svg b/images/close_next_steps_arrow.svg new file mode 100644 index 000000000000..508fab2a162e --- /dev/null +++ b/images/close_next_steps_arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/reducers/views/index.js b/reducers/views/index.js index 2fa7cbf74676..f37edb2ab5db 100644 --- a/reducers/views/index.js +++ b/reducers/views/index.js @@ -21,6 +21,7 @@ import settings from './settings'; import marketplace from './marketplace'; import channelSidebar from './channel_sidebar'; import textbox from './textbox'; +import nextSteps from './next_steps'; export default combineReducers({ admin, @@ -41,4 +42,5 @@ export default combineReducers({ marketplace, textbox, channelSidebar, + nextSteps, }); diff --git a/reducers/views/next_steps.ts b/reducers/views/next_steps.ts new file mode 100644 index 000000000000..48d3a84d72e1 --- /dev/null +++ b/reducers/views/next_steps.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {combineReducers} from 'redux'; + +import {GenericAction} from 'mattermost-redux/types/actions'; +import {UserTypes} from 'mattermost-redux/action_types'; + +import {ActionTypes} from 'utils/constants'; + +export function show(state = false, action: GenericAction) { + switch (action.type) { + case ActionTypes.SET_SHOW_NEXT_STEPS_VIEW: + return action.show; + + case UserTypes.LOGOUT_SUCCESS: + return true; + default: + return state; + } +} + +export default combineReducers({ + show, +}); diff --git a/types/store/index.ts b/types/store/index.ts index b3fd04ebe6a8..fbed30c8a7cc 100644 --- a/types/store/index.ts +++ b/types/store/index.ts @@ -135,5 +135,9 @@ export type GlobalState = BaseGlobalState & { draggingState: DraggingState; newCategoryIds: string[]; }; + + nextSteps: { + show: boolean; + }; }; }; diff --git a/utils/constants.jsx b/utils/constants.jsx index fb5a2facefe5..f0aff45439cc 100644 --- a/utils/constants.jsx +++ b/utils/constants.jsx @@ -209,6 +209,8 @@ export const ActionTypes = keyMirror({ DISMISS_ANNOUNCEMENT_BAR: null, PREFETCH_POSTS_FOR_CHANNEL: null, + + SET_SHOW_NEXT_STEPS_VIEW: null, }); export const PostRequestTypes = keyMirror({ @@ -260,6 +262,7 @@ export const ModalIdentifiers = { DELETE_CATEGORY: 'delete_category', SIDEBAR_WHATS_NEW_MODAL: 'sidebar_whats_new_modal', WARN_METRIC_ACK: 'warn_metric_acknowledgement', + REMOVE_NEXT_STEPS_MODAL: 'remove_next_steps_modal', }; export const UserStatuses = {