[MM-52926] Deprecating work templates (#23466)
Co-authored-by: Mattermost Build <build@mattermost.com>
@@ -35,11 +35,9 @@ import KeyboardShortcutsModal from 'components/keyboard_shortcuts/keyboard_short
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import WorkTemplateModal from 'components/work_templates';
|
||||
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {isMarketplaceEnabled} from 'mattermost-redux/selectors/entities/general';
|
||||
import {areWorkTemplatesEnabled} from 'selectors/work_template';
|
||||
|
||||
import {doAppSubmit, openAppsModal, postEphemeralCallResponseForCommandArgs} from './apps';
|
||||
import {trackEvent} from './telemetry_actions';
|
||||
@@ -140,15 +138,6 @@ export function executeCommand(message: string, args: CommandArgs): ActionFunc {
|
||||
|
||||
dispatch(openModal({modalId: ModalIdentifiers.PLUGIN_MARKETPLACE, dialogType: MarketplaceModal, dialogProps: {openedFrom: 'command'}}));
|
||||
return {data: true};
|
||||
case '/templates': {
|
||||
const workTemplateEnabled = areWorkTemplatesEnabled(state);
|
||||
if (!workTemplateEnabled) {
|
||||
return {error: {message: localizeMessage('templates_command.disabled', 'Templates are disabled. Please contact your System Administrator for details.')}};
|
||||
}
|
||||
|
||||
dispatch(openModal({modalId: ModalIdentifiers.WORK_TEMPLATE, dialogType: WorkTemplateModal}));
|
||||
return {data: true};
|
||||
}
|
||||
case '/collapse':
|
||||
case '/expand':
|
||||
dispatch(PostActions.resetEmbedVisibility());
|
||||
|
||||
@@ -12,8 +12,6 @@ import {ActionTypes, Constants, ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import {getTeamRedirectChannelIfIsAccesible} from 'actions/global_actions';
|
||||
|
||||
import WorkTemplateModal from 'components/work_templates';
|
||||
|
||||
import {openModal} from './modals';
|
||||
|
||||
export function switchToChannels() {
|
||||
@@ -47,23 +45,6 @@ export function openInvitationsModal(timeout = 1) {
|
||||
};
|
||||
}
|
||||
|
||||
export function openWorkTemplateModal(redirectToChannels = true) {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
if (redirectToChannels) {
|
||||
dispatch(switchToChannels());
|
||||
}
|
||||
setTimeout(() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.WORK_TEMPLATE,
|
||||
dialogType: WorkTemplateModal,
|
||||
dialogProps: {
|
||||
},
|
||||
}));
|
||||
}, redirectToChannels ? 1000 : 1);
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function setShowOnboardingTaskCompletion(open: boolean) {
|
||||
return {
|
||||
type: ActionTypes.SHOW_ONBOARDING_TASK_COMPLETION,
|
||||
|
||||
@@ -7,7 +7,6 @@ export const OnboardingTaskCategory = 'onboarding_task_list';
|
||||
|
||||
// Whole task list is based on these
|
||||
export const OnboardingTasksName = {
|
||||
CREATE_FROM_WORK_TEMPLATE: 'create_from_work_template',
|
||||
CHANNELS_TOUR: 'channels_tour',
|
||||
BOARDS_TOUR: 'boards_tour',
|
||||
PLAYBOOKS_TOUR: 'playbooks_tour',
|
||||
@@ -37,7 +36,6 @@ export const GenericTaskSteps = {
|
||||
};
|
||||
|
||||
export const TaskNameMapToSteps = {
|
||||
[OnboardingTasksName.CREATE_FROM_WORK_TEMPLATE]: GenericTaskSteps,
|
||||
[OnboardingTasksName.CHANNELS_TOUR]: GenericTaskSteps,
|
||||
[OnboardingTasksName.BOARDS_TOUR]: GenericTaskSteps,
|
||||
[OnboardingTasksName.PLAYBOOKS_TOUR]: GenericTaskSteps,
|
||||
|
||||
@@ -32,7 +32,6 @@ import {isCurrentUserGuestUser, isCurrentUserSystemAdmin, isFirstAdmin} from 'ma
|
||||
import {GlobalState} from 'types/store';
|
||||
import {
|
||||
openInvitationsModal,
|
||||
openWorkTemplateModal,
|
||||
setShowOnboardingCompleteProfileTour,
|
||||
setShowOnboardingVisitConsoleTour,
|
||||
switchToChannels,
|
||||
@@ -43,14 +42,12 @@ import {ModalIdentifiers, TELEMETRY_CATEGORIES, ExploreOtherToolsTourSteps} from
|
||||
import BullsEye from 'components/common/svg_images_components/bulls_eye_svg';
|
||||
import Channels from 'components/common/svg_images_components/channels_svg';
|
||||
import Clipboard from 'components/common/svg_images_components/clipboard_svg';
|
||||
import Newspaper from 'components/common/svg_images_components/newspaper_svg';
|
||||
import Gears from 'components/common/svg_images_components/gears_svg';
|
||||
import Handshake from 'components/common/svg_images_components/handshake_svg';
|
||||
import Phone from 'components/common/svg_images_components/phone_svg';
|
||||
import Security from 'components/common/svg_images_components/security_svg';
|
||||
import Sunglasses from 'components/common/svg_images_components/sunglasses_svg';
|
||||
import Wrench from 'components/common/svg_images_components/wrench_svg';
|
||||
import {areWorkTemplatesEnabled} from 'selectors/work_template';
|
||||
|
||||
import {OnboardingTaskCategory, OnboardingTaskList, OnboardingTasksName, TaskNameMapToSteps} from './constants';
|
||||
import {generateTelemetryTag} from './utils';
|
||||
@@ -60,14 +57,6 @@ const getCategory = makeGetCategory();
|
||||
const useGetTaskDetails = () => {
|
||||
const {formatMessage} = useIntl();
|
||||
return {
|
||||
[OnboardingTasksName.CREATE_FROM_WORK_TEMPLATE]: {
|
||||
id: 'task_create_from_work_template',
|
||||
svg: Newspaper,
|
||||
message: formatMessage({
|
||||
id: 'onboardingTask.checklist.task_create_from_work_template',
|
||||
defaultMessage: 'Create from a template.',
|
||||
}),
|
||||
},
|
||||
[OnboardingTasksName.CHANNELS_TOUR]: {
|
||||
id: 'task_learn_more_about_messaging',
|
||||
svg: Channels,
|
||||
@@ -158,7 +147,6 @@ export const useTasksList = () => {
|
||||
const isThinOnBoardingTaskList = useSelector((state: GlobalState) => {
|
||||
return isReduceOnBoardingTaskList(state);
|
||||
});
|
||||
const workTemplateEnabled = useSelector(areWorkTemplatesEnabled);
|
||||
|
||||
// Cloud conditions
|
||||
const subscription = useSelector((state: GlobalState) => state.entities.cloud.subscription);
|
||||
@@ -205,10 +193,6 @@ export const useTasksList = () => {
|
||||
delete list.VISIT_SYSTEM_CONSOLE;
|
||||
}
|
||||
|
||||
if (!workTemplateEnabled) {
|
||||
delete list.CREATE_FROM_WORK_TEMPLATE;
|
||||
}
|
||||
|
||||
return Object.values(list);
|
||||
};
|
||||
|
||||
@@ -286,12 +270,6 @@ export const useHandleOnBoardingTaskTrigger = () => {
|
||||
|
||||
return (taskName: string) => {
|
||||
switch (taskName) {
|
||||
case OnboardingTasksName.CREATE_FROM_WORK_TEMPLATE: {
|
||||
localStorage.setItem(OnboardingTaskCategory, 'true');
|
||||
dispatch(openWorkTemplateModal(inAdminConsole));
|
||||
handleSaveData(taskName, TaskNameMapToSteps[taskName].FINISHED, true);
|
||||
break;
|
||||
}
|
||||
case OnboardingTasksName.CHANNELS_TOUR: {
|
||||
handleSaveData(taskName, TaskNameMapToSteps[taskName].STARTED, true);
|
||||
const tourCategory = TutorialTourName.ONBOARDING_TUTORIAL_STEP;
|
||||
|
||||
@@ -15,15 +15,12 @@ import {Team} from '@mattermost/types/teams';
|
||||
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentTeam, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getFirstAdminSetupComplete, getConfig, getLicense, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getFirstAdminSetupComplete, getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {CategoryOther} from '@mattermost/types/work_templates';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {getSiteURL, teamNameToUrl} from 'utils/url';
|
||||
import {makeNewTeam} from 'utils/team_utils';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {pageVisited, trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
@@ -45,8 +42,6 @@ import {
|
||||
} from './steps';
|
||||
|
||||
import Organization from './organization';
|
||||
import Roles from './roles';
|
||||
import RolesIllustration from './roles_illustration';
|
||||
import Plugins from './plugins';
|
||||
import Progress from './progress';
|
||||
import InviteMembers from './invite_members';
|
||||
@@ -95,7 +90,6 @@ function makeSubmitFail(step: WizardStep) {
|
||||
|
||||
const trackSubmitFail = {
|
||||
[WizardSteps.Organization]: makeSubmitFail(WizardSteps.Organization),
|
||||
[WizardSteps.Roles]: makeSubmitFail(WizardSteps.Roles),
|
||||
[WizardSteps.Plugins]: makeSubmitFail(WizardSteps.Plugins),
|
||||
[WizardSteps.InviteMembers]: makeSubmitFail(WizardSteps.InviteMembers),
|
||||
[WizardSteps.LaunchingWorkspace]: makeSubmitFail(WizardSteps.LaunchingWorkspace),
|
||||
@@ -103,7 +97,6 @@ const trackSubmitFail = {
|
||||
|
||||
const onPageViews = {
|
||||
[WizardSteps.Organization]: makeOnPageView(WizardSteps.Organization),
|
||||
[WizardSteps.Roles]: makeOnPageView(WizardSteps.Roles),
|
||||
[WizardSteps.Plugins]: makeOnPageView(WizardSteps.Plugins),
|
||||
[WizardSteps.InviteMembers]: makeOnPageView(WizardSteps.InviteMembers),
|
||||
[WizardSteps.LaunchingWorkspace]: makeOnPageView(WizardSteps.LaunchingWorkspace),
|
||||
@@ -118,7 +111,6 @@ const PreparingWorkspace = (props: Props) => {
|
||||
});
|
||||
const isUserFirstAdmin = useSelector(isFirstAdmin);
|
||||
const onboardingFlowEnabled = useSelector(getIsOnboardingFlowEnabled);
|
||||
const isWorkTemplateEnabled = useSelector((state: GlobalState) => getFeatureFlagValue(state, 'WorkTemplate') === 'true');
|
||||
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
const myTeams = useSelector(getMyTeams);
|
||||
@@ -136,7 +128,6 @@ const PreparingWorkspace = (props: Props) => {
|
||||
|
||||
const stepOrder = [
|
||||
isSelfHosted && WizardSteps.Organization,
|
||||
isWorkTemplateEnabled && WizardSteps.Roles,
|
||||
pluginsEnabled && WizardSteps.Plugins,
|
||||
WizardSteps.InviteMembers,
|
||||
WizardSteps.LaunchingWorkspace,
|
||||
@@ -265,7 +256,6 @@ const PreparingWorkspace = (props: Props) => {
|
||||
// even if admin skipped submitting plugins.
|
||||
const completeSetupRequest = {
|
||||
organization: form.organization,
|
||||
role: form.role === CategoryOther ? form.roleOther : form.role,
|
||||
install_plugins: pluginsToSetup,
|
||||
};
|
||||
|
||||
@@ -375,15 +365,6 @@ const PreparingWorkspace = (props: Props) => {
|
||||
return '';
|
||||
}, [currentStep]);
|
||||
|
||||
const getRolesAnimationClass = useCallback(() => {
|
||||
if (currentStep === WizardSteps.Roles) {
|
||||
return 'enter';
|
||||
} else if (mostRecentStep === WizardSteps.Roles) {
|
||||
return 'exit';
|
||||
}
|
||||
return '';
|
||||
}, [currentStep]);
|
||||
|
||||
let previous: React.ReactNode = (
|
||||
<div
|
||||
onClick={goPrevious}
|
||||
@@ -450,48 +431,6 @@ const PreparingWorkspace = (props: Props) => {
|
||||
updateTeam={updateTeam}
|
||||
/>
|
||||
|
||||
<Roles
|
||||
onPageView={onPageViews[WizardSteps.Roles]}
|
||||
previous={previous}
|
||||
next={() => {
|
||||
makeNext(WizardSteps.Roles)({
|
||||
role: form.role,
|
||||
roleOther: form.roleOther,
|
||||
});
|
||||
}}
|
||||
quickNext={(role: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
role,
|
||||
roleOther: '',
|
||||
});
|
||||
makeNext(WizardSteps.Roles)({
|
||||
role,
|
||||
roleOther: '',
|
||||
});
|
||||
}}
|
||||
skip={() => {
|
||||
setForm({
|
||||
...form,
|
||||
role: '',
|
||||
roleOther: '',
|
||||
});
|
||||
makeNext(WizardSteps.Roles, true)();
|
||||
}}
|
||||
transitionDirection={getTransitionDirection(WizardSteps.Roles)}
|
||||
show={shouldShowPage(WizardSteps.Roles)}
|
||||
role={form.role}
|
||||
roleOther={form.roleOther}
|
||||
setRole={(role: Form['role'], roleOther: Form['roleOther']) => {
|
||||
setForm({
|
||||
...form,
|
||||
role,
|
||||
roleOther,
|
||||
});
|
||||
}}
|
||||
className='child-page'
|
||||
/>
|
||||
|
||||
<Plugins
|
||||
isSelfHosted={isSelfHosted}
|
||||
onPageView={onPageViews[WizardSteps.Plugins]}
|
||||
@@ -569,9 +508,6 @@ const PreparingWorkspace = (props: Props) => {
|
||||
<div className={`PreparingWorkspace__invite-members-illustration ${getInviteMembersAnimationClass()}`}>
|
||||
<InviteMembersIllustration/>
|
||||
</div>
|
||||
<div className={`PreparingWorkspace__roles-illustration ${getRolesAnimationClass()}`}>
|
||||
<RolesIllustration/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
@import 'utils/mixins';
|
||||
|
||||
$btn-margin-right: 24px;
|
||||
|
||||
.plugins-skip-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.Roles__input {
|
||||
width: calc(100% - $btn-margin-right);
|
||||
padding: 10px 16px;
|
||||
border: 2px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
border: 2px solid var(--button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.Roles-body {
|
||||
.PreparingWorkspacePageBody {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.Roles-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.role-button {
|
||||
@include secondary-button;
|
||||
@include button-large;
|
||||
|
||||
border-color: rgba(63, 67, 80, 0.16);
|
||||
margin-right: $btn-margin-right;
|
||||
margin-bottom: 24px;
|
||||
color: #3f4350;
|
||||
|
||||
&.active {
|
||||
background: rgba(var(--denim-button-bg-rgb), 0.16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include simple-in-and-out("Roles");
|
||||
@@ -1,158 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {CSSTransition} from 'react-transition-group';
|
||||
|
||||
import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps} from './steps';
|
||||
|
||||
import Title from './title';
|
||||
import Description from './description';
|
||||
import PageBody from './page_body';
|
||||
import SingleColumnLayout from './single_column_layout';
|
||||
|
||||
import PageLine from './page_line';
|
||||
import './roles.scss';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {getWorkTemplateCategories as fetchCategories} from 'mattermost-redux/actions/work_templates';
|
||||
import {getWorkTemplateCategories} from 'selectors/work_template';
|
||||
import classNames from 'classnames';
|
||||
import QuickInput from 'components/quick_input';
|
||||
import {CategoryOther} from '@mattermost/types/work_templates';
|
||||
|
||||
type Props = PreparingWorkspacePageProps & {
|
||||
role: Form['role'];
|
||||
roleOther: Form['roleOther'];
|
||||
setRole: (role: string, roleOther: string) => void;
|
||||
className?: string;
|
||||
quickNext(role: string): void;
|
||||
}
|
||||
const Roles = ({role, next, ...props}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const categories = useSelector(getWorkTemplateCategories);
|
||||
|
||||
let className = 'Roles-body';
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCategories());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.show) {
|
||||
props.onPageView();
|
||||
}
|
||||
}, [props.show]);
|
||||
|
||||
if (props.className) {
|
||||
className += ' ' + props.className;
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.roles.title'}
|
||||
defaultMessage='What is your primary function?'
|
||||
/>
|
||||
);
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.roles.description'}
|
||||
defaultMessage={'We’ll use this to suggest templates to help get you started.'}
|
||||
/>
|
||||
);
|
||||
|
||||
const selectRole = (role: string) => {
|
||||
if (role !== CategoryOther) {
|
||||
props.quickNext(role);
|
||||
}
|
||||
props.setRole(role, '');
|
||||
};
|
||||
|
||||
const roleIsSet = Boolean(role);
|
||||
const roleOtherIsSet = Boolean(props.roleOther);
|
||||
const canContinue = (roleIsSet && role !== CategoryOther) || (role === CategoryOther && roleOtherIsSet);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
in={props.show}
|
||||
timeout={Animations.PAGE_SLIDE}
|
||||
classNames={mapAnimationReasonToClass('Roles', props.transitionDirection)}
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
<div className={className}>
|
||||
<SingleColumnLayout>
|
||||
<PageLine
|
||||
style={{
|
||||
marginBottom: '50px',
|
||||
marginLeft: '50px',
|
||||
height: 'calc(25vh)',
|
||||
}}
|
||||
noLeft={true}
|
||||
/>
|
||||
{props.previous}
|
||||
<Title>
|
||||
{title}
|
||||
</Title>
|
||||
<Description>{description}</Description>
|
||||
<PageBody>
|
||||
<div className='Roles-list'>
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
className={classNames('role-button', {active: category.id === role})}
|
||||
onClick={() => selectRole(category.id)}
|
||||
>
|
||||
{category.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{role === CategoryOther && (
|
||||
<QuickInput
|
||||
value={props.roleOther || ''}
|
||||
className='Roles__input'
|
||||
onChange={(e) => props.setRole(CategoryOther, e.target.value)}
|
||||
placeholder={formatMessage({
|
||||
id: 'onboarding_wizard.roles.other_input_placeholder',
|
||||
defaultMessage: 'Please share your primary function',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</PageBody>
|
||||
<div>
|
||||
<button
|
||||
className='primary-button'
|
||||
onClick={next}
|
||||
disabled={!canContinue}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.next'}
|
||||
defaultMessage='Continue'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className='link-style plugins-skip-btn'
|
||||
onClick={props.skip}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.skip-button'}
|
||||
defaultMessage='Skip'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<PageLine
|
||||
style={{
|
||||
marginTop: '50px',
|
||||
marginLeft: '50px',
|
||||
height: 'calc(30vh)',
|
||||
}}
|
||||
noLeft={true}
|
||||
/>
|
||||
</SingleColumnLayout>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default Roles;
|
||||
@@ -5,7 +5,6 @@ import deepFreeze from 'mattermost-redux/utils/deep_freeze';
|
||||
|
||||
export const WizardSteps = {
|
||||
Organization: 'Organization',
|
||||
Roles: 'Roles',
|
||||
Plugins: 'Plugins',
|
||||
InviteMembers: 'InviteMembers',
|
||||
LaunchingWorkspace: 'LaunchingWorkspace',
|
||||
@@ -25,8 +24,6 @@ export function mapStepToNextName(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'admin_onboarding_next_organization';
|
||||
case WizardSteps.Roles:
|
||||
return 'admin_onboarding_next_roles';
|
||||
case WizardSteps.Plugins:
|
||||
return 'admin_onboarding_next_plugins';
|
||||
case WizardSteps.InviteMembers:
|
||||
@@ -42,8 +39,6 @@ export function mapStepToPrevious(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'admin_onboarding_previous_organization';
|
||||
case WizardSteps.Roles:
|
||||
return 'admin_onboarding_previous_roles';
|
||||
case WizardSteps.Plugins:
|
||||
return 'admin_onboarding_previous_plugins';
|
||||
case WizardSteps.InviteMembers:
|
||||
@@ -59,8 +54,6 @@ export function mapStepToPageView(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'pageview_admin_onboarding_organization';
|
||||
case WizardSteps.Roles:
|
||||
return 'pageview_admin_onboarding_roles';
|
||||
case WizardSteps.Plugins:
|
||||
return 'pageview_admin_onboarding_plugins';
|
||||
case WizardSteps.InviteMembers:
|
||||
@@ -76,8 +69,6 @@ export function mapStepToSubmitFail(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'admin_onboarding_organization_submit_fail';
|
||||
case WizardSteps.Roles:
|
||||
return 'admin_onboarding_roles_submit_fail';
|
||||
case WizardSteps.Plugins:
|
||||
return 'admin_onboarding_plugins_submit_fail';
|
||||
case WizardSteps.InviteMembers:
|
||||
@@ -93,8 +84,6 @@ export function mapStepToSkipName(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'admin_onboarding_skip_organization';
|
||||
case WizardSteps.Roles:
|
||||
return 'admin_onboarding_skip_roles';
|
||||
case WizardSteps.Plugins:
|
||||
return 'admin_onboarding_skip_plugins';
|
||||
case WizardSteps.InviteMembers:
|
||||
@@ -138,8 +127,6 @@ export const PLUGIN_NAME_TO_ID_MAP: PluginNameMap = {
|
||||
|
||||
export type Form = {
|
||||
organization?: string;
|
||||
role?: string;
|
||||
roleOther?: string;
|
||||
url?: string;
|
||||
urlSkipped: boolean;
|
||||
inferredProtocol: 'http' | 'https' | null;
|
||||
@@ -197,8 +184,6 @@ export const emptyForm = deepFreeze({
|
||||
invites: [],
|
||||
skipped: false,
|
||||
},
|
||||
role: '',
|
||||
roleOther: '',
|
||||
});
|
||||
|
||||
export type PreparingWorkspacePageProps = {
|
||||
|
||||
@@ -17,7 +17,6 @@ exports[`components/sidebar should match snapshot 1`] = `
|
||||
showCreateUserGroupModal={[Function]}
|
||||
showMoreChannelsModal={[Function]}
|
||||
showNewChannelModal={[Function]}
|
||||
showWorkTemplateButton={true}
|
||||
unreadFilterEnabled={false}
|
||||
userGroupsEnabled={false}
|
||||
/>
|
||||
@@ -72,7 +71,6 @@ exports[`components/sidebar should match snapshot when direct channels modal is
|
||||
showCreateUserGroupModal={[Function]}
|
||||
showMoreChannelsModal={[Function]}
|
||||
showNewChannelModal={[Function]}
|
||||
showWorkTemplateButton={true}
|
||||
unreadFilterEnabled={false}
|
||||
userGroupsEnabled={false}
|
||||
/>
|
||||
@@ -131,7 +129,6 @@ exports[`components/sidebar should match snapshot when more channels modal is op
|
||||
showCreateUserGroupModal={[Function]}
|
||||
showMoreChannelsModal={[Function]}
|
||||
showNewChannelModal={[Function]}
|
||||
showWorkTemplateButton={true}
|
||||
unreadFilterEnabled={false}
|
||||
userGroupsEnabled={false}
|
||||
/>
|
||||
|
||||
@@ -11,8 +11,6 @@ import Menu from 'components/widgets/menu/menu';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import {CreateAndJoinChannelsTour, InvitePeopleTour} from 'components/tours/onboarding_tour';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import WorkTemplateModal from 'components/work_templates';
|
||||
|
||||
type Props = {
|
||||
canCreateChannel: boolean;
|
||||
@@ -30,7 +28,6 @@ type Props = {
|
||||
isAddChannelOpen: boolean;
|
||||
openAddChannelOpen: (open: boolean) => void;
|
||||
canCreateCustomGroups: boolean;
|
||||
showWorkTemplateButton: boolean;
|
||||
};
|
||||
|
||||
const AddChannelDropdown = ({
|
||||
@@ -48,7 +45,6 @@ const AddChannelDropdown = ({
|
||||
isAddChannelOpen,
|
||||
openAddChannelOpen,
|
||||
canCreateCustomGroups,
|
||||
showWorkTemplateButton,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -66,21 +62,6 @@ const AddChannelDropdown = ({
|
||||
</Menu.Group>
|
||||
);
|
||||
|
||||
let workTemplate;
|
||||
if (showWorkTemplateButton) {
|
||||
workTemplate = (
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='work-template'
|
||||
modalId={ModalIdentifiers.WORK_TEMPLATE}
|
||||
dialogType={WorkTemplateModal}
|
||||
text={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.work_template', defaultMessage: 'Create from a template'})}
|
||||
extraText={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.work_template_extra', defaultMessage: 'Link channels, boards, and playbooks together'})}
|
||||
icon={<i className='icon-layers-outline'/>}
|
||||
className='work-template'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let joinPublicChannel;
|
||||
if (canJoinPublicChannel) {
|
||||
joinPublicChannel = (
|
||||
@@ -142,7 +123,6 @@ const AddChannelDropdown = ({
|
||||
return (
|
||||
<>
|
||||
<Menu.Group>
|
||||
{workTemplate}
|
||||
{createChannel}
|
||||
{joinPublicChannel}
|
||||
{createDirectMessage}
|
||||
|
||||
@@ -22,7 +22,6 @@ import {getIsLhsOpen} from 'selectors/lhs';
|
||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
import {areWorkTemplatesEnabled} from 'selectors/work_template';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import Sidebar from './sidebar';
|
||||
@@ -44,8 +43,6 @@ function mapStateToProps(state: GlobalState) {
|
||||
|
||||
const canCreateCustomGroups = haveISystemPermission(state, {permission: Permissions.CREATE_CUSTOM_GROUP}) && isCustomGroupsEnabled(state);
|
||||
|
||||
const showWorkTemplateButton = areWorkTemplatesEnabled(state);
|
||||
|
||||
return {
|
||||
teamId: currentTeam ? currentTeam.id : '',
|
||||
canCreatePrivateChannel,
|
||||
@@ -66,7 +63,6 @@ function mapStateToProps(state: GlobalState) {
|
||||
canCreateCustomGroups,
|
||||
rhsState: getRhsState(state),
|
||||
rhsOpen: getIsRhsOpen(state),
|
||||
showWorkTemplateButton,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ describe('components/sidebar', () => {
|
||||
isKeyBoardShortcutModalOpen: false,
|
||||
userGroupsEnabled: false,
|
||||
canCreateCustomGroups: true,
|
||||
showWorkTemplateButton: true,
|
||||
actions: {
|
||||
createCategory: jest.fn(),
|
||||
fetchMyCategories: jest.fn(),
|
||||
|
||||
@@ -53,7 +53,6 @@ type Props = {
|
||||
canCreateCustomGroups: boolean;
|
||||
rhsState?: RhsState;
|
||||
rhsOpen?: boolean;
|
||||
showWorkTemplateButton: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -256,7 +255,6 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
||||
unreadFilterEnabled={this.props.unreadFilterEnabled}
|
||||
userGroupsEnabled={this.props.userGroupsEnabled}
|
||||
canCreateCustomGroups={this.props.canCreateCustomGroups}
|
||||
showWorkTemplateButton={this.props.showWorkTemplateButton}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
|
||||
@@ -37,7 +37,6 @@ describe('Components/SidebarHeader', () => {
|
||||
showCreateUserGroupModal: jest.fn(),
|
||||
userGroupsEnabled: false,
|
||||
canCreateCustomGroups: true,
|
||||
showWorkTemplateButton: true,
|
||||
};
|
||||
|
||||
mockState = {
|
||||
|
||||
@@ -98,7 +98,6 @@ export type Props = {
|
||||
unreadFilterEnabled: boolean;
|
||||
userGroupsEnabled: boolean;
|
||||
canCreateCustomGroups: boolean;
|
||||
showWorkTemplateButton: boolean;
|
||||
}
|
||||
|
||||
const SidebarHeader: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
@@ -164,7 +163,6 @@ const SidebarHeader: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
canCreateCustomGroups={props.canCreateCustomGroups}
|
||||
showCreateUserGroupModal={props.showCreateUserGroupModal}
|
||||
userGroupsEnabled={props.userGroupsEnabled}
|
||||
showWorkTemplateButton={props.showWorkTemplateButton}
|
||||
/>
|
||||
</SidebarHeaderContainer>
|
||||
</CompassThemeProvider>
|
||||
|
||||
@@ -17,8 +17,6 @@ export const ChannelsTour = 'channels_tour';
|
||||
|
||||
export const OtherToolsTour = 'other_tools_tour';
|
||||
|
||||
export const WorkTemplatesTour = 'work_templates_tour';
|
||||
|
||||
export const TutorialTourName = {
|
||||
ONBOARDING_TUTORIAL_STEP: 'tutorial_step',
|
||||
ONBOARDING_TUTORIAL_STEP_FOR_GUESTS: 'tutorial_step_for_guest',
|
||||
@@ -26,7 +24,6 @@ export const TutorialTourName = {
|
||||
CRT_THREAD_PANE_STEP: 'crt_thread_pane_step',
|
||||
AUTO_TOUR_STATUS: 'auto_tour_status',
|
||||
EXPLORE_OTHER_TOOLS: 'explore_tools',
|
||||
WORK_TEMPLATE_TUTORIAL: 'work_template',
|
||||
};
|
||||
|
||||
export const OnboardingTourSteps = {
|
||||
@@ -57,12 +54,6 @@ export const CrtTutorialSteps = {
|
||||
FINISHED,
|
||||
};
|
||||
|
||||
export const WorkTemplateTourSteps = {
|
||||
PLAYBOOKS_TOUR_TIP: 0,
|
||||
BOARDS_TOUR_TIP: 1,
|
||||
FINISHED,
|
||||
};
|
||||
|
||||
export const CrtTutorialTriggerSteps = {
|
||||
START: 0,
|
||||
STARTED: 1,
|
||||
@@ -74,7 +65,6 @@ export const TTNameMapToATStatusKey = {
|
||||
[TutorialTourName.CRT_TUTORIAL_STEP]: 'crt_tutorial_auto_tour_status',
|
||||
[TutorialTourName.CRT_THREAD_PANE_STEP]: TutorialTourName.CRT_THREAD_PANE_STEP + AutoStatusSuffix,
|
||||
[TutorialTourName.EXPLORE_OTHER_TOOLS]: TutorialTourName.EXPLORE_OTHER_TOOLS + AutoStatusSuffix,
|
||||
[TutorialTourName.WORK_TEMPLATE_TUTORIAL]: TutorialTourName.WORK_TEMPLATE_TUTORIAL + AutoStatusSuffix,
|
||||
};
|
||||
|
||||
export const TTNameMapToTourSteps = {
|
||||
@@ -82,5 +72,4 @@ export const TTNameMapToTourSteps = {
|
||||
[TutorialTourName.ONBOARDING_TUTORIAL_STEP_FOR_GUESTS]: OnboardingTourStepsForGuestUsers,
|
||||
[TutorialTourName.CRT_TUTORIAL_STEP]: CrtTutorialSteps,
|
||||
[TutorialTourName.EXPLORE_OTHER_TOOLS]: ExploreOtherToolsTourSteps,
|
||||
[TutorialTourName.WORK_TEMPLATE_TUTORIAL]: WorkTemplateTourSteps,
|
||||
};
|
||||
|
||||
@@ -5,23 +5,16 @@ import {useCallback} from 'react';
|
||||
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {without} from 'lodash';
|
||||
|
||||
import {getCurrentUserId, isCurrentUserGuestUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getWorkTemplatesLinkedProducts} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {close as closeLhs, open as openLhs} from 'actions/views/lhs';
|
||||
import {setAddChannelDropdown} from 'actions/views/add_channel_dropdown';
|
||||
import {switchToChannels} from 'actions/views/onboarding_tasks';
|
||||
import {showRHSPlugin} from 'actions/views/rhs';
|
||||
import {setProductMenuSwitcherOpen} from 'actions/views/product_menu';
|
||||
|
||||
import {useGetRHSPluggablesIds} from 'components/work_templates/hooks';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {suitePluginIds} from 'utils/constants';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {useGetPluginsActivationState} from 'plugins/useGetPluginsActivationState';
|
||||
|
||||
@@ -34,13 +27,11 @@ import {
|
||||
OnboardingTourSteps,
|
||||
TTNameMapToTourSteps,
|
||||
TutorialTourName,
|
||||
WorkTemplateTourSteps,
|
||||
} from './constant';
|
||||
|
||||
export const useGetTourSteps = (tourCategory: string) => {
|
||||
const isGuestUser = useSelector((state: GlobalState) => isCurrentUserGuestUser(state));
|
||||
|
||||
const workTemplatesLinkedItems = useSelector(getWorkTemplatesLinkedProducts);
|
||||
let tourSteps: Record<string, number> = TTNameMapToTourSteps[tourCategory];
|
||||
|
||||
const {playbooksPlugin, playbooksProductEnabled, boardsPlugin, boardsProductEnabled} = useGetPluginsActivationState();
|
||||
@@ -55,17 +46,6 @@ export const useGetTourSteps = (tourCategory: string) => {
|
||||
delete steps.BOARDS_TOUR;
|
||||
}
|
||||
tourSteps = steps;
|
||||
} else if (tourCategory === TutorialTourName.WORK_TEMPLATE_TUTORIAL) {
|
||||
const steps: Record<string, number> = tourSteps as typeof WorkTemplateTourSteps;
|
||||
|
||||
if (workTemplatesLinkedItems.playbooks && workTemplatesLinkedItems.playbooks === 0) {
|
||||
delete steps.PLAYBOOKS_TOUR;
|
||||
}
|
||||
|
||||
if (workTemplatesLinkedItems.boards && workTemplatesLinkedItems.boards === 0) {
|
||||
delete steps.BOARDS_TOUR;
|
||||
}
|
||||
tourSteps = steps;
|
||||
} else if (tourCategory === TutorialTourName.ONBOARDING_TUTORIAL_STEP && isGuestUser) {
|
||||
// restrict the 'learn more about messaging' tour when user is guest (townSquare, channel creation and user invite are restricted to guests)
|
||||
tourSteps = TTNameMapToTourSteps[TutorialTourName.ONBOARDING_TUTORIAL_STEP_FOR_GUESTS];
|
||||
@@ -76,12 +56,6 @@ export const useHandleNavigationAndExtraActions = (tourCategory: string) => {
|
||||
const dispatch = useDispatch();
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
const teamUrl = useSelector((state: GlobalState) => getCurrentRelativeTeamUrl(state));
|
||||
const {pluggableId, rhsPluggableIds} = useGetRHSPluggablesIds();
|
||||
const pluggableIds = [rhsPluggableIds.get(suitePluginIds.boards), rhsPluggableIds.get(suitePluginIds.playbooks)];
|
||||
|
||||
const channelLinkedItems = useSelector(getWorkTemplatesLinkedProducts);
|
||||
const boardsCount = channelLinkedItems?.boards || 0;
|
||||
const playbooksCount = channelLinkedItems?.playbooks || 0;
|
||||
|
||||
const nextStepActions = useCallback((step: number) => {
|
||||
if (tourCategory === TutorialTourName.ONBOARDING_TUTORIAL_STEP) {
|
||||
@@ -165,26 +139,6 @@ export const useHandleNavigationAndExtraActions = (tourCategory: string) => {
|
||||
}
|
||||
default:
|
||||
}
|
||||
} else if (tourCategory === TutorialTourName.WORK_TEMPLATE_TUTORIAL) {
|
||||
const navigationPluggableId = without(pluggableIds, pluggableId)[0];
|
||||
const stepMatches = step === WorkTemplateTourSteps.BOARDS_TOUR_TIP || step === WorkTemplateTourSteps.PLAYBOOKS_TOUR_TIP;
|
||||
const multiStep = Boolean(boardsCount && playbooksCount);
|
||||
|
||||
if (!multiStep) {
|
||||
const preferences = [
|
||||
{
|
||||
user_id: currentUserId,
|
||||
category: TutorialTourName.WORK_TEMPLATE_TUTORIAL,
|
||||
name: currentUserId,
|
||||
value: FINISHED.toString(),
|
||||
},
|
||||
];
|
||||
dispatch(savePreferences(currentUserId, preferences));
|
||||
return;
|
||||
}
|
||||
if (navigationPluggableId && stepMatches) {
|
||||
dispatch(showRHSPlugin(navigationPluggableId));
|
||||
}
|
||||
}
|
||||
}, [currentUserId, teamUrl, tourCategory]);
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {useFollowElementDimensions, useMeasurePunchouts} from '@mattermost/components';
|
||||
|
||||
import OnboardingWorkTemplateTourTip from './worktemplate_explore_tour_tip';
|
||||
import {useShowTourTip} from './useShowTourTip';
|
||||
|
||||
export const BoardsTourTip = (): JSX.Element | null => {
|
||||
const {formatMessage} = useIntl();
|
||||
const {playbooksCount, boardsCount, showBoardsTour} = useShowTourTip();
|
||||
const dimensions = useFollowElementDimensions('sidebar-right');
|
||||
const overlayPunchOut = useMeasurePunchouts(['sidebar-right'], [dimensions?.width]);
|
||||
|
||||
if (!showBoardsTour) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='pluggable_rhs.tourtip.boards.title'
|
||||
defaultMessage={'Access your {count} linked {count, plural, one {board} other {boards}}!'}
|
||||
values={{count: boardsCount === 0 ? undefined : String(boardsCount)}}
|
||||
/>
|
||||
);
|
||||
|
||||
const screen = (
|
||||
<ul>
|
||||
<li>
|
||||
{formatMessage({
|
||||
id: 'pluggable_rhs.tourtip.boards.access',
|
||||
defaultMessage: 'Access your linked boards from the Boards icon on the right hand App bar.',
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{formatMessage({
|
||||
id: 'pluggable_rhs.tourtip.boards.click',
|
||||
defaultMessage: 'Click into boards from this right panel.',
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{formatMessage({
|
||||
id: 'pluggable_rhs.tourtip.boards.review',
|
||||
defaultMessage: 'Review boards updates from your channels.',
|
||||
})}
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<OnboardingWorkTemplateTourTip
|
||||
pulsatingDotPlacement={'left'}
|
||||
pulsatingDotTranslate={{x: 10, y: -350}}
|
||||
title={title}
|
||||
screen={screen}
|
||||
overlayPunchOut={overlayPunchOut}
|
||||
singleTip={playbooksCount === 0}
|
||||
placement='left-start'
|
||||
showOptOut={false}
|
||||
interactivePunchOut={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export * from './boards_tour_tip';
|
||||
export * from './playbooks_tour_tip';
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {useFollowElementDimensions, useMeasurePunchouts} from '@mattermost/components';
|
||||
|
||||
import {useShowTourTip} from './useShowTourTip';
|
||||
import OnboardingWorkTemplateTourTip from './worktemplate_explore_tour_tip';
|
||||
|
||||
export const PlaybooksTourTip = (): JSX.Element | null => {
|
||||
const {formatMessage} = useIntl();
|
||||
const {playbooksCount, boardsCount, showPlaybooksTour} = useShowTourTip();
|
||||
const dimensions = useFollowElementDimensions('sidebar-right');
|
||||
const overlayPunchOut = useMeasurePunchouts(['sidebar-right'], [dimensions?.width]);
|
||||
|
||||
if (!showPlaybooksTour) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='pluggable_rhs.tourtip.playbooks.title'
|
||||
defaultMessage={'Access your {count} linked {count, plural, one {playbook} other {playbooks}}!'}
|
||||
values={{count: playbooksCount === 0 ? undefined : String(playbooksCount)}}
|
||||
/>
|
||||
);
|
||||
|
||||
const screen = (
|
||||
<ul>
|
||||
<li>
|
||||
{formatMessage({
|
||||
id: 'pluggable_rhs.tourtip.playbooks.access',
|
||||
defaultMessage: 'Access your linked playbooks from the Playbooks icon on the right hand App bar.',
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{formatMessage({
|
||||
id: 'pluggable_rhs.tourtip.playbooks.click',
|
||||
defaultMessage: 'Click into playbooks from this right panel.',
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{formatMessage({
|
||||
id: 'pluggable_rhs.tourtip.playbooks.review',
|
||||
defaultMessage: 'Review playbook updates from your channels.',
|
||||
})}
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<OnboardingWorkTemplateTourTip
|
||||
pulsatingDotPlacement={'left'}
|
||||
pulsatingDotTranslate={{x: 10, y: -140}}
|
||||
title={title}
|
||||
screen={screen}
|
||||
singleTip={boardsCount === 0}
|
||||
overlayPunchOut={overlayPunchOut}
|
||||
placement='left-start'
|
||||
showOptOut={false}
|
||||
interactivePunchOut={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getCurrentChannelId, getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
|
||||
import {getConfig, getWorkTemplatesLinkedProducts} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getInt} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {getActiveRhsComponent} from 'selectors/rhs';
|
||||
import {suitePluginIds} from 'utils/constants';
|
||||
import {TutorialTourName, WorkTemplateTourSteps} from '../constant';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
export const useShowTourTip = () => {
|
||||
const activeRhsComponent = useSelector(getActiveRhsComponent);
|
||||
const pluginId = activeRhsComponent?.pluginId || '';
|
||||
|
||||
const currentChannelId = useSelector(getCurrentChannelId);
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
|
||||
const enableTutorial = useSelector(getConfig).EnableTutorial === 'true';
|
||||
|
||||
const tutorialStep = useSelector((state: GlobalState) => getInt(state, TutorialTourName.WORK_TEMPLATE_TUTORIAL, currentUserId, 0));
|
||||
|
||||
const workTemplateTourTipShown = tutorialStep === WorkTemplateTourSteps.FINISHED;
|
||||
|
||||
const channelLinkedItems = useSelector(getWorkTemplatesLinkedProducts);
|
||||
|
||||
const boardsCount = channelLinkedItems?.boards || 0;
|
||||
const playbooksCount = channelLinkedItems?.playbooks || 0;
|
||||
const channelId = channelLinkedItems?.channelId || null;
|
||||
|
||||
const showProductTour = channelId && channelId === currentChannelId && !workTemplateTourTipShown && enableTutorial;
|
||||
|
||||
const showBoardsTour = showProductTour && pluginId === suitePluginIds.boards && boardsCount > 0;
|
||||
const showPlaybooksTour = showProductTour && pluginId === suitePluginIds.playbooks && playbooksCount > 0;
|
||||
|
||||
return {
|
||||
showBoardsTour,
|
||||
showPlaybooksTour,
|
||||
boardsCount,
|
||||
playbooksCount,
|
||||
showProductTour,
|
||||
};
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {ChannelsTourTip, ChannelsTourTipProps, TutorialTourName} from 'components/tours';
|
||||
|
||||
const OnboardingWorkTemplateTourTip = (props: Omit<ChannelsTourTipProps, 'tourCategory'>) => {
|
||||
return (
|
||||
<ChannelsTourTip
|
||||
{...props}
|
||||
tourCategory={TutorialTourName.WORK_TEMPLATE_TUTORIAL}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingWorkTemplateTourTip;
|
||||
@@ -1,186 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import PublicPrivateSelector from 'components/widgets/public-private-selector/public-private-selector';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import Constants, {TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
import {isEnterpriseOrE20License} from 'utils/license_utils';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {Visibility, WorkTemplate} from '@mattermost/types/work_templates';
|
||||
import {ChannelType} from '@mattermost/types/channels';
|
||||
|
||||
export interface CustomizeProps {
|
||||
className?: string;
|
||||
name: string;
|
||||
visibility: Visibility;
|
||||
template: WorkTemplate;
|
||||
|
||||
onNameChanged: (name: string) => void;
|
||||
onVisibilityChanged: (visibility: Visibility) => void;
|
||||
}
|
||||
|
||||
const Customize = ({
|
||||
name,
|
||||
visibility,
|
||||
template,
|
||||
onNameChanged,
|
||||
onVisibilityChanged,
|
||||
...props
|
||||
}: CustomizeProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const license = useSelector(getLicense);
|
||||
const licenseIsEnterprise = isEnterpriseOrE20License(license);
|
||||
const templateHasChannels = template.content.findIndex((item) => item.channel) !== -1;
|
||||
const templateHasBoards = template.content.findIndex((item) => item.board) !== -1;
|
||||
const templateHasPlaybooks = template.content.findIndex((item) => item.playbook) !== -1;
|
||||
const canCreatePublicChannel = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.CREATE_PUBLIC_CHANNEL));
|
||||
const canCreatePrivateChannel = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.CREATE_PRIVATE_CHANNEL));
|
||||
const canCreatePublicPlaybook = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.PLAYBOOK_PUBLIC_CREATE));
|
||||
const canCreatePrivatePlaybook = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.PLAYBOOK_PRIVATE_CREATE));
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'pageview_customize');
|
||||
}, []);
|
||||
|
||||
const privacySelectorValue = (visibility === Visibility.Public ? Constants.OPEN_CHANNEL : Constants.PRIVATE_CHANNEL) as ChannelType;
|
||||
const onPrivacySelectorChanged = (value: ChannelType) => {
|
||||
onVisibilityChanged(value === Constants.PRIVATE_CHANNEL ? Visibility.Private : Visibility.Public);
|
||||
};
|
||||
|
||||
let privateButtonProps = {};
|
||||
let publicButtonProps = {};
|
||||
if (templateHasPlaybooks) {
|
||||
if (!canCreatePublicPlaybook) {
|
||||
publicButtonProps = {
|
||||
tooltip: formatMessage({id: 'work_templates.customize.public_playbook_permission_issue', defaultMessage: 'You do not have permission to create public playbooks.'}),
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
if (!canCreatePrivatePlaybook) {
|
||||
privateButtonProps = {
|
||||
tooltip: formatMessage({id: 'work_templates.customize.private_playbook_permission_issue', defaultMessage: 'You do not have permission to create private playbooks.'}),
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (templateHasChannels) {
|
||||
if (!canCreatePublicChannel) {
|
||||
publicButtonProps = {
|
||||
tooltip: formatMessage({id: 'work_templates.customize.public_channel_permission_issue', defaultMessage: 'You do not have permission to create public channels.'}),
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
if (!canCreatePrivateChannel) {
|
||||
privateButtonProps = {
|
||||
tooltip: formatMessage({id: 'work_templates.customize.private_channel_permission_issue', defaultMessage: 'You do not have permission to create private channels.'}),
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// leave this rule last as it has priority
|
||||
if (templateHasPlaybooks && !licenseIsEnterprise) {
|
||||
privateButtonProps = {
|
||||
tooltip: formatMessage({id: 'work_templates.customize.private_playbook_license_issue', defaultMessage: 'Private playbooks requires an Enterprise license.'}),
|
||||
locked: true,
|
||||
};
|
||||
}
|
||||
|
||||
let nameFieldLabel;
|
||||
if (templateHasChannels && templateHasBoards && templateHasPlaybooks) {
|
||||
nameFieldLabel = formatMessage({id: 'work_templates.customize.name_label_all', defaultMessage: 'Name your channel, board, and playbook'});
|
||||
} else if (templateHasChannels && templateHasBoards) {
|
||||
nameFieldLabel = formatMessage({id: 'work_templates.customize.name_label_channels_boards', defaultMessage: 'Name your channel and board'});
|
||||
} else if (templateHasChannels && templateHasPlaybooks) {
|
||||
nameFieldLabel = formatMessage({id: 'work_templates.customize.name_label_channels_playbooks', defaultMessage: 'Name your channel and playbook'});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<div className='name-section-container'>
|
||||
<p>
|
||||
<strong>
|
||||
{nameFieldLabel}
|
||||
</strong>
|
||||
</p>
|
||||
<p className='customize-name-text'>
|
||||
{formatMessage({id: 'work_templates.customize.name_description', defaultMessage: 'This will help you and others find your project items. You can always edit this later.'})}
|
||||
</p>
|
||||
<input
|
||||
type='text'
|
||||
autoFocus={true}
|
||||
placeholder={formatMessage({id: 'work_templates.customize.name_input_placeholder', defaultMessage: 'e.g. Web app, Growth, Customer Journey etc.'})}
|
||||
value={name}
|
||||
onChange={(e) => onNameChanged(e.target.value)}
|
||||
maxLength={Constants.MAX_CHANNELNAME_LENGTH}
|
||||
/>
|
||||
</div>
|
||||
<div className='visibility-section-container'>
|
||||
<p>
|
||||
<strong>
|
||||
{formatMessage({id: 'work_templates.customize.visibility_title', defaultMessage: 'Who should have access to this?'})}
|
||||
</strong>
|
||||
</p>
|
||||
<PublicPrivateSelector
|
||||
selected={privacySelectorValue}
|
||||
onChange={onPrivacySelectorChanged}
|
||||
privateButtonProps={privateButtonProps}
|
||||
publicButtonProps={publicButtonProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledCustomized = styled(Customize)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 509px;
|
||||
margin: 0 auto;
|
||||
|
||||
.public-private-selector .public-private-selector-button.locked {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--center-channel-text);
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(var(--center-channel-text-rgb), 0.16);
|
||||
&:focus {
|
||||
border: 1px solid var(--button-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.name-section-container {
|
||||
margin-top: 33px;
|
||||
}
|
||||
|
||||
.visibility-section-container {
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
.customize-name-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledCustomized;
|
||||
@@ -1,131 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {useIntl} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {Category, WorkTemplate} from '@mattermost/types/work_templates';
|
||||
import {TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
|
||||
import UseCaseMenuItem from './menu/use_case';
|
||||
|
||||
const Categories = styled.div`
|
||||
h2 {
|
||||
margin: 0;
|
||||
padding: 8px 16px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
width: 176px;
|
||||
}
|
||||
`;
|
||||
|
||||
const CategoryButton = styled.button`
|
||||
width: 155px;
|
||||
padding: 10px 16px;
|
||||
border: 0;
|
||||
background: none;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--denim-button-bg-rgb), 0.04);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: rgba(var(--denim-button-bg-rgb), 0.04);
|
||||
font-weight: 600;
|
||||
color: var(--denim-button-bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const UseCases = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
width: 692px;
|
||||
`;
|
||||
|
||||
interface MenuProps {
|
||||
className?: string;
|
||||
onTemplateSelected: (template: WorkTemplate, quickUse: boolean) => void;
|
||||
categories: Category[];
|
||||
changeCategory: (category: Category) => void;
|
||||
workTemplates: Record<string, WorkTemplate[]>;
|
||||
currentCategoryId: string;
|
||||
disableQuickUse: boolean;
|
||||
}
|
||||
|
||||
const Menu = ({className, disableQuickUse, categories, workTemplates, currentCategoryId, changeCategory, onTemplateSelected}: MenuProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'pageview_menu');
|
||||
}, []);
|
||||
|
||||
const quickUse = (template: WorkTemplate) => {
|
||||
onTemplateSelected(template, true);
|
||||
};
|
||||
|
||||
const selectTemplate = (template: WorkTemplate) => {
|
||||
onTemplateSelected(template, false);
|
||||
};
|
||||
|
||||
if (!categories.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Categories>
|
||||
<h2>
|
||||
{formatMessage({id: 'work_templates.menu.template_title', defaultMessage: 'TEMPLATE'})}
|
||||
</h2>
|
||||
<ul>
|
||||
{categories.map((category) => (
|
||||
<li key={category.id}>
|
||||
<CategoryButton
|
||||
onClick={() => changeCategory(category)}
|
||||
className={classNames({selected: category.id === currentCategoryId})}
|
||||
>
|
||||
{category.name}
|
||||
</CategoryButton>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Categories>
|
||||
<UseCases>
|
||||
{workTemplates[currentCategoryId]?.map((workTemplate) => (
|
||||
<UseCaseMenuItem
|
||||
disableQuickUse={disableQuickUse}
|
||||
key={workTemplate.id}
|
||||
name={workTemplate.useCase}
|
||||
illustration={workTemplate.illustration}
|
||||
channelsCount={workTemplate.content.filter((c) => c.channel).length}
|
||||
boardsCount={workTemplate.content.filter((c) => c.board).length}
|
||||
playbooksCount={workTemplate.content.filter((c) => c.playbook).length}
|
||||
onQuickUse={() => quickUse(workTemplate)}
|
||||
onSelectTemplate={() => selectTemplate(workTemplate)}
|
||||
/>
|
||||
))}
|
||||
</UseCases>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export default StyledMenu;
|
||||
@@ -1,192 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const QuickUse = styled.button`
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
|
||||
text-align: center;
|
||||
padding: 4px 10px;
|
||||
border: 0px;
|
||||
|
||||
background: var(--denim-button-bg);
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
color: var(--button-color);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
|
||||
transition: visibility 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
||||
`;
|
||||
|
||||
interface UseCaseProps {
|
||||
className?: string;
|
||||
name: string;
|
||||
illustration: string;
|
||||
channelsCount: number;
|
||||
boardsCount: number;
|
||||
playbooksCount: number;
|
||||
disableQuickUse: boolean;
|
||||
|
||||
onQuickUse: () => void;
|
||||
onSelectTemplate: () => void;
|
||||
}
|
||||
|
||||
const UseCase = (props: UseCaseProps) => {
|
||||
const {formatMessage, formatList} = useIntl();
|
||||
|
||||
const details = useMemo(() => {
|
||||
const detailBuilder: string[] = [];
|
||||
if (props.channelsCount) {
|
||||
detailBuilder.push(formatMessage({
|
||||
id: 'work_templates.menu.usecase_channels_count',
|
||||
defaultMessage: '{channelsCount, plural, =1 {# channel} other {# channels}}',
|
||||
}, {channelsCount: props.channelsCount}));
|
||||
}
|
||||
|
||||
if (props.boardsCount) {
|
||||
detailBuilder.push(formatMessage({
|
||||
id: 'work_templates.menu.usecase_boards_count',
|
||||
defaultMessage: '{boardsCount, plural, =1 {# board} other {# boards}}',
|
||||
}, {boardsCount: props.boardsCount}));
|
||||
}
|
||||
|
||||
if (props.playbooksCount) {
|
||||
detailBuilder.push(formatMessage({
|
||||
id: 'work_templates.menu.usecase_playbooks_count',
|
||||
defaultMessage: '{playbooksCount, plural, =1 {# playbook} other {# playbooks}}',
|
||||
}, {playbooksCount: props.playbooksCount}));
|
||||
}
|
||||
|
||||
return formatList(detailBuilder, {style: 'narrow'});
|
||||
}, [props.channelsCount, props.boardsCount, props.playbooksCount]);
|
||||
|
||||
const selectTemplate = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
props.onSelectTemplate();
|
||||
};
|
||||
|
||||
const quickUse = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
props.onQuickUse();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={props.className}
|
||||
onClick={selectTemplate}
|
||||
>
|
||||
<div className='illustration'>
|
||||
<QuickUse
|
||||
onClick={quickUse}
|
||||
disabled={props.disableQuickUse}
|
||||
>{formatMessage({id: 'work_templates.menu.quick_use', defaultMessage: 'Quick use'})}</QuickUse>
|
||||
<img src={props.illustration}/>
|
||||
</div>
|
||||
<div className='name'>
|
||||
{props.name}
|
||||
<p className='details'>
|
||||
{details}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledUseCaseMenuItem = styled(UseCase)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 220px;
|
||||
border: 1px solid rgba(var(--center-channel-text-rgb), 0.16);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 16px;
|
||||
margin-right: 10px;
|
||||
|
||||
.illustration {
|
||||
height: 130px;
|
||||
background: rgba(73, 146, 243, 0.2);
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
transition: height 0.2s ease-in-out;
|
||||
|
||||
img {
|
||||
width: 204px;
|
||||
height: 123px;
|
||||
z-index: 1;
|
||||
transition: margin 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
padding: 14px 12px;
|
||||
width: 220px;
|
||||
height: 44px;
|
||||
font-family: 'Open Sans';
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: var(--center-channel-color);
|
||||
transition: height 0.2s ease-in-out;
|
||||
flex-grow: 2;
|
||||
|
||||
.details {
|
||||
visibility: hidden;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.02em;
|
||||
color: rgba(var(--center-channel-text-rgb), 0.72);
|
||||
transition: visibility 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--elevation-2);
|
||||
${QuickUse} {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-bottom: -12px;
|
||||
}
|
||||
|
||||
.name {
|
||||
height: 56px;
|
||||
padding: 12px 12px 0;
|
||||
.details {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.illustration {
|
||||
height: 118px;
|
||||
}
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledUseCaseMenuItem;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
|
||||
const Modal = styled(GenericModal)`
|
||||
width: 960px;
|
||||
|
||||
.modal-body {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
&.work-template-modal--customize, &.work-template-modal--preview {
|
||||
.modal-body {
|
||||
background: rgba(var(--denim-button-bg-rgb), 0.04);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Modal;
|
||||
@@ -1,364 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import {CSSTransition} from 'react-transition-group';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {AccordionItemType} from 'components/common/accordion/accordion';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
import {Board, Channel, Integration, Playbook, WorkTemplate} from '@mattermost/types/work_templates';
|
||||
import {MarketplacePlugin} from '@mattermost/types/marketplace';
|
||||
|
||||
import {getTemplateDefaultIllustration} from '../utils';
|
||||
|
||||
import Accordion from './preview/accordion';
|
||||
import Chip from './preview/chip';
|
||||
import PreviewSection from './preview/section';
|
||||
|
||||
export interface PreviewProps {
|
||||
className?: string;
|
||||
template: WorkTemplate;
|
||||
pluginsEnabled: boolean;
|
||||
}
|
||||
|
||||
interface IllustrationAnimations {
|
||||
prior: {
|
||||
animateIn: boolean;
|
||||
illustration: string;
|
||||
};
|
||||
current: {
|
||||
animateIn: boolean;
|
||||
illustration: string;
|
||||
};
|
||||
}
|
||||
|
||||
const ANIMATE_TIMEOUTS = {
|
||||
appear: 0,
|
||||
enter: 200,
|
||||
exit: 200,
|
||||
};
|
||||
|
||||
const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const nodeRefForPrior = useRef(null);
|
||||
const nodeRefForCurrent = useRef(null);
|
||||
|
||||
const [integrations, setIntegrations] = useState<Integration[]>();
|
||||
|
||||
const marketplacePlugins: MarketplacePlugin[] = useSelector((state: GlobalState) => state.views.marketplace.plugins);
|
||||
const loadedPlugins = useSelector((state: GlobalState) => state.plugins.plugins);
|
||||
|
||||
const [illustrationDetails, setIllustrationDetails] = useState<IllustrationAnimations>(() => {
|
||||
const defaultIllustration = getTemplateDefaultIllustration(template);
|
||||
return {
|
||||
prior: {
|
||||
animateIn: false,
|
||||
illustration: defaultIllustration,
|
||||
},
|
||||
current: {
|
||||
animateIn: true,
|
||||
illustration: defaultIllustration,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'pageview_preview');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (illustrationDetails.prior.animateIn) {
|
||||
setIllustrationDetails((prevState: IllustrationAnimations) => ({
|
||||
prior: {
|
||||
...prevState.prior,
|
||||
animateIn: false,
|
||||
},
|
||||
current: {
|
||||
...prevState.current,
|
||||
animateIn: true,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [illustrationDetails.prior.animateIn]);
|
||||
|
||||
const handleIllustrationUpdate = (illustration: string) => {
|
||||
// don't refresh if this is the same illustration
|
||||
if (illustrationDetails.current.illustration === illustration) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIllustrationDetails({
|
||||
prior: {...illustrationDetails.current},
|
||||
current: {
|
||||
animateIn: false,
|
||||
illustration,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const [channels, boards, playbooks, availableIntegrations] = useMemo(() => {
|
||||
const channels: Channel[] = [];
|
||||
const boards: Board[] = [];
|
||||
const playbooks: Playbook[] = [];
|
||||
const availableIntegrations: Integration[] = [];
|
||||
template.content.forEach((c) => {
|
||||
if (c.channel) {
|
||||
channels.push(c.channel);
|
||||
}
|
||||
if (c.board) {
|
||||
boards.push(c.board);
|
||||
}
|
||||
if (c.playbook) {
|
||||
playbooks.push(c.playbook);
|
||||
}
|
||||
if (c.integration && c.integration.recommended) {
|
||||
availableIntegrations.push(c.integration);
|
||||
}
|
||||
});
|
||||
return [channels, boards, playbooks, availableIntegrations];
|
||||
}, [template.content]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pluginsEnabled) {
|
||||
return;
|
||||
}
|
||||
const intg =
|
||||
availableIntegrations?.
|
||||
flatMap((integration) => {
|
||||
return marketplacePlugins.reduce((acc: Integration[], curr) => {
|
||||
if (curr.manifest.id === integration.id) {
|
||||
const installed = Boolean(loadedPlugins[integration.id]);
|
||||
acc.push({
|
||||
...integration,
|
||||
name: curr.manifest.name,
|
||||
icon: curr.icon_data,
|
||||
installed,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
return acc;
|
||||
}, [] as Integration[]);
|
||||
}).sort((first: Integration) => {
|
||||
return first.installed ? -1 : 1;
|
||||
});
|
||||
if (intg?.length) {
|
||||
setIntegrations(intg);
|
||||
}
|
||||
}, [marketplacePlugins, availableIntegrations, loadedPlugins, pluginsEnabled]);
|
||||
|
||||
// building accordion items
|
||||
const accordionItemsData: AccordionItemType[] = [];
|
||||
if (channels.length > 0) {
|
||||
accordionItemsData.push({
|
||||
id: 'channels',
|
||||
icon: <i className='icon-product-channels'/>,
|
||||
title: formatMessage({id: 'work_templates.preview.accordion_title_channels', defaultMessage: 'Channels'}),
|
||||
extraContent: <Chip>{channels.length}</Chip>,
|
||||
items: [(
|
||||
<PreviewSection
|
||||
key={'channels'}
|
||||
id={'channels'}
|
||||
message={template.description.channel.message}
|
||||
items={channels}
|
||||
onUpdateIllustration={(illustration) => handleIllustrationUpdate(illustration)}
|
||||
/>
|
||||
)],
|
||||
});
|
||||
}
|
||||
if (boards.length > 0) {
|
||||
accordionItemsData.push({
|
||||
id: 'boards',
|
||||
icon: <i className='icon-product-boards'/>,
|
||||
title: formatMessage({id: 'work_templates.preview.accordion_title_boards', defaultMessage: 'Boards'}),
|
||||
extraContent: <Chip>{boards.length}</Chip>,
|
||||
items: [(
|
||||
<PreviewSection
|
||||
key={'boards'}
|
||||
id={'boards'}
|
||||
message={template.description.board.message}
|
||||
items={boards}
|
||||
onUpdateIllustration={(illustration) => handleIllustrationUpdate(illustration)}
|
||||
/>
|
||||
)],
|
||||
});
|
||||
}
|
||||
if (playbooks.length > 0) {
|
||||
accordionItemsData.push({
|
||||
id: 'playbooks',
|
||||
icon: <i className='icon-product-playbooks'/>,
|
||||
title: formatMessage({id: 'work_templates.preview.accordion_title_playbooks', defaultMessage: 'Playbooks'}),
|
||||
extraContent: <Chip>{playbooks.length}</Chip>,
|
||||
items: [(
|
||||
<PreviewSection
|
||||
key={'playbooks'}
|
||||
id={'playbooks'}
|
||||
message={template.description.playbook.message}
|
||||
items={playbooks}
|
||||
onUpdateIllustration={(illustration) => handleIllustrationUpdate(illustration)}
|
||||
/>
|
||||
)],
|
||||
});
|
||||
}
|
||||
if (pluginsEnabled && integrations?.length) {
|
||||
accordionItemsData.push({
|
||||
id: 'integrations',
|
||||
icon: <i className='icon-power-plug-outline'/>,
|
||||
title: 'Integrations',
|
||||
extraContent: <Chip>{integrations.length}</Chip>,
|
||||
items: [(
|
||||
<PreviewSection
|
||||
key={'integrations'}
|
||||
id={'integrations'}
|
||||
message={template.description.integration.message}
|
||||
items={integrations}
|
||||
categoryId={template.category}
|
||||
/>
|
||||
)],
|
||||
});
|
||||
}
|
||||
|
||||
// When opening an accordion section, change the illustration to whatever has been open
|
||||
const handleItemOpened = (index: number) => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'expand_preview_section', {section: accordionItemsData[index].id, category: template.category, template: template.id});
|
||||
const item = accordionItemsData[index];
|
||||
const newPrior = {
|
||||
...illustrationDetails.current,
|
||||
animateIn: true,
|
||||
};
|
||||
const newCurrent: IllustrationAnimations['current'] = {
|
||||
animateIn: false,
|
||||
illustration: '',
|
||||
};
|
||||
switch (item.id) {
|
||||
case 'channels':
|
||||
newCurrent.illustration = channels[0].illustration;
|
||||
break;
|
||||
case 'boards':
|
||||
newCurrent.illustration = boards[0].illustration;
|
||||
break;
|
||||
case 'playbooks':
|
||||
newCurrent.illustration = playbooks[0].illustration;
|
||||
break;
|
||||
case 'integrations':
|
||||
newCurrent.illustration = template.description.integration.illustration;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
setIllustrationDetails({
|
||||
prior: newPrior,
|
||||
current: newCurrent,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='content-side'>
|
||||
<strong>{formatMessage({id: 'work_templates.preview.what_you_get', defaultMessage: 'Here\'s what you\'ll get:'})}</strong>
|
||||
<Accordion
|
||||
accordionItemsData={accordionItemsData}
|
||||
openFirstElement={true}
|
||||
onItemOpened={handleItemOpened}
|
||||
/>
|
||||
</div>
|
||||
<div className='img-wrapper'>
|
||||
<CSSTransition
|
||||
nodeRef={nodeRefForPrior}
|
||||
in={illustrationDetails.prior.animateIn}
|
||||
timeout={ANIMATE_TIMEOUTS}
|
||||
classNames='prior-illustration'
|
||||
>
|
||||
<img
|
||||
ref={nodeRefForPrior}
|
||||
src={illustrationDetails.prior.illustration}
|
||||
/>
|
||||
</CSSTransition>
|
||||
<CSSTransition
|
||||
nodeRef={nodeRefForCurrent}
|
||||
in={illustrationDetails.current.animateIn}
|
||||
timeout={ANIMATE_TIMEOUTS}
|
||||
classNames='current-illustration'
|
||||
>
|
||||
<img
|
||||
ref={nodeRefForCurrent}
|
||||
src={illustrationDetails.current.illustration}
|
||||
/>
|
||||
</CSSTransition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledPreview = styled(Preview)`
|
||||
display: flex;
|
||||
|
||||
.content-side {
|
||||
min-width: 387px;
|
||||
width: 387px;
|
||||
height: 416px;
|
||||
padding-right: 32px;
|
||||
margin-top: 17px;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
font-family: 'Metropolis';
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
color: var(--center-channel-text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.img-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
img {
|
||||
box-shadow: var(--elevation-2);
|
||||
border-radius: 8px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.prior-illustration-enter,
|
||||
.prior-illustration-enter-done,
|
||||
.prior-illustration-exit-done {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.prior-illustration-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.prior-illustration-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
}
|
||||
|
||||
.current-illustration-enter,
|
||||
.current-illustration-exit,
|
||||
.current-illustration-exit-done {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.current-illustration-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
}
|
||||
|
||||
.current-illustration-enter-done {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledPreview;
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import LibAccordion from 'components/common/accordion/accordion';
|
||||
|
||||
import Chip from './chip';
|
||||
|
||||
const Accordion = styled(LibAccordion)`
|
||||
&.Accordion {
|
||||
.accordion-card {
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
color: var(--center-channel-color);
|
||||
|
||||
.accordion-card-header {
|
||||
padding: 14.5px 0px 14.5px 16px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--center-channel-color);
|
||||
align-items: center;
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
&__extraContent {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
max-width: initial;
|
||||
font-size: 18px;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-card-container__content {
|
||||
padding: 4px 16px 16px 16px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--denim-button-bg);
|
||||
|
||||
.accordion-card-header {
|
||||
color: var(--denim-button-bg);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
${Chip} {
|
||||
background: rgba(var(--denim-button-bg-rgb), 0.08);
|
||||
color: var(--denim-button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Accordion;
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Chip = styled.span`
|
||||
padding: 0px 4px;
|
||||
width: 20px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.02em;
|
||||
|
||||
background: rgba(var(--center-channel-text-rgb), 0.08);
|
||||
color: rgba(var(--center-channel-text-rgb), 0.56);
|
||||
`;
|
||||
|
||||
export default Chip;
|
||||
@@ -1,310 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode, useCallback, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import classnames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
|
||||
import store from 'stores/redux_store';
|
||||
import NotifyAdminCTA from 'components/notify_admin_cta/notify_admin_cta';
|
||||
|
||||
import {MattermostFeatures} from 'utils/constants';
|
||||
|
||||
interface GenericPreviewSectionProps {
|
||||
items: Array<{ id: string; name?: string; illustration?: string }>;
|
||||
onUpdateIllustration?: (illustration: string) => void;
|
||||
className?: string;
|
||||
message?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
type IntegrationPreviewSectionItemsProps = {id: string; name?: string; category?: string; description?: string; icon?: string; installed?: boolean}
|
||||
|
||||
interface IntegrationPreviewSectionProps {
|
||||
items: IntegrationPreviewSectionItemsProps[];
|
||||
className?: string;
|
||||
message?: string;
|
||||
id?: string;
|
||||
categoryId?: string;
|
||||
}
|
||||
|
||||
const SYSCONSOLE_WRITE_PLUGINS = 'sysconsole_write_plugins';
|
||||
const getState = store.getState;
|
||||
|
||||
const viewPreview = (props: GenericPreviewSectionProps | IntegrationPreviewSectionProps) => {
|
||||
if (props.id === 'integrations') {
|
||||
const integrationsProps = props as IntegrationPreviewSectionProps;
|
||||
return (
|
||||
<IntegrationsPreview
|
||||
categoryId={integrationsProps.categoryId}
|
||||
items={integrationsProps.items}
|
||||
/>);
|
||||
}
|
||||
const genericProps = props as GenericPreviewSectionProps;
|
||||
return (
|
||||
<GenericPreview
|
||||
items={props.items}
|
||||
onUpdateIllustration={genericProps.onUpdateIllustration}
|
||||
/>);
|
||||
};
|
||||
|
||||
const PreviewSection = (props: GenericPreviewSectionProps | IntegrationPreviewSectionProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<p>
|
||||
{props.message}
|
||||
</p>
|
||||
<span className='included-title'>
|
||||
{
|
||||
formatMessage({
|
||||
id: 'work_templates.preview.section.included',
|
||||
defaultMessage: 'Included',
|
||||
})
|
||||
}
|
||||
</span>
|
||||
{viewPreview(props)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const IntegrationsPreview = ({items, categoryId}: IntegrationPreviewSectionProps) => {
|
||||
const state = getState();
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const haveIWritePluginPermission = haveISystemPermission(state, {permission: SYSCONSOLE_WRITE_PLUGINS});
|
||||
const [pluginInstallationPossible, setPluginInstallationPossible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (haveIWritePluginPermission) {
|
||||
setPluginInstallationPossible(true);
|
||||
}
|
||||
}, [haveIWritePluginPermission]);
|
||||
|
||||
const pluginsToInstall = items.filter((item) => !item.installed);
|
||||
|
||||
const createWarningMessage = () => {
|
||||
if (pluginsToInstall.length === 1) {
|
||||
return formatMessage(
|
||||
{
|
||||
id: 'work_templates.preview.integrations.admin_install.single_plugin',
|
||||
defaultMessage: '{plugin} will not be added until admin installs it.',
|
||||
},
|
||||
{
|
||||
plugin: pluginsToInstall[0].name,
|
||||
});
|
||||
} else if (pluginsToInstall.length > 1) {
|
||||
return formatMessage({
|
||||
id: 'work_templates.preview.integrations.admin_install.multiple_plugin',
|
||||
defaultMessage: 'Integrations will not be added until admin installs them.',
|
||||
});
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const warningMessage = pluginInstallationPossible ? '' : createWarningMessage();
|
||||
const notifyAdminCTA = formatMessage({
|
||||
id: 'work_templates.preview.integrations.admin_install.notify',
|
||||
defaultMessage: 'Notify admin to install integrations.',
|
||||
});
|
||||
|
||||
const makeIntegrationSubtext = useCallback((integration: IntegrationPreviewSectionItemsProps) => {
|
||||
if (integration.installed) {
|
||||
return formatMessage({
|
||||
id: 'work_templates.preview.integrations.already_installed',
|
||||
defaultMessage: 'Already installed',
|
||||
});
|
||||
}
|
||||
|
||||
if (!pluginInstallationPossible) {
|
||||
return formatMessage({
|
||||
id: 'work_templates.preview.integrations.app_install',
|
||||
defaultMessage: 'App Install',
|
||||
});
|
||||
}
|
||||
|
||||
return formatMessage({
|
||||
id: 'work_templates.preview.integrations.to_be_installed',
|
||||
defaultMessage: 'To be installed',
|
||||
});
|
||||
}, [pluginInstallationPossible, formatMessage]);
|
||||
|
||||
return (
|
||||
<div className='preview-integrations'>
|
||||
<div className='preview-integrations-plugins'>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={classnames('preview-integrations-plugins-item', {'preview-integrations-plugins-item__readonly': !item.installed && !pluginInstallationPossible})}
|
||||
>
|
||||
<div className='preview-integrations-plugins-item__illustration'>
|
||||
<img src={item.icon}/>
|
||||
</div>
|
||||
<div className='preview-integrations-plugins-item__name'>
|
||||
{item.name}<br/>
|
||||
<span className='preview-integrations-plugins-item__name-sub'>
|
||||
{makeIntegrationSubtext(item)}
|
||||
</span>
|
||||
</div>
|
||||
{item.installed &&
|
||||
<div className='preview-integrations-plugins-item__icon icon-check-circle preview-integrations-plugins-item__icon_blue'/>}
|
||||
</div>);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{warningMessage &&
|
||||
<>
|
||||
<div className='preview-integrations-warning'>
|
||||
<div className='icon-alert-outline'/>
|
||||
<div className='preview-integrations-warning-message'> {warningMessage} </div>
|
||||
</div>
|
||||
|
||||
<NotifyAdminCTA
|
||||
callerInfo={`${MattermostFeatures.PLUGIN_FEATURE}-${categoryId}`}
|
||||
ctaText={notifyAdminCTA}
|
||||
notifyRequestData={{
|
||||
required_plan: pluginsToInstall.map((plugin) => plugin.id).join(','),
|
||||
required_feature: `${MattermostFeatures.PLUGIN_FEATURE}-${categoryId}`,
|
||||
trial_notification: false,
|
||||
}}
|
||||
/>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
const GenericPreview = ({items, onUpdateIllustration}: GenericPreviewSectionProps) => {
|
||||
const updateIllustration = (e: React.MouseEvent<HTMLAnchorElement>, illustration: string) => {
|
||||
e.preventDefault();
|
||||
onUpdateIllustration?.(illustration);
|
||||
};
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let list: ReactNode = (<li key={items[0].id}>{items[0].name}</li>);
|
||||
if (items.length > 1) {
|
||||
list = items.map((c) => (
|
||||
<li key={c.id}>
|
||||
<a
|
||||
href='#'
|
||||
onClick={(e) => updateIllustration(e, c.illustration || '')}
|
||||
>
|
||||
{c.name}
|
||||
</a>
|
||||
</li>
|
||||
));
|
||||
}
|
||||
|
||||
return (<ul>{list}</ul>);
|
||||
};
|
||||
|
||||
const StyledPreviewSection = styled(PreviewSection)`
|
||||
.included-title {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.preview-integrations {
|
||||
#notify_admin_cta {
|
||||
padding: 0 2px;
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
line-height: 10px;
|
||||
}
|
||||
&-plugins {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
gap: 8px;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 128px;
|
||||
height: 48px;
|
||||
flex-basis: 45%;
|
||||
border: 1px solid rgba(var(--center-channel-text-rgb), 0.24);
|
||||
border-radius: 4px;
|
||||
|
||||
&__readonly {
|
||||
opacity: 65%;
|
||||
}
|
||||
|
||||
&__illustration {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
margin: 12px 10px;
|
||||
|
||||
img {
|
||||
position: relative !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 2;
|
||||
color: var(--center-channel-text);
|
||||
font-family: 'Open Sans';
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 16px;
|
||||
&-sub {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
align-self: flex-start;
|
||||
|
||||
&_blue {
|
||||
color: var(--denim-button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-warning {
|
||||
display: flex;
|
||||
margin: 8px 0px;
|
||||
color: var(--error-text);
|
||||
|
||||
&-message {
|
||||
margin-left: 3px;
|
||||
font-family: 'Open Sans';
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-check-circle::before {
|
||||
margin-top: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.icon-download-outline::before {
|
||||
margin-top: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledPreviewSection;
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {PluginComponent} from 'types/store/plugins';
|
||||
|
||||
type HookReturnType = {
|
||||
pluggableId: string;
|
||||
rhsPluggableIds: Map<string, string>;
|
||||
pluginComponent?: PluginComponent;
|
||||
};
|
||||
|
||||
export const useGetRHSPluggablesIds = (): HookReturnType => {
|
||||
const rhsPlugins = useSelector((state: GlobalState) => state.plugins.components.RightHandSidebarComponent);
|
||||
const pluggableId = useSelector((state: GlobalState) => state.views.rhs.pluggableId);
|
||||
|
||||
const rhsPluggableIds: Map<string, string> = new Map<string, string>();
|
||||
rhsPlugins.forEach((plugin) => rhsPluggableIds.set(plugin.pluginId, plugin.id));
|
||||
|
||||
const pluginComponent = rhsPlugins.find((element: PluginComponent) => element.id === pluggableId);
|
||||
|
||||
return {pluggableId, rhsPluggableIds, pluginComponent};
|
||||
};
|
||||
@@ -1,385 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import classnames from 'classnames';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import LocalizedIcon from 'components/localized_icon';
|
||||
import {TTNameMapToATStatusKey, TutorialTourName} from 'components/tours/constant';
|
||||
|
||||
import {closeModal as closeModalAction} from 'actions/views/modals';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {showRHSPlugin} from 'actions/views/rhs';
|
||||
import {fetchRemoteListing} from 'actions/marketplace';
|
||||
import {loadIfNecessaryAndSwitchToChannelById} from 'actions/views/channel';
|
||||
|
||||
import {
|
||||
clearCategories,
|
||||
clearWorkTemplates,
|
||||
executeWorkTemplate,
|
||||
getWorkTemplateCategories,
|
||||
getWorkTemplates,
|
||||
onExecuteSuccess,
|
||||
} from 'mattermost-redux/actions/work_templates';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
|
||||
import {
|
||||
Category,
|
||||
ExecuteWorkTemplateRequest,
|
||||
ExecuteWorkTemplateResponse,
|
||||
Visibility,
|
||||
WorkTemplate,
|
||||
} from '@mattermost/types/work_templates';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {ModalIdentifiers, suitePluginIds, TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
|
||||
import {AutoTourStatus} from 'components/tours';
|
||||
|
||||
import Customize from './components/customize';
|
||||
import Menu from './components/menu';
|
||||
import GenericModal from './components/modal';
|
||||
import Preview from './components/preview';
|
||||
import {useGetRHSPluggablesIds} from './hooks';
|
||||
import {getContentCount} from './utils';
|
||||
|
||||
const BackIconInHeader = styled(LocalizedIcon)`
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
color: rgba(var(--center-channel-text-rbg), 0.56);
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ModalTitleProps {
|
||||
text: string;
|
||||
backArrowAction?: () => void;
|
||||
}
|
||||
|
||||
const ModalTitle = (props: ModalTitleProps) => {
|
||||
return (
|
||||
<div className='work-template-modal__title'>
|
||||
{props.backArrowAction &&
|
||||
<BackIconInHeader
|
||||
className='icon icon-arrow-left'
|
||||
aria-label={'Back Icon'}
|
||||
onClick={props.backArrowAction}
|
||||
/>
|
||||
}
|
||||
<span style={{marginLeft: 18}}>{props.text}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
enum ModalState {
|
||||
Menu = 'menu',
|
||||
Customize = 'customize',
|
||||
Preview = 'preview',
|
||||
}
|
||||
|
||||
const WorkTemplateModal = () => {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [modalState, setModalState] = useState(ModalState.Menu);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<WorkTemplate | null>(null);
|
||||
const [selectedName, setSelectedName] = useState<string>('');
|
||||
const [selectedVisibility, setSelectedVisibility] = useState(Visibility.Public);
|
||||
const [currentCategoryId, setCurrentCategoryId] = useState('');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [errorText, setErrorText] = useState('');
|
||||
|
||||
const categories = useSelector((state: GlobalState) => state.entities.worktemplates.categories);
|
||||
const workTemplates = useSelector((state: GlobalState) => state.entities.worktemplates.templatesInCategory);
|
||||
const config = useSelector(getConfig);
|
||||
const pluginsEnabled = config.PluginsEnabled === 'true' && config.EnableMarketplace === 'true' && config.IsDefaultMarketplace === 'true';
|
||||
const teamId = useSelector(getCurrentTeamId);
|
||||
const playbookTemplates = useSelector((state: GlobalState) => state.entities.worktemplates.playbookTemplates);
|
||||
const {rhsPluggableIds} = useGetRHSPluggablesIds();
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'open_modal');
|
||||
}, []);
|
||||
|
||||
// load the categories if they are not found, or load the work templates for those categories.
|
||||
useEffect(() => {
|
||||
if (categories?.length) {
|
||||
setCurrentCategoryId(categories[0].id);
|
||||
dispatch(getWorkTemplates(categories[0].id));
|
||||
return;
|
||||
}
|
||||
dispatch(getWorkTemplateCategories());
|
||||
}, [dispatch, categories]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pluginsEnabled) {
|
||||
dispatch(fetchRemoteListing());
|
||||
}
|
||||
}, [dispatch, pluginsEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(clearCategories());
|
||||
dispatch(clearWorkTemplates());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// error resetter
|
||||
useEffect(() => {
|
||||
setErrorText('');
|
||||
}, [currentCategoryId, modalState, selectedTemplate, selectedVisibility, selectedName]);
|
||||
|
||||
const changeCategory = (category: Category) => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'change_category', {category: category.id});
|
||||
setCurrentCategoryId(category.id);
|
||||
if (workTemplates[category.id]?.length) {
|
||||
return;
|
||||
}
|
||||
dispatch(getWorkTemplates(category.id));
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
dispatch(closeModalAction(ModalIdentifiers.WORK_TEMPLATE));
|
||||
};
|
||||
|
||||
const goToMenu = () => {
|
||||
setModalState(ModalState.Menu);
|
||||
setSelectedTemplate(null);
|
||||
};
|
||||
|
||||
const handleTemplateSelected = (template: WorkTemplate, quickUse: boolean) => {
|
||||
setSelectedTemplate(template);
|
||||
if (quickUse) {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'quick_use', {category: template.category, template: template.id});
|
||||
execute(template, '', template.visibility);
|
||||
return;
|
||||
}
|
||||
|
||||
// clear the name and set default visibility
|
||||
setSelectedName('');
|
||||
setSelectedVisibility(template.visibility);
|
||||
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'select_template', {category: template.category, template: template.id});
|
||||
setModalState(ModalState.Preview);
|
||||
};
|
||||
|
||||
const handleOnNameChanged = (name: string) => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'customize_name', {category: selectedTemplate?.category, template: selectedTemplate?.id});
|
||||
setSelectedName(name);
|
||||
};
|
||||
|
||||
const handleOnVisibilityChanged = (visibility: Visibility) => {
|
||||
if (visibility === Visibility.Public) {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'changed_visibility_public', {category: selectedTemplate?.category, template: selectedTemplate?.id});
|
||||
} else {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'customized_visibility_private', {category: selectedTemplate?.category, template: selectedTemplate?.id});
|
||||
}
|
||||
|
||||
setSelectedVisibility(visibility);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the necessary data in the global store as long storing in DB preferences the tourtip information
|
||||
* @param template current used worktempplate
|
||||
*/
|
||||
const tourTipActions = async (template: WorkTemplate, firstChannelId: string) => {
|
||||
const linkedProductsCount = getContentCount(template, playbookTemplates, firstChannelId);
|
||||
|
||||
// stepValue and pluginId are used for showing the tourtip for the used template
|
||||
let pluginId;
|
||||
if (linkedProductsCount.playbooks) {
|
||||
pluginId = rhsPluggableIds.get(suitePluginIds.playbooks);
|
||||
} else {
|
||||
pluginId = rhsPluggableIds.get(suitePluginIds.boards);
|
||||
}
|
||||
|
||||
if (!pluginId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// store in the global state the plugins/integrations information related to the used template
|
||||
// so we can display that data in the tourtip
|
||||
await dispatch(onExecuteSuccess(linkedProductsCount));
|
||||
|
||||
// store the required preferences for the tourtip
|
||||
const tourCategory = TutorialTourName.WORK_TEMPLATE_TUTORIAL;
|
||||
|
||||
const preferences = [
|
||||
{
|
||||
user_id: currentUserId,
|
||||
category: tourCategory,
|
||||
name: TTNameMapToATStatusKey[tourCategory],
|
||||
value: String(AutoTourStatus.ENABLED),
|
||||
},
|
||||
];
|
||||
await dispatch(savePreferences(currentUserId, preferences));
|
||||
|
||||
dispatch(showRHSPlugin(pluginId));
|
||||
};
|
||||
|
||||
const execute = async (template: WorkTemplate, name = '', visibility: Visibility) => {
|
||||
const pbTemplates = [];
|
||||
for (const ctt in template.content) {
|
||||
if (!Object.hasOwn(template.content, ctt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const item = template.content[ctt];
|
||||
if (item.playbook) {
|
||||
const pbTemplate = playbookTemplates.find((pb) => pb.title === item.playbook.template);
|
||||
if (pbTemplate) {
|
||||
pbTemplates.push(pbTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove non recommended integrations
|
||||
const filteredTemplate = {...template};
|
||||
filteredTemplate.content = template.content.filter((item) => {
|
||||
if (!item.integration) {
|
||||
return true;
|
||||
}
|
||||
return item.integration.recommended;
|
||||
});
|
||||
|
||||
const req: ExecuteWorkTemplateRequest = {
|
||||
team_id: teamId,
|
||||
name,
|
||||
visibility,
|
||||
work_template: filteredTemplate,
|
||||
playbook_templates: pbTemplates,
|
||||
};
|
||||
|
||||
setIsCreating(true);
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'executing', {category: template.category, template: template.id, customized_name: name !== '', customized_visibility: visibility !== template.visibility});
|
||||
const {data, error} = await dispatch(executeWorkTemplate(req)) as ActionResult<ExecuteWorkTemplateResponse>;
|
||||
|
||||
if (error) {
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'execution_error', {category: template.category, template: template.id, customized_name: name !== '', customized_visibility: visibility !== template.visibility, error: error.message});
|
||||
setErrorText(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, 'execution_success', {category: template.category, template: template.id, customized_name: name !== '', customized_visibility: visibility !== template.visibility});
|
||||
let firstChannelId = '';
|
||||
|
||||
if (data?.channel_with_playbook_ids.length) {
|
||||
firstChannelId = data.channel_with_playbook_ids[0];
|
||||
} else if (data?.channel_ids.length) {
|
||||
firstChannelId = data.channel_ids[0];
|
||||
}
|
||||
|
||||
if (firstChannelId) {
|
||||
dispatch(loadIfNecessaryAndSwitchToChannelById(firstChannelId));
|
||||
}
|
||||
|
||||
await tourTipActions(template, firstChannelId);
|
||||
|
||||
setIsCreating(false);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const trackAction = (action: string, actionFn: () => void) => {
|
||||
return () => {
|
||||
let props = {};
|
||||
if (selectedTemplate) {
|
||||
props = {category: selectedTemplate?.category, template: selectedTemplate?.id};
|
||||
}
|
||||
trackEvent(TELEMETRY_CATEGORIES.WORK_TEMPLATES, action, props);
|
||||
|
||||
actionFn();
|
||||
};
|
||||
};
|
||||
|
||||
let title;
|
||||
let cancelButtonText;
|
||||
let cancelButtonAction;
|
||||
let backArrowAction;
|
||||
let confirmButtonText;
|
||||
let confirmButtonAction;
|
||||
switch (modalState) {
|
||||
case ModalState.Menu:
|
||||
title = formatMessage({id: 'work_templates.menu.modal_title', defaultMessage: 'Create from a template'});
|
||||
break;
|
||||
case ModalState.Preview:
|
||||
title = formatMessage({id: 'work_templates.preview.modal_title', defaultMessage: 'Preview {useCase}'}, {useCase: selectedTemplate?.useCase});
|
||||
cancelButtonText = formatMessage({id: 'work_templates.preview.modal_cancel_button', defaultMessage: 'Back'});
|
||||
cancelButtonAction = trackAction('btn_back_to_menu', goToMenu);
|
||||
backArrowAction = trackAction('arrow_back_to_menu', goToMenu);
|
||||
confirmButtonText = formatMessage({id: 'work_templates.preview.modal_next_button', defaultMessage: 'Next'});
|
||||
confirmButtonAction = trackAction('btn_go_to_customize', () => setModalState(ModalState.Customize));
|
||||
break;
|
||||
case ModalState.Customize:
|
||||
title = formatMessage({id: 'work_templates.customize.modal_title', defaultMessage: 'Name your {useCase}'}, {useCase: selectedTemplate?.useCase});
|
||||
cancelButtonText = formatMessage({id: 'work_templates.customize.modal_cancel_button', defaultMessage: 'Back'});
|
||||
cancelButtonAction = trackAction('btn_back_to_preview', () => setModalState(ModalState.Preview));
|
||||
backArrowAction = trackAction('arrow_back_to_preview', () => setModalState(ModalState.Preview));
|
||||
confirmButtonText = formatMessage({id: 'work_templates.customize.modal_create_button', defaultMessage: 'Create'});
|
||||
confirmButtonAction = trackAction('btn_execute', () => execute(selectedTemplate!, selectedName, selectedVisibility));
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
id='work-template-modal'
|
||||
className={classnames('work-template-modal', `work-template-modal--${modalState}`)}
|
||||
modalHeaderText={
|
||||
<ModalTitle
|
||||
text={title}
|
||||
backArrowAction={backArrowAction}
|
||||
/>
|
||||
}
|
||||
compassDesign={true}
|
||||
onExited={closeModal}
|
||||
cancelButtonText={cancelButtonText}
|
||||
handleCancel={cancelButtonAction}
|
||||
confirmButtonText={confirmButtonText}
|
||||
handleConfirm={confirmButtonAction}
|
||||
isConfirmDisabled={isCreating || (modalState === ModalState.Customize && errorText !== '')}
|
||||
autoCloseOnCancelButton={false}
|
||||
autoCloseOnConfirmButton={false}
|
||||
errorText={errorText}
|
||||
>
|
||||
{modalState === ModalState.Menu && (
|
||||
<Menu
|
||||
categories={categories}
|
||||
onTemplateSelected={handleTemplateSelected}
|
||||
changeCategory={changeCategory}
|
||||
workTemplates={workTemplates}
|
||||
currentCategoryId={currentCategoryId}
|
||||
disableQuickUse={isCreating}
|
||||
/>
|
||||
)}
|
||||
{(modalState === ModalState.Preview && selectedTemplate) && (
|
||||
<Preview
|
||||
template={selectedTemplate}
|
||||
pluginsEnabled={pluginsEnabled}
|
||||
/>
|
||||
)}
|
||||
{(modalState === ModalState.Customize && selectedTemplate) && (
|
||||
<Customize
|
||||
name={selectedName}
|
||||
visibility={selectedVisibility}
|
||||
onNameChanged={handleOnNameChanged}
|
||||
onVisibilityChanged={handleOnVisibilityChanged}
|
||||
template={selectedTemplate}
|
||||
/>
|
||||
)}
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkTemplateModal;
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {PlaybookTemplateType, WorkTemplate} from '@mattermost/types/work_templates';
|
||||
|
||||
export function getTemplateDefaultIllustration(template: WorkTemplate): string {
|
||||
const channels = template.content.filter((c) => c.channel).map((c) => c.channel!);
|
||||
if (channels.length) {
|
||||
return channels[0].illustration;
|
||||
}
|
||||
if (template.description.channel.illustration) {
|
||||
return template.description.channel.illustration;
|
||||
}
|
||||
|
||||
const boards = template.content.filter((c) => c.board).map((c) => c.board!);
|
||||
if (boards.length) {
|
||||
return boards[0].illustration;
|
||||
}
|
||||
if (template.description.board.illustration) {
|
||||
return template.description.board.illustration;
|
||||
}
|
||||
|
||||
const playbooks = template.content.filter((c) => c.playbook).map((c) => c.playbook!);
|
||||
if (playbooks.length) {
|
||||
return playbooks[0].illustration;
|
||||
}
|
||||
if (template.description.playbook.illustration) {
|
||||
return template.description.playbook.illustration;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export const getContentCount = (template: WorkTemplate, playbookTemplates: PlaybookTemplateType[], channelId: string) => {
|
||||
const res = {
|
||||
playbooks: 0,
|
||||
boards: 0,
|
||||
channelId,
|
||||
};
|
||||
for (const item of template.content) {
|
||||
if (item.playbook) {
|
||||
const pbTemplate = playbookTemplates.find((pb) => pb.title === item.playbook.template);
|
||||
if (pbTemplate) {
|
||||
res.playbooks++;
|
||||
}
|
||||
} else if (item.board) {
|
||||
res.boards++;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
@@ -3476,8 +3476,8 @@
|
||||
"file_search_result_item.open_in_channel": "Open in channel",
|
||||
"file_upload.disabled": "File attachments are disabled.",
|
||||
"file_upload.drag_folder": "Folders cannot be uploaded. Please drag all files separately.",
|
||||
"file_upload.fileAbove": "File above {max}MB cannot be uploaded: {filename}",
|
||||
"file_upload.filesAbove": "Files above {max}MB cannot be uploaded: {filenames}",
|
||||
"file_upload.fileAbove": "File above {max}MB could not be uploaded: {filename}",
|
||||
"file_upload.filesAbove": "Files above {max}MB could not be uploaded: {filenames}",
|
||||
"file_upload.generic_error": "There was a problem uploading your files.",
|
||||
"file_upload.limited": "Uploads limited to {count, number} files maximum. Please use additional posts for more files.",
|
||||
"file_upload.menuAriaLabel": "Upload type selector",
|
||||
@@ -4255,9 +4255,6 @@
|
||||
"onboarding_wizard.plugins.zoom": "Zoom",
|
||||
"onboarding_wizard.plugins.zoom.tooltip": "Start Zoom audio and video conferencing calls in Mattermost with a single click",
|
||||
"onboarding_wizard.previous": "Previous",
|
||||
"onboarding_wizard.roles.description": "We’ll use this to suggest templates to help get you started.",
|
||||
"onboarding_wizard.roles.other_input_placeholder": "Please share your primary function",
|
||||
"onboarding_wizard.roles.title": "What is your primary function?",
|
||||
"onboarding_wizard.self_hosted_plugins.description": "Choose the tools you work with, and we'll add them to your workspace. Additional set up may be needed later.",
|
||||
"onboarding_wizard.self_hosted_plugins.title": "What tools do you use?",
|
||||
"onboarding_wizard.skip-button": "Skip",
|
||||
@@ -4272,7 +4269,6 @@
|
||||
"onboardingTask.checklist.no_thanks": "No, thanks",
|
||||
"onboardingTask.checklist.start_enterprise_now": "Start your free Enterprise trial now!",
|
||||
"onboardingTask.checklist.task_complete_your_profile": "Complete your profile.",
|
||||
"onboardingTask.checklist.task_create_from_work_template": "Create from a template.",
|
||||
"onboardingTask.checklist.task_download_mm_apps": "Download the Desktop and Mobile Apps.",
|
||||
"onboardingTask.checklist.task_explore_other_tools_in_platform": "Explore other tools in the platform.",
|
||||
"onboardingTask.checklist.task_invite_team_members": "Invite team members to the workspace.",
|
||||
@@ -4367,14 +4363,6 @@
|
||||
"picture_selector.select_button.ariaLabel": "Select picture",
|
||||
"plan.cloud": "Cloud",
|
||||
"plan.self_serve": "Self-serve",
|
||||
"pluggable_rhs.tourtip.boards.access": "Access your linked boards from the Boards icon on the right hand App bar.",
|
||||
"pluggable_rhs.tourtip.boards.click": "Click into boards from this right panel.",
|
||||
"pluggable_rhs.tourtip.boards.review": "Review board updates from your channels.",
|
||||
"pluggable_rhs.tourtip.boards.title": "Access your {count} linked {num, plural, one {board} other {boards}}!",
|
||||
"pluggable_rhs.tourtip.playbooks.access": "Access your linked playbooks from the Playbooks icon on the right hand App bar.",
|
||||
"pluggable_rhs.tourtip.playbooks.click": "Click into playbooks from this right panel.",
|
||||
"pluggable_rhs.tourtip.playbooks.review": "Review playbook updates from your channels.",
|
||||
"pluggable_rhs.tourtip.playbooks.title": "Access your {count} linked {num, plural, one {playbook} other {playbooks}}.",
|
||||
"pluggable.errorOccurred": "An error occurred in the {pluginId} plugin.",
|
||||
"pluggable.errorRefresh": "Refresh?",
|
||||
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They cannot be added to the channel because they are not a member of the linked groups. To add them to this channel, they must be added to the linked groups.",
|
||||
@@ -4877,8 +4865,6 @@
|
||||
"sidebar_left.add_channel_dropdown.dropdownAriaLabel": "Add Channel Dropdown",
|
||||
"sidebar_left.add_channel_dropdown.invitePeople": "Invite people",
|
||||
"sidebar_left.add_channel_dropdown.invitePeopleExtraText": "Add people to the team",
|
||||
"sidebar_left.add_channel_dropdown.work_template": "Create from a template",
|
||||
"sidebar_left.add_channel_dropdown.work_template_extra": "Link channels, boards, and playbooks together",
|
||||
"sidebar_left.addChannelsCta": "Add channels",
|
||||
"sidebar_left.channel_filter.filterByUnread": "Filter by unread",
|
||||
"sidebar_left.channel_filter.filterUnreadAria": "unreads filter",
|
||||
@@ -5102,7 +5088,6 @@
|
||||
"team.button.tooltip": "Ctrl|Alt|{order}",
|
||||
"team.button.tooltip.mac": "⌘|⌥|{order}",
|
||||
"team.button.unread.ariaLabel": "{teamName} team unread",
|
||||
"templates_command.disabled": "Templates are disabled. Please contact your System Administrator for details.",
|
||||
"terms_of_service.agreeButton": "I Agree",
|
||||
"terms_of_service.api_error": "Unable to complete the request. If this issue persists, contact your System Administrator.",
|
||||
"terms_of_service.disagreeButton": "I Disagree",
|
||||
@@ -5703,40 +5688,6 @@
|
||||
"widgets.users_emails_input.loading": "Loading",
|
||||
"widgets.users_emails_input.no_user_found_matching": "No one found matching **{text}**. Enter their email to invite them.",
|
||||
"widgets.users_emails_input.valid_email": "Add **{email}**",
|
||||
"work_templates.customize.modal_cancel_button": "Back",
|
||||
"work_templates.customize.modal_create_button": "Create",
|
||||
"work_templates.customize.modal_title": "Name your {useCase}",
|
||||
"work_templates.customize.name_description": "This will help you and others find your project items. You can always edit this later.",
|
||||
"work_templates.customize.name_input_placeholder": "e.g. Web app, Growth, etc.",
|
||||
"work_templates.customize.name_label_all": "Name your channel, board, and playbook",
|
||||
"work_templates.customize.name_label_channels_boards": "Name your channel and board",
|
||||
"work_templates.customize.name_label_channels_playbooks": "Name your channel and playbook",
|
||||
"work_templates.customize.private_channel_permission_issue": "You do not have permission to create private channels.",
|
||||
"work_templates.customize.private_playbook_license_issue": "Private playbooks requires an Enterprise license.",
|
||||
"work_templates.customize.private_playbook_permission_issue": "You do not have permission to create private playbooks.",
|
||||
"work_templates.customize.public_channel_permission_issue": "You do not have permission to create public channels.",
|
||||
"work_templates.customize.public_playbook_permission_issue": "You do not have permission to create public playbooks.",
|
||||
"work_templates.customize.visibility_title": "Who should have access to this?",
|
||||
"work_templates.menu.modal_title": "Create from a template",
|
||||
"work_templates.menu.quick_use": "Quick use",
|
||||
"work_templates.menu.template_title": "TEMPLATE",
|
||||
"work_templates.menu.usecase_boards_count": "{boardsCount, plural, =1 {# board} other {# boards}}",
|
||||
"work_templates.menu.usecase_channels_count": "{channelsCount, plural, =1 {# channel} other {# channels}}",
|
||||
"work_templates.menu.usecase_playbooks_count": "{playbooksCount, plural, =1 {# playbook} other {# playbooks}}",
|
||||
"work_templates.preview.accordion_title_boards": "Boards",
|
||||
"work_templates.preview.accordion_title_channels": "Channels",
|
||||
"work_templates.preview.accordion_title_playbooks": "Playbooks",
|
||||
"work_templates.preview.integrations.admin_install.multiple_plugin": "Integrations will not be added until admin installs them.",
|
||||
"work_templates.preview.integrations.admin_install.notify": "Notify admin to install integrations",
|
||||
"work_templates.preview.integrations.admin_install.single_plugin": "{plugin} will not be added until admin installs it.",
|
||||
"work_templates.preview.integrations.already_installed": "Already installed",
|
||||
"work_templates.preview.integrations.app_install": "App Install",
|
||||
"work_templates.preview.integrations.to_be_installed": "To be installed",
|
||||
"work_templates.preview.modal_cancel_button": "Back",
|
||||
"work_templates.preview.modal_next_button": "Next",
|
||||
"work_templates.preview.modal_title": "Preview {useCase}",
|
||||
"work_templates.preview.section.included": "Included",
|
||||
"work_templates.preview.what_you_get": "Here's what you'll get:",
|
||||
"workspace_limits.archived_file.archived": "This file is archived",
|
||||
"workspace_limits.archived_file.archived_compact": "(archived)",
|
||||
"workspace_limits.archived_file.tooltip_description": "Your workspace has hit the file storage limit of {storageLimit}. To view this again, upgrade to a paid plan",
|
||||
|
||||
|
До Ширина: | Высота: | Размер: 143 KiB |
|
До Ширина: | Высота: | Размер: 53 KiB |
|
До Ширина: | Высота: | Размер: 259 KiB |
|
До Ширина: | Высота: | Размер: 419 KiB |
Двоичные данные
webapp/channels/src/images/worktemplates/boards/roadmap.png
|
До Ширина: | Высота: | Размер: 296 KiB |
|
До Ширина: | Высота: | Размер: 306 KiB |
|
До Ширина: | Высота: | Размер: 236 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 119 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 182 KiB |
|
До Ширина: | Высота: | Размер: 80 KiB |
|
До Ширина: | Высота: | Размер: 22 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 119 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 98 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 113 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 68 KiB |
|
До Ширина: | Высота: | Размер: 109 KiB |
|
До Ширина: | Высота: | Размер: 565 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 119 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 88 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 113 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 68 KiB |
|
До Ширина: | Высота: | Размер: 109 KiB |
|
До Ширина: | Высота: | Размер: 565 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 119 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 98 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 182 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 68 KiB |
Двоичные данные
webapp/channels/src/images/worktemplates/integrations.png
|
До Ширина: | Высота: | Размер: 38 KiB |
|
До Ширина: | Высота: | Размер: 80 KiB |
|
До Ширина: | Высота: | Размер: 22 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 119 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 182 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 88 KiB |
|
До Ширина: | Высота: | Размер: 80 KiB |
|
До Ширина: | Высота: | Размер: 22 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 119 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 182 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 113 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 119 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |
|
До Ширина: | Высота: | Размер: 98 KiB |
|
До Ширина: | Высота: | Размер: 568 KiB |
|
До Ширина: | Высота: | Размер: 182 KiB |
|
До Ширина: | Высота: | Размер: 567 KiB |