[MM-52926] Deprecating work templates (#23466)

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Julien Tant
2023-06-05 18:42:03 -07:00
коммит произвёл GitHub
родитель 22275fa0f5
Коммит 0468e772a9
184 изменённых файлов: 4 добавлений и 10688 удалений

Просмотреть файл

@@ -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={'Well 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": "Well 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

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 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

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 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

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше