Merge branch 'master' into MM-51456-update-overage-notices
Этот коммит содержится в:
@@ -14,7 +14,7 @@ import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getBool, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
|
||||
import {ChannelTypes} from 'mattermost-redux/action_types';
|
||||
@@ -367,11 +367,19 @@ export async function redirectUserToDefaultTeam() {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the user is the first admin
|
||||
const isUserFirstAdmin = isFirstAdmin(state);
|
||||
|
||||
const locale = getCurrentLocale(state);
|
||||
const teamId = LocalStorageStore.getPreviousTeamId(user.id);
|
||||
|
||||
let myTeams = getMyTeams(state);
|
||||
if (myTeams.length === 0) {
|
||||
if (isUserFirstAdmin) {
|
||||
getHistory().push('/preparing-workspace');
|
||||
return;
|
||||
}
|
||||
|
||||
getHistory().push('/select_team');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6887,6 +6887,15 @@ const AdminDefinition = {
|
||||
isHidden: it.licensedForFeature('Cloud'),
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
{
|
||||
type: Constants.SettingsTypes.TYPE_BOOL,
|
||||
key: 'ExperimentalSettings.DisableRefetchingOnBrowserFocus',
|
||||
label: t('admin.experimental.disableRefetchingOnBrowserFocus.title'),
|
||||
label_default: 'Disable data refetching on browser refocus:',
|
||||
help_text: t('admin.experimental.disableRefetchingOnBrowserFocus.desc'),
|
||||
help_text_default: 'When true, Mattermost will not refetch channels and channel members when the browser regains focus. This may result in improved performance for users with many channels and channel members.',
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -197,6 +197,58 @@ describe('ToYearlyNudgeBannerDismissable', () => {
|
||||
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show when subscription has billing type of internal', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_professional',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
billing_type: 'internal',
|
||||
},
|
||||
products: {
|
||||
prod_professional: {
|
||||
id: 'prod_professional',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
recurring_interval: RecurringIntervals.MONTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show when subscription has billing type of licensed', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users.profiles = {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_professional',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
billing_type: 'licensed',
|
||||
},
|
||||
products: {
|
||||
prod_professional: {
|
||||
id: 'prod_professional',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
recurring_interval: RecurringIntervals.MONTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ToYearlyNudgeBanner', () => {
|
||||
@@ -241,6 +293,51 @@ describe('ToYearlyNudgeBanner', () => {
|
||||
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
test('should NOT show when subscription has billing type of internal', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_professional',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
billing_type: 'internal',
|
||||
},
|
||||
products: {
|
||||
prod_professional: {
|
||||
id: 'prod_professional',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
recurring_interval: RecurringIntervals.MONTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show when subscription has billing type of licensed', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_professional',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
billing_type: 'licensed',
|
||||
},
|
||||
products: {
|
||||
prod_professional: {
|
||||
id: 'prod_professional',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
recurring_interval: RecurringIntervals.MONTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
|
||||
|
||||
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,12 +11,12 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
|
||||
|
||||
import {getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getSubscriptionProduct as selectSubscriptionProduct, getCloudSubscription as selectCloudSubscription} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {get as getPreference} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {AnnouncementBarTypes, CloudBanners, CloudProducts, Preferences, RecurringIntervals} from 'utils/constants';
|
||||
import {AnnouncementBarTypes, CloudBanners, CloudProducts, Preferences, RecurringIntervals, CloudBillingTypes} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
@@ -53,6 +53,7 @@ const ToYearlyNudgeBannerDismissable = () => {
|
||||
const show = snoozeInfo.show;
|
||||
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const subscription = useSelector(selectCloudSubscription);
|
||||
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
const currentProductProfessional = product?.sku === CloudProducts.PROFESSIONAL;
|
||||
@@ -139,6 +140,10 @@ const ToYearlyNudgeBannerDismissable = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (subscription?.billing_type === CloudBillingTypes.INTERNAL || subscription?.billing_type === CloudBillingTypes.LICENSED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_yearly.announcement_bar'
|
||||
@@ -159,7 +164,7 @@ const ToYearlyNudgeBannerDismissable = () => {
|
||||
showCloseButton={daysToProMonthlyEnd > 10}
|
||||
onButtonClick={() => openPurchaseModal({trackingLocation: 'to_yearly_nudge_annoucement_bar'})}
|
||||
modalButtonText={t('cloud_billing.nudge_to_yearly.learn_more')}
|
||||
modalButtonDefaultText='Learn more'
|
||||
modalButtonDefaultText='Update billing'
|
||||
message={message}
|
||||
showLinkAsButton={true}
|
||||
handleClose={showBanner}
|
||||
@@ -173,6 +178,7 @@ const ToYearlyNudgeBanner = () => {
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
|
||||
const subscription = useSelector(selectCloudSubscription);
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
const currentProductProfessional = product?.sku === CloudProducts.PROFESSIONAL;
|
||||
const currentProductIsMonthly = product?.recurring_interval === RecurringIntervals.MONTH;
|
||||
@@ -182,6 +188,10 @@ const ToYearlyNudgeBanner = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (subscription?.billing_type === CloudBillingTypes.INTERNAL || subscription?.billing_type === CloudBillingTypes.LICENSED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = moment(Date.now());
|
||||
const proMonthlyEndDate = moment(cloudProMonthlyCloseMoment, 'YYYYMMDD');
|
||||
const daysToProMonthlyEnd = proMonthlyEndDate.diff(now, 'days');
|
||||
|
||||
@@ -38,7 +38,11 @@ export default class GeneratedSetting extends React.PureComponent<Props> {
|
||||
private regenerate = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onChange(this.props.id, crypto.randomBytes(256).toString('base64').substring(0, 32));
|
||||
// Pure base64 implementation can contain characters that are not URL safe without additional
|
||||
// encoding. Adopt a URL/Filename safer alphabet as noted in https://datatracker.ietf.org/doc/html/rfc4648#section-5
|
||||
// where: 62 - (minus) , 63 _ (underscore)
|
||||
const value = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
this.props.onChange(this.props.id, value.replaceAll('+', '-').replaceAll('/', '_'));
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
||||
@@ -90,7 +90,7 @@ export default class AppsFormSelectField extends React.PureComponent<Props, Stat
|
||||
loadDynamicUserOptions = async (userInput: string): Promise<AppSelectOption[]> => {
|
||||
const usersSearchResults: UserAutocomplete = await this.props.actions.autocompleteUsers(userInput.toLowerCase());
|
||||
|
||||
return usersSearchResults.users.map((user) => {
|
||||
return usersSearchResults.users.filter((user) => !user.is_bot).map((user) => {
|
||||
const label = this.props.teammateNameDisplay ? displayUsername(user, this.props.teammateNameDisplay) : user.username;
|
||||
|
||||
return {...user, label, value: user.id, icon_data: imageURLForUser(user.id)};
|
||||
|
||||
@@ -65,7 +65,6 @@ exports[`component/create_user_groups_modal should match snapshot with back butt
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
className="overflow--visible"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
@@ -184,7 +183,6 @@ exports[`component/create_user_groups_modal should match snapshot without back b
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
className="overflow--visible"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
margin-top: calc(50vh - 240px);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.Input___error {
|
||||
&.group-error {
|
||||
position: absolute;
|
||||
|
||||
@@ -213,9 +213,7 @@ export default class CreateUserGroupsModal extends React.PureComponent<Props, St
|
||||
}
|
||||
|
||||
</Modal.Header>
|
||||
<Modal.Body
|
||||
className='overflow--visible'
|
||||
>
|
||||
<Modal.Body>
|
||||
<div className='user-groups-modal__content'>
|
||||
<div className='group-name-input-wrapper'>
|
||||
<Input
|
||||
|
||||
@@ -6,7 +6,6 @@ import {useIntl} from 'react-intl';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
import {useLocation, useHistory} from 'react-router-dom';
|
||||
|
||||
import {redirectUserToDefaultTeam} from 'actions/global_actions';
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
|
||||
import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg';
|
||||
@@ -15,7 +14,6 @@ import LoadingScreen from 'components/loading_screen';
|
||||
|
||||
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
||||
import {verifyUserEmail, getMe} from 'mattermost-redux/actions/users';
|
||||
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
@@ -40,7 +38,6 @@ const DoVerifyEmail = () => {
|
||||
const token = params.get('token') ?? '';
|
||||
|
||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||
|
||||
const [verifyStatus, setVerifyStatus] = useState(VerifyStatus.PENDING);
|
||||
const [serverError, setServerError] = useState('');
|
||||
@@ -52,16 +49,11 @@ const DoVerifyEmail = () => {
|
||||
|
||||
const handleRedirect = () => {
|
||||
if (loggedIn) {
|
||||
if (useCaseOnboarding) {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
return;
|
||||
}
|
||||
|
||||
redirectUserToDefaultTeam();
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {withRouter} from 'react-router-dom';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getUserGuideDropdownPluginMenuItems} from 'selectors/plugins';
|
||||
@@ -32,7 +31,6 @@ function mapStateToProps(state: GlobalState) {
|
||||
teamUrl: getCurrentRelativeTeamUrl(state),
|
||||
pluginMenuItems: getUserGuideDropdownPluginMenuItems(state),
|
||||
isFirstAdmin: isFirstAdmin(state),
|
||||
useCaseOnboarding: getUseCaseOnboarding(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ describe('components/channel_header/components/UserGuideDropdown', () => {
|
||||
},
|
||||
pluginMenuItems: [],
|
||||
isFirstAdmin: false,
|
||||
useCaseOnboarding: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -288,4 +288,18 @@ describe('components/login/Login', () => {
|
||||
expect(externalLoginButton.props().label).toEqual('OpenID 2');
|
||||
expect(externalLoginButton.props().style).toEqual({color: '#00ff00', borderColor: '#00ff00'});
|
||||
});
|
||||
|
||||
it('should redirect on login', () => {
|
||||
mockState.entities.users.currentUserId = 'user1';
|
||||
LocalStorageStore.setWasLoggedIn(true);
|
||||
mockConfig.EnableSignInWithEmail = 'true';
|
||||
const redirectPath = '/boards/team/teamID/boardID';
|
||||
mockLocation.search = '?redirect_to=' + redirectPath;
|
||||
mount(
|
||||
<MemoryRouter>
|
||||
<Login/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(redirectPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getUseCaseOnboarding, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTeamByName, getMyTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
@@ -104,7 +104,6 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const experimentalPrimaryTeam = useSelector((state: GlobalState) => (ExperimentalPrimaryTeam ? getTeamByName(state, ExperimentalPrimaryTeam) : undefined));
|
||||
const experimentalPrimaryTeamMember = useSelector((state: GlobalState) => getMyTeamMember(state, experimentalPrimaryTeam?.id ?? ''));
|
||||
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||
const isCloud = useSelector(isCurrentLicenseCloud);
|
||||
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
||||
|
||||
@@ -142,6 +141,9 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableSignUpWithSaml;
|
||||
const showSignup = enableOpenServer && (enableExternalSignup || enableSignUpWithEmail || enableLdap);
|
||||
|
||||
const query = new URLSearchParams(search);
|
||||
const redirectTo = query.get('redirect_to');
|
||||
|
||||
const getExternalLoginOptions = () => {
|
||||
const externalLoginOptions: ExternalLoginButtonType[] = [];
|
||||
|
||||
@@ -373,6 +375,10 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
|
||||
history.push(redirectTo);
|
||||
return;
|
||||
}
|
||||
redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
@@ -616,9 +622,6 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
dispatch(setNeedsLoggedInLimitReachedCheck(true));
|
||||
}
|
||||
|
||||
const query = new URLSearchParams(search);
|
||||
const redirectTo = query.get('redirect_to');
|
||||
|
||||
setCSRFFromCookie();
|
||||
|
||||
// Record a successful login to local storage. If an unintentional logout occurs, e.g.
|
||||
@@ -631,14 +634,12 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
} else if (experimentalPrimaryTeamMember.team_id) {
|
||||
// Only set experimental team if user is on that team
|
||||
history.push(`/${ExperimentalPrimaryTeam}`);
|
||||
} else if (useCaseOnboarding) {
|
||||
} else {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
} else {
|
||||
redirectUserToDefaultTeam();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InviteMembers component should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="InviteMembers-body test-class"
|
||||
>
|
||||
<div
|
||||
class="SingleColumnLayout"
|
||||
style="width: 547px;"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="PageLine PageLine--no-left"
|
||||
style="margin-bottom: 50px; margin-left: 50px; height: calc(25vh);"
|
||||
/>
|
||||
<div>
|
||||
Previous step
|
||||
</div>
|
||||
<h1
|
||||
class="PreparingWorkspaceTitle"
|
||||
>
|
||||
<span>
|
||||
Invite your team members
|
||||
</span>
|
||||
</h1>
|
||||
<p
|
||||
class="PreparingWorkspaceDescription"
|
||||
>
|
||||
<span>
|
||||
Collaboration is tough by yourself. Invite a few team members using the invitation link below.
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="PreparingWorkspacePageBody"
|
||||
>
|
||||
<div
|
||||
class="InviteMembersLink"
|
||||
>
|
||||
<input
|
||||
aria-label="team invite link"
|
||||
class="InviteMembersLink__input"
|
||||
data-testid="shareLinkInput"
|
||||
readonly=""
|
||||
type="text"
|
||||
value="https://my-org.mattermost.com/config/signup_user_complete/?id=1234"
|
||||
/>
|
||||
<button
|
||||
class="InviteMembersLink__button"
|
||||
data-testid="shareLinkInputButton"
|
||||
>
|
||||
<i
|
||||
class="icon icon-link-variant"
|
||||
/>
|
||||
<span>
|
||||
Copy Link
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="InviteMembers__submit"
|
||||
>
|
||||
<button
|
||||
class="primary-button"
|
||||
>
|
||||
<span>
|
||||
Finish setup
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="PageLine PageLine--no-left"
|
||||
style="margin-top: 50px; margin-left: 50px; height: calc(30vh);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,29 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/preparing-workspace/invite_members_link should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="InviteMembersLink"
|
||||
>
|
||||
<input
|
||||
aria-label="team invite link"
|
||||
class="InviteMembersLink__input"
|
||||
data-testid="shareLinkInput"
|
||||
readonly=""
|
||||
type="text"
|
||||
value="https://invite-url.mattermost.com"
|
||||
/>
|
||||
<button
|
||||
class="InviteMembersLink__button"
|
||||
data-testid="shareLinkInputButton"
|
||||
>
|
||||
<i
|
||||
class="icon icon-link-variant"
|
||||
/>
|
||||
<span>
|
||||
Copy Link
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/preparing-workspace/organization_status should match snapshot 1`] = `
|
||||
<div
|
||||
class="Organization__status"
|
||||
/>
|
||||
`;
|
||||
@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {Action} from 'mattermost-redux/types/actions';
|
||||
import {checkIfTeamExists, createTeam} from 'mattermost-redux/actions/teams';
|
||||
import {checkIfTeamExists, createTeam, updateTeam} from 'mattermost-redux/actions/teams';
|
||||
import {getProfiles} from 'mattermost-redux/actions/users';
|
||||
|
||||
import PreparingWorkspace, {Actions} from './preparing_workspace';
|
||||
@@ -13,6 +13,7 @@ import PreparingWorkspace, {Actions} from './preparing_workspace';
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
|
||||
updateTeam,
|
||||
createTeam,
|
||||
getProfiles,
|
||||
checkIfTeamExists,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
@import 'utils/mixins';
|
||||
|
||||
.InviteMembers-body {
|
||||
display: flex;
|
||||
// page width - channels preview width - progress dots width - people overlap width
|
||||
max-width: calc(100vw - 600px - 120px - 30px);
|
||||
|
||||
.UsersEmailsInput {
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
.InviteMembers {
|
||||
&__submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@include simple-in-and-out-before("InviteMembers");
|
||||
|
||||
.ChannelsPreview--enter-from-after {
|
||||
&-enter {
|
||||
transform: translateX(-100vw);
|
||||
}
|
||||
|
||||
&-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: transform 300ms ease-in-out;
|
||||
}
|
||||
|
||||
&-enter-done {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.ChannelsPreview--exit-to-after {
|
||||
&-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&-exit-active {
|
||||
transform: translateX(-100vw);
|
||||
transition: transform 300ms ease-in-out;
|
||||
}
|
||||
|
||||
&-exit-done {
|
||||
transform: translateX(-100vw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ComponentProps} from 'react';
|
||||
import {render, screen, fireEvent} from '@testing-library/react';
|
||||
import {withIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
import InviteMembers from './invite_members';
|
||||
|
||||
describe('InviteMembers component', () => {
|
||||
let defaultProps: ComponentProps<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultProps = {
|
||||
disableEdits: false,
|
||||
browserSiteUrl: 'https://my-org.mattermost.com',
|
||||
formUrl: 'https://my-org.mattermost.com/signup',
|
||||
teamInviteId: '1234',
|
||||
className: 'test-class',
|
||||
configSiteUrl: 'https://my-org.mattermost.com/config',
|
||||
onPageView: jest.fn(),
|
||||
previous: <div>{'Previous step'}</div>,
|
||||
next: jest.fn(),
|
||||
show: true,
|
||||
transitionDirection: 'forward',
|
||||
};
|
||||
});
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const component = withIntl(<InviteMembers {...defaultProps}/>);
|
||||
const {container} = render(component);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders invite URL', () => {
|
||||
const component = withIntl(<InviteMembers {...defaultProps}/>);
|
||||
render(component);
|
||||
const inviteLink = screen.getByTestId('shareLinkInput');
|
||||
expect(inviteLink).toHaveAttribute(
|
||||
'value',
|
||||
'https://my-org.mattermost.com/config/signup_user_complete/?id=1234',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders submit button with correct text', () => {
|
||||
const component = withIntl(<InviteMembers {...defaultProps}/>);
|
||||
render(component);
|
||||
const button = screen.getByRole('button', {name: 'Finish setup'});
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('button is disabled when disableEdits is true', () => {
|
||||
const component = withIntl(
|
||||
<InviteMembers
|
||||
{...defaultProps}
|
||||
disableEdits={true}
|
||||
/>,
|
||||
);
|
||||
render(component);
|
||||
const button = screen.getByRole('button', {name: 'Finish setup'});
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it('invokes next prop on button click', () => {
|
||||
const component = withIntl(<InviteMembers {...defaultProps}/>);
|
||||
render(component);
|
||||
const button = screen.getByRole('button', {name: 'Finish setup'});
|
||||
fireEvent.click(button);
|
||||
expect(defaultProps.next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo, useEffect} from 'react';
|
||||
import {CSSTransition} from 'react-transition-group';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
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 InviteMembersLink from './invite_members_link';
|
||||
import PageLine from './page_line';
|
||||
import './invite_members.scss';
|
||||
|
||||
type Props = PreparingWorkspacePageProps & {
|
||||
disableEdits: boolean;
|
||||
className?: string;
|
||||
teamInviteId?: string;
|
||||
formUrl: Form['url'];
|
||||
configSiteUrl?: string;
|
||||
browserSiteUrl: string;
|
||||
}
|
||||
|
||||
const InviteMembers = (props: Props) => {
|
||||
let className = 'InviteMembers-body';
|
||||
if (props.className) {
|
||||
className += ' ' + props.className;
|
||||
}
|
||||
|
||||
useEffect(props.onPageView, []);
|
||||
|
||||
const inviteURL = useMemo(() => {
|
||||
let urlBase = '';
|
||||
if (props.configSiteUrl && !props.configSiteUrl.includes('localhost')) {
|
||||
urlBase = props.configSiteUrl;
|
||||
} else if (props.formUrl && !props.formUrl.includes('localhost')) {
|
||||
urlBase = props.formUrl;
|
||||
} else {
|
||||
urlBase = props.browserSiteUrl;
|
||||
}
|
||||
return `${urlBase}/signup_user_complete/?id=${props.teamInviteId}`;
|
||||
}, [props.teamInviteId, props.configSiteUrl, props.browserSiteUrl, props.formUrl]);
|
||||
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.description_link'}
|
||||
defaultMessage='Collaboration is tough by yourself. Invite a few team members using the invitation link below.'
|
||||
/>
|
||||
);
|
||||
|
||||
const inviteInteraction = <InviteMembersLink inviteURL={inviteURL}/>;
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
in={props.show}
|
||||
timeout={Animations.PAGE_SLIDE}
|
||||
classNames={mapAnimationReasonToClass('InviteMembers', props.transitionDirection)}
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
<div className={className}>
|
||||
<SingleColumnLayout style={{width: 547}}>
|
||||
<PageLine
|
||||
style={{
|
||||
marginBottom: '50px',
|
||||
marginLeft: '50px',
|
||||
height: 'calc(25vh)',
|
||||
}}
|
||||
noLeft={true}
|
||||
/>
|
||||
{props.previous}
|
||||
<Title>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.title'}
|
||||
defaultMessage='Invite your team members'
|
||||
/>
|
||||
</Title>
|
||||
<Description>
|
||||
{description}
|
||||
</Description>
|
||||
<PageBody>
|
||||
{inviteInteraction}
|
||||
</PageBody>
|
||||
<div className='InviteMembers__submit'>
|
||||
<button
|
||||
className='primary-button'
|
||||
disabled={props.disableEdits}
|
||||
onClick={props.next}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.invite_members.next_link'}
|
||||
defaultMessage='Finish setup'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<PageLine
|
||||
style={{
|
||||
marginTop: '50px',
|
||||
marginLeft: '50px',
|
||||
height: 'calc(30vh)',
|
||||
}}
|
||||
noLeft={true}
|
||||
/>
|
||||
</SingleColumnLayout>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteMembers;
|
||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -0,0 +1,51 @@
|
||||
.InviteMembersLink {
|
||||
display: flex;
|
||||
|
||||
&__input {
|
||||
height: 48px;
|
||||
flex-grow: 1;
|
||||
padding: 12px 14px;
|
||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.2);
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.2);
|
||||
border-left: 1px solid rgba(var(--center-channel-color-rgb), 0.2);
|
||||
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
border-radius: 4px 0 0 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
width: 180px;
|
||||
max-width: 382px;
|
||||
height: 48px;
|
||||
flex-grow: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--button-bg);
|
||||
background: var(--center-channel-bg);
|
||||
border-radius: 0 4px 4px 0;
|
||||
color: var(--button-bg);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
margin-right: 9px;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {render, screen, fireEvent} from '@testing-library/react';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import InviteMembersLink from './invite_members_link';
|
||||
import {withIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
jest.mock('actions/telemetry_actions', () => ({
|
||||
trackEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('components/preparing-workspace/invite_members_link', () => {
|
||||
const inviteURL = 'https://invite-url.mattermost.com';
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
||||
|
||||
const {container} = render(component);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders an input field with the invite URL', () => {
|
||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
||||
render(component);
|
||||
const input = screen.getByDisplayValue(inviteURL);
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a button to copy the invite URL', () => {
|
||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
||||
render(component);
|
||||
const button = screen.getByRole('button', {name: /copy link/i});
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls the trackEvent function when the copy button is clicked', () => {
|
||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
||||
render(component);
|
||||
const button = screen.getByRole('button', {name: /copy link/i});
|
||||
fireEvent.click(button);
|
||||
expect(trackEvent).toHaveBeenCalledWith(
|
||||
'first_admin_setup',
|
||||
'admin_setup_click_copy_invite_link',
|
||||
);
|
||||
});
|
||||
|
||||
it('changes the button text to "Link Copied" when the URL is copied', () => {
|
||||
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
|
||||
render(component);
|
||||
const button = screen.getByRole('button', {name: /copy link/i});
|
||||
const originalText = 'Copy Link';
|
||||
const linkCopiedText = 'Link Copied';
|
||||
expect(button).toHaveTextContent(originalText);
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(button).toHaveTextContent(linkCopiedText);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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 useCopyText from 'components/common/hooks/useCopyText';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import './invite_members_link.scss';
|
||||
|
||||
type Props = {
|
||||
inviteURL: string;
|
||||
}
|
||||
|
||||
const InviteMembersLink = (props: Props) => {
|
||||
const copyText = useCopyText({
|
||||
trackCallback: () => trackEvent('first_admin_setup', 'admin_setup_click_copy_invite_link'),
|
||||
text: props.inviteURL,
|
||||
});
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className='InviteMembersLink'>
|
||||
<input
|
||||
className='InviteMembersLink__input'
|
||||
type='text'
|
||||
readOnly={true}
|
||||
value={props.inviteURL}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'onboarding_wizard.invite_members.copy_link_input',
|
||||
defaultMessage: 'team invite link',
|
||||
})}
|
||||
data-testid='shareLinkInput'
|
||||
/>
|
||||
<button
|
||||
className='InviteMembersLink__button'
|
||||
onClick={copyText.onClick}
|
||||
data-testid='shareLinkInputButton'
|
||||
>
|
||||
{copyText.copiedRecently ? (
|
||||
<>
|
||||
<i className='icon icon-check'/>
|
||||
<FormattedMessage
|
||||
id='onboarding_wizard.invite_members.copied_link'
|
||||
defaultMessage='Link Copied'
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className='icon icon-link-variant'/>
|
||||
<FormattedMessage
|
||||
id='onboarding_wizard.invite_members.copy_link'
|
||||
defaultMessage='Copy Link'
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteMembersLink;
|
||||
12
webapp/channels/src/components/preparing_workspace/mixins.scss
Обычный файл
12
webapp/channels/src/components/preparing_workspace/mixins.scss
Обычный файл
@@ -0,0 +1,12 @@
|
||||
@mixin input {
|
||||
width: 452px;
|
||||
padding: 12px 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
@import 'utils/variables';
|
||||
@import 'utils/mixins';
|
||||
@import './mixins';
|
||||
|
||||
.Organization-body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Organization-form-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Organization-left-col {
|
||||
width: 210px;
|
||||
min-width: 210px;
|
||||
}
|
||||
|
||||
.Organization-right-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Organization {
|
||||
&__input {
|
||||
@include input;
|
||||
}
|
||||
|
||||
&__status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
font-size: 12px;
|
||||
|
||||
&--error {
|
||||
margin-top: 8px;
|
||||
color: var(--dnd-indicator);
|
||||
}
|
||||
}
|
||||
|
||||
&__progress-path {
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
left: -55px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__content {
|
||||
margin-left: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.Organization-left-col {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include simple-in-and-out("Organization");
|
||||
206
webapp/channels/src/components/preparing_workspace/organization.tsx
Обычный файл
206
webapp/channels/src/components/preparing_workspace/organization.tsx
Обычный файл
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useEffect, useRef, ChangeEvent} from 'react';
|
||||
import {CSSTransition} from 'react-transition-group';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import OrganizationSVG from 'components/common/svg_images_components/organization-building_svg';
|
||||
import QuickInput from 'components/quick_input';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import {getTeams} from 'mattermost-redux/actions/teams';
|
||||
import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {teamNameToUrl} from 'utils/url';
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import OrganizationStatus, {TeamApiError} from './organization_status';
|
||||
import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps} from './steps';
|
||||
import PageLine from './page_line';
|
||||
import Title from './title';
|
||||
import Description from './description';
|
||||
import PageBody from './page_body';
|
||||
|
||||
import './organization.scss';
|
||||
|
||||
type Props = PreparingWorkspacePageProps & {
|
||||
organization: Form['organization'];
|
||||
setOrganization: (organization: Form['organization']) => void;
|
||||
className?: string;
|
||||
createTeam: (OrganizationName: string) => Promise<{error: string | null; newTeam: Team | null}>;
|
||||
updateTeam: (teamToUpdate: Team) => Promise<{error: string | null; updatedTeam: Team | null}>;
|
||||
setInviteId: (inviteId: string) => void;
|
||||
}
|
||||
|
||||
const reportValidationError = debounce(() => {
|
||||
trackEvent('first_admin_setup', 'validate_organization_error');
|
||||
}, 700, {leading: false});
|
||||
|
||||
const Organization = (props: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [triedNext, setTriedNext] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
const validation = teamNameToUrl(props.organization || '');
|
||||
const teamApiError = useRef<typeof TeamApiError | null>(null);
|
||||
|
||||
useEffect(props.onPageView, []);
|
||||
|
||||
const teams = useSelector(getActiveTeamsList);
|
||||
useEffect(() => {
|
||||
if (!teams) {
|
||||
dispatch(getTeams(0, 60));
|
||||
}
|
||||
}, [teams]);
|
||||
|
||||
const setApiCallError = () => {
|
||||
teamApiError.current = TeamApiError;
|
||||
};
|
||||
|
||||
const updateTeamNameFromOrgName = async () => {
|
||||
if (!inputRef.current?.value) {
|
||||
return;
|
||||
}
|
||||
const name = inputRef.current?.value.trim();
|
||||
|
||||
const currentTeam = teams[0];
|
||||
|
||||
if (currentTeam && name && name !== currentTeam.display_name) {
|
||||
const {error} = await props.updateTeam({...currentTeam, display_name: name});
|
||||
if (error !== null) {
|
||||
setApiCallError();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createTeamFromOrgName = async () => {
|
||||
if (!inputRef.current?.value) {
|
||||
return;
|
||||
}
|
||||
const name = inputRef.current?.value.trim();
|
||||
|
||||
if (name) {
|
||||
const {error, newTeam} = await props.createTeam(name);
|
||||
if (error !== null || newTeam === null) {
|
||||
props.setInviteId('');
|
||||
setApiCallError();
|
||||
return;
|
||||
}
|
||||
props.setInviteId(newTeam.invite_id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
props.setOrganization(e.target.value);
|
||||
teamApiError.current = null;
|
||||
};
|
||||
|
||||
const onNext = (e?: React.KeyboardEvent | React.MouseEvent) => {
|
||||
if (e && (e as React.KeyboardEvent).key) {
|
||||
if ((e as React.KeyboardEvent).key !== Constants.KeyCodes.ENTER[0]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!triedNext) {
|
||||
setTriedNext(true);
|
||||
}
|
||||
|
||||
// if there is already a team, maybe because a page reload, then just update the teamname
|
||||
const thereIsAlreadyATeam = teams.length > 0;
|
||||
teamApiError.current = null;
|
||||
|
||||
if (!validation.error && !thereIsAlreadyATeam) {
|
||||
createTeamFromOrgName();
|
||||
} else if (!validation.error && thereIsAlreadyATeam) {
|
||||
updateTeamNameFromOrgName();
|
||||
}
|
||||
|
||||
if (validation.error || teamApiError.current) {
|
||||
reportValidationError();
|
||||
return;
|
||||
}
|
||||
props.next?.();
|
||||
};
|
||||
|
||||
let className = 'Organization-body';
|
||||
if (props.className) {
|
||||
className += ' ' + props.className;
|
||||
}
|
||||
return (
|
||||
<CSSTransition
|
||||
in={props.show}
|
||||
timeout={Animations.PAGE_SLIDE}
|
||||
classNames={mapAnimationReasonToClass('Organization', props.transitionDirection)}
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
<div className={className}>
|
||||
<div className='Organization-right-col'>
|
||||
<div className='Organization-form-wrapper'>
|
||||
<div className='Organization__progress-path'>
|
||||
<OrganizationSVG/>
|
||||
<PageLine
|
||||
style={{
|
||||
marginTop: '5px',
|
||||
height: 'calc(50vh)',
|
||||
}}
|
||||
noLeft={true}
|
||||
/>
|
||||
</div>
|
||||
<div className='Organization__content'>
|
||||
{props.previous}
|
||||
<Title>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.organization.title'}
|
||||
defaultMessage='What’s the name of your organization?'
|
||||
/>
|
||||
</Title>
|
||||
<Description>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.organization.description'}
|
||||
defaultMessage='We’ll use this to help personalize your workspace.'
|
||||
/>
|
||||
</Description>
|
||||
<PageBody>
|
||||
<QuickInput
|
||||
placeholder={
|
||||
formatMessage({
|
||||
id: 'onboarding_wizard.organization.placeholder',
|
||||
defaultMessage: 'Organization name',
|
||||
})
|
||||
}
|
||||
className='Organization__input'
|
||||
value={props.organization || ''}
|
||||
onChange={(e) => handleOnChange(e)}
|
||||
onKeyUp={onNext}
|
||||
autoFocus={true}
|
||||
ref={inputRef as unknown as any}
|
||||
/>
|
||||
{triedNext ? <OrganizationStatus error={validation.error || teamApiError.current}/> : null}
|
||||
</PageBody>
|
||||
<button
|
||||
className='primary-button'
|
||||
data-testid='continue'
|
||||
onClick={onNext}
|
||||
disabled={!props.organization}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.next'}
|
||||
defaultMessage='Continue'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
export default Organization;
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {render} from '@testing-library/react';
|
||||
import {BadUrlReasons} from 'utils/url';
|
||||
import OrganizationStatus, {TeamApiError} from './organization_status';
|
||||
import {withIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
describe('components/preparing-workspace/organization_status', () => {
|
||||
const defaultProps = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const {container} = render(<OrganizationStatus {...defaultProps}/>);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render no error message when error prop is null', () => {
|
||||
const {queryByText, container} = render(<OrganizationStatus {...defaultProps}/>);
|
||||
expect((container.getElementsByClassName('Organization__status').length)).toBe(1);
|
||||
expect(queryByText(/empty/i)).not.toBeInTheDocument();
|
||||
expect(queryByText(/team api error/i)).not.toBeInTheDocument();
|
||||
expect(queryByText(/length/i)).not.toBeInTheDocument();
|
||||
expect(queryByText(/reserved/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render an error message for an empty organization name', () => {
|
||||
const component = withIntl(<OrganizationStatus error={BadUrlReasons.Empty}/>);
|
||||
const {getByText} = render(component);
|
||||
expect(getByText(/You must enter an organization name/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render an error message for a team API error', () => {
|
||||
const component = withIntl(<OrganizationStatus error={TeamApiError}/>);
|
||||
const {getByText} = render(component);
|
||||
expect(getByText(/There was an error, please try again/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render an error message for an organization name with invalid length', () => {
|
||||
const component = withIntl(<OrganizationStatus error={BadUrlReasons.Length}/>);
|
||||
const {getByText} = render(component);
|
||||
expect(getByText(/Organization name must be between 2 and 64 characters/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {BadUrlReasons, UrlValidationCheck} from 'utils/url';
|
||||
import Constants, {DocLinks} from 'utils/constants';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
export const TeamApiError = 'team_api_error';
|
||||
|
||||
const OrganizationStatus = (props: {error: (UrlValidationCheck['error'] | typeof TeamApiError | null)}): JSX.Element => {
|
||||
let children = null;
|
||||
let className = 'Organization__status';
|
||||
if (props.error) {
|
||||
className += ' Organization__status--error';
|
||||
switch (props.error) {
|
||||
case BadUrlReasons.Empty:
|
||||
children = (
|
||||
<FormattedMessage
|
||||
id='onboarding_wizard.organization.empty'
|
||||
defaultMessage='You must enter an organization name'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case TeamApiError:
|
||||
children = (
|
||||
<FormattedMessage
|
||||
id='onboarding_wizard.organization.team_api_error'
|
||||
defaultMessage='There was an error, please try again.'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case BadUrlReasons.Length:
|
||||
children = (
|
||||
<FormattedMessage
|
||||
id='onboarding_wizard.organization.length'
|
||||
defaultMessage='Organization name must be between {min} and {max} characters'
|
||||
values={{
|
||||
min: Constants.MIN_TEAMNAME_LENGTH,
|
||||
max: Constants.MAX_TEAMNAME_LENGTH,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case BadUrlReasons.Reserved:
|
||||
children = (
|
||||
<FormattedMessage
|
||||
|
||||
id='onboarding_wizard.organization.reserved'
|
||||
defaultMessage='Organization name may not <a>start with a reserved word</a>.'
|
||||
values={{
|
||||
a: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<ExternalLink
|
||||
href={DocLinks.ABOUT_TEAMS}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
{chunks}
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
children = (
|
||||
<FormattedMessage
|
||||
id='onboarding_wizard.organization.other'
|
||||
defaultMessage='Invalid organization name: {reason}'
|
||||
values={{
|
||||
reason: props.error,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return <div className={className}>{children}</div>;
|
||||
};
|
||||
|
||||
export default OrganizationStatus;
|
||||
@@ -0,0 +1,10 @@
|
||||
.PageLine {
|
||||
position: relative;
|
||||
left: 100px;
|
||||
width: 1px;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.24);
|
||||
|
||||
&--no-left {
|
||||
left: initial;
|
||||
}
|
||||
}
|
||||
35
webapp/channels/src/components/preparing_workspace/page_line.tsx
Обычный файл
35
webapp/channels/src/components/preparing_workspace/page_line.tsx
Обычный файл
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import './page_line.scss';
|
||||
|
||||
type Props = {
|
||||
style?: Record<string, string>;
|
||||
noLeft?: boolean;
|
||||
}
|
||||
const PageLine = (props: Props) => {
|
||||
let className = 'PageLine';
|
||||
if (props.noLeft) {
|
||||
className += ' PageLine--no-left';
|
||||
}
|
||||
const styles: Record<string, string> = {};
|
||||
if (props?.style) {
|
||||
Object.assign(styles, props.style);
|
||||
}
|
||||
if (!styles.height) {
|
||||
styles.height = '100vh';
|
||||
}
|
||||
if ((!props.style?.height && styles.height === '100vh') && !styles.marginTop) {
|
||||
styles.marginTop = '50px';
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={styles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageLine;
|
||||
@@ -4,6 +4,9 @@
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.plugins-skip-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
// preempt cards wrapping
|
||||
@media screen and (max-width: 900px) {
|
||||
.Plugins-body {
|
||||
|
||||
@@ -21,15 +21,16 @@ import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps
|
||||
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 './plugins.scss';
|
||||
|
||||
type Props = PreparingWorkspacePageProps & {
|
||||
options: Form['plugins'];
|
||||
setOption: (option: keyof Form['plugins']) => void;
|
||||
className?: string;
|
||||
isSelfHosted: boolean;
|
||||
}
|
||||
const Plugins = (props: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
@@ -44,6 +45,34 @@ const Plugins = (props: Props) => {
|
||||
if (props.className) {
|
||||
className += ' ' + props.className;
|
||||
}
|
||||
|
||||
let title = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.cloud_plugins.title'}
|
||||
defaultMessage='Welcome to Mattermost!'
|
||||
/>
|
||||
);
|
||||
let description = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.cloud_plugins.description'}
|
||||
defaultMessage={'Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we\'ll add them to your workspace. Additional set up may be needed later.'}
|
||||
/>
|
||||
);
|
||||
if (props.isSelfHosted) {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.self_hosted_plugins.title'}
|
||||
defaultMessage='What tools do you use?'
|
||||
/>
|
||||
);
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.self_hosted_plugins.description'}
|
||||
defaultMessage={'Choose the tools you work with, and we\'ll add them to your workspace. Additional set up may be needed later.'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
in={props.show}
|
||||
@@ -54,26 +83,29 @@ const Plugins = (props: Props) => {
|
||||
>
|
||||
<div className={className}>
|
||||
<SingleColumnLayout>
|
||||
<PageLine
|
||||
style={{
|
||||
marginBottom: '50px',
|
||||
marginLeft: '50px',
|
||||
height: 'calc(25vh)',
|
||||
}}
|
||||
noLeft={true}
|
||||
/>
|
||||
{props.previous}
|
||||
<Title>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.plugins.title'}
|
||||
defaultMessage='Welcome to Mattermost!'
|
||||
/>
|
||||
<div className='subtitle'>
|
||||
<CelebrateSVG/>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.plugins.subtitle'}
|
||||
defaultMessage='(almost there!)'
|
||||
/>
|
||||
</div>
|
||||
{title}
|
||||
{!props.isSelfHosted && (
|
||||
<div className='subtitle'>
|
||||
<CelebrateSVG/>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.cloud_plugins.subtitle'}
|
||||
defaultMessage='(almost there!)'
|
||||
/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Title>
|
||||
<Description>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.plugins.description'}
|
||||
defaultMessage={'Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we\'ll add them to your workspace. Additional set up may be needed later.'}
|
||||
/>
|
||||
</Description>
|
||||
<Description>{description}</Description>
|
||||
<PageBody>
|
||||
<MultiSelectCards
|
||||
size='small'
|
||||
@@ -166,15 +198,23 @@ const Plugins = (props: Props) => {
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className='tertiary-button'
|
||||
className='link-style plugins-skip-btn'
|
||||
onClick={props.skip}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.skip'}
|
||||
defaultMessage='Skip for now'
|
||||
id={'onboarding_wizard.skip-button'}
|
||||
defaultMessage='Skip'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<PageLine
|
||||
style={{
|
||||
marginTop: '50px',
|
||||
marginLeft: '50px',
|
||||
height: 'calc(30vh)',
|
||||
}}
|
||||
noLeft={true}
|
||||
/>
|
||||
</SingleColumnLayout>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
|
||||
@@ -63,6 +63,21 @@
|
||||
.primary-button {
|
||||
@include primary-button;
|
||||
@include button-medium;
|
||||
|
||||
box-sizing: border-box;
|
||||
border: 2px solid var(--button-bg);
|
||||
}
|
||||
|
||||
.primary-button[disabled] {
|
||||
box-sizing: border-box;
|
||||
border: 2px solid rgba(var(--center-channel-color-rgb), 0.01);
|
||||
}
|
||||
|
||||
.link-style {
|
||||
@include link;
|
||||
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.child-page {
|
||||
@@ -70,6 +85,43 @@
|
||||
position: absolute;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&__invite-members-illustration {
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
right: -651px;
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.enter {
|
||||
animation-name: slideInRight;
|
||||
}
|
||||
|
||||
.exit {
|
||||
animation-name: slideOutRight;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
right: -651px;
|
||||
}
|
||||
|
||||
to {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
right: -651px;
|
||||
}
|
||||
}
|
||||
|
||||
.PreparingWorkspacePageContainer {
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useCallback, useEffect, useRef} from 'react';
|
||||
import React, {useState, useCallback, useEffect, useRef, useMemo} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {RouterProps} from 'react-router-dom';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getFirstAdminSetupComplete as getFirstAdminSetupCompleteAction} from 'mattermost-redux/actions/general';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
import {getUseCaseOnboarding} 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} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getFirstAdminSetupComplete, getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {getSiteURL, teamNameToUrl} from 'utils/url';
|
||||
import {makeNewTeam} from 'utils/team_utils';
|
||||
|
||||
import {pageVisited, trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
@@ -35,10 +36,14 @@ import {
|
||||
mapStepToPageView,
|
||||
mapStepToSubmitFail,
|
||||
PLUGIN_NAME_TO_ID_MAP,
|
||||
mapStepToPrevious,
|
||||
} from './steps';
|
||||
|
||||
import Organization from './organization';
|
||||
import Plugins from './plugins';
|
||||
import Progress from './progress';
|
||||
import InviteMembers from './invite_members';
|
||||
import InviteMembersIllustration from './invite_members_illustration';
|
||||
import LaunchingWorkspace, {START_TRANSITIONING_OUT} from './launching_workspace';
|
||||
|
||||
import './preparing_workspace.scss';
|
||||
@@ -58,6 +63,7 @@ const WAIT_FOR_REDIRECT_TIME = 2000 - START_TRANSITIONING_OUT;
|
||||
|
||||
export type Actions = {
|
||||
createTeam: (team: Team) => ActionResult;
|
||||
updateTeam: (team: Team) => ActionResult;
|
||||
checkIfTeamExists: (teamName: string) => ActionResult;
|
||||
getProfiles: (page: number, perPage: number, options: Record<string, any>) => ActionResult;
|
||||
}
|
||||
@@ -81,12 +87,16 @@ function makeSubmitFail(step: WizardStep) {
|
||||
}
|
||||
|
||||
const trackSubmitFail = {
|
||||
[WizardSteps.Organization]: makeSubmitFail(WizardSteps.Organization),
|
||||
[WizardSteps.Plugins]: makeSubmitFail(WizardSteps.Plugins),
|
||||
[WizardSteps.InviteMembers]: makeSubmitFail(WizardSteps.InviteMembers),
|
||||
[WizardSteps.LaunchingWorkspace]: makeSubmitFail(WizardSteps.LaunchingWorkspace),
|
||||
};
|
||||
|
||||
const onPageViews = {
|
||||
[WizardSteps.Organization]: makeOnPageView(WizardSteps.Organization),
|
||||
[WizardSteps.Plugins]: makeOnPageView(WizardSteps.Plugins),
|
||||
[WizardSteps.InviteMembers]: makeOnPageView(WizardSteps.InviteMembers),
|
||||
[WizardSteps.LaunchingWorkspace]: makeOnPageView(WizardSteps.LaunchingWorkspace),
|
||||
};
|
||||
|
||||
@@ -98,28 +108,35 @@ const PreparingWorkspace = (props: Props) => {
|
||||
defaultMessage: 'Something went wrong. Please try again.',
|
||||
});
|
||||
const isUserFirstAdmin = useSelector(isFirstAdmin);
|
||||
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
const myTeams = useSelector(getMyTeams);
|
||||
|
||||
// In cloud instances created from portal,
|
||||
// new admin user has a team in myTeams but not in currentTeam.
|
||||
const team = currentTeam || myTeams?.[0];
|
||||
let team = currentTeam || myTeams?.[0];
|
||||
|
||||
const config = useSelector(getConfig);
|
||||
const pluginsEnabled = config.PluginsEnabled === 'true';
|
||||
const showOnMountTimeout = useRef<NodeJS.Timeout>();
|
||||
const configSiteUrl = config.SiteURL;
|
||||
const isSelfHosted = useSelector(getLicense).Cloud !== 'true';
|
||||
|
||||
const stepOrder = [
|
||||
isSelfHosted && WizardSteps.Organization,
|
||||
pluginsEnabled && WizardSteps.Plugins,
|
||||
isSelfHosted && WizardSteps.InviteMembers,
|
||||
WizardSteps.LaunchingWorkspace,
|
||||
].filter((x) => Boolean(x)) as WizardStep[];
|
||||
|
||||
// first steporder that is not false
|
||||
const firstShowablePage = stepOrder[0];
|
||||
|
||||
const firstAdminSetupComplete = useSelector(getFirstAdminSetupComplete);
|
||||
|
||||
const [[mostRecentStep, currentStep], setStepHistory] = useState<[WizardStep, WizardStep]>([stepOrder[0], stepOrder[0]]);
|
||||
const [submissionState, setSubmissionState] = useState<SubmissionState>(SubmissionStates.Presubmit);
|
||||
const browserSiteUrl = useMemo(getSiteURL, []);
|
||||
const [form, setForm] = useState({
|
||||
...emptyForm,
|
||||
});
|
||||
@@ -188,13 +205,44 @@ const PreparingWorkspace = (props: Props) => {
|
||||
trackSubmitFail[redirectTo]();
|
||||
}, []);
|
||||
|
||||
const createTeam = async (OrganizationName: string): Promise<{error: string | null; newTeam: Team | null}> => {
|
||||
const data = await props.actions.createTeam(makeNewTeam(OrganizationName, teamNameToUrl(OrganizationName || '').url));
|
||||
if (data.error) {
|
||||
return {error: genericSubmitError, newTeam: null};
|
||||
}
|
||||
return {error: null, newTeam: data.data};
|
||||
};
|
||||
|
||||
const updateTeam = async (teamToUpdate: Team): Promise<{error: string | null; updatedTeam: Team | null}> => {
|
||||
const data = await props.actions.updateTeam(teamToUpdate);
|
||||
if (data.error) {
|
||||
return {error: genericSubmitError, updatedTeam: null};
|
||||
}
|
||||
return {error: null, updatedTeam: data.data};
|
||||
};
|
||||
|
||||
const sendForm = async () => {
|
||||
const sendFormStart = Date.now();
|
||||
setSubmissionState(SubmissionStates.Submitting);
|
||||
|
||||
if (form.organization && !isSelfHosted) {
|
||||
try {
|
||||
const {error, newTeam} = await createTeam(form.organization);
|
||||
if (error !== null) {
|
||||
redirectWithError(WizardSteps.Organization, genericSubmitError);
|
||||
return;
|
||||
}
|
||||
team = newTeam as Team;
|
||||
} catch (e) {
|
||||
redirectWithError(WizardSteps.Organization, genericSubmitError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// send plugins
|
||||
const {skipped: skippedPlugins, ...pluginChoices} = form.plugins;
|
||||
let pluginsToSetup: string[] = [];
|
||||
|
||||
if (!skippedPlugins) {
|
||||
pluginsToSetup = Object.entries(pluginChoices).reduce(
|
||||
(acc: string[], [k, v]): string[] => (v ? [...acc, PLUGIN_NAME_TO_ID_MAP[k as keyof Omit<Form['plugins'], 'skipped'>]] : acc), [],
|
||||
@@ -204,8 +252,10 @@ const PreparingWorkspace = (props: Props) => {
|
||||
// This endpoint sets setup complete state, so we need to make this request
|
||||
// even if admin skipped submitting plugins.
|
||||
const completeSetupRequest = {
|
||||
organization: form.organization,
|
||||
install_plugins: pluginsToSetup,
|
||||
};
|
||||
|
||||
try {
|
||||
await Client4.completeSetup(completeSetupRequest);
|
||||
dispatch({type: GeneralTypes.FIRST_ADMIN_COMPLETE_SETUP_RECEIVED, data: true});
|
||||
@@ -221,6 +271,7 @@ const PreparingWorkspace = (props: Props) => {
|
||||
|
||||
const sendFormEnd = Date.now();
|
||||
const timeToWait = WAIT_FOR_REDIRECT_TIME - (sendFormEnd - sendFormStart);
|
||||
|
||||
if (timeToWait > 0) {
|
||||
setTimeout(goToChannels, timeToWait);
|
||||
} else {
|
||||
@@ -236,7 +287,8 @@ const PreparingWorkspace = (props: Props) => {
|
||||
}, [submissionState]);
|
||||
|
||||
const adminRevisitedPage = firstAdminSetupComplete && submissionState === SubmissionStates.Presubmit;
|
||||
const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage || !useCaseOnboarding;
|
||||
const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage;
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldRedirect) {
|
||||
props.history.push('/');
|
||||
@@ -256,6 +308,24 @@ const PreparingWorkspace = (props: Props) => {
|
||||
return stepIndex > currentStepIndex ? Animations.Reasons.ExitToBefore : Animations.Reasons.ExitToAfter;
|
||||
};
|
||||
|
||||
const goPrevious = useCallback((e?: React.KeyboardEvent | React.MouseEvent) => {
|
||||
if (e && (e as React.KeyboardEvent).key) {
|
||||
const key = (e as React.KeyboardEvent).key;
|
||||
if (key !== Constants.KeyCodes.ENTER[0] && key !== Constants.KeyCodes.SPACE[0]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (submissionState !== SubmissionStates.Presubmit && submissionState !== SubmissionStates.SubmitFail) {
|
||||
return;
|
||||
}
|
||||
const stepIndex = stepOrder.indexOf(currentStep);
|
||||
if (stepIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
trackEvent('first_admin_setup', mapStepToPrevious(currentStep));
|
||||
setStepHistory([currentStep, stepOrder[stepIndex - 1]]);
|
||||
}, [currentStep]);
|
||||
|
||||
const skipPlugins = useCallback((skipped: boolean) => {
|
||||
if (skipped === form.plugins.skipped) {
|
||||
return;
|
||||
@@ -269,6 +339,46 @@ const PreparingWorkspace = (props: Props) => {
|
||||
});
|
||||
}, [form]);
|
||||
|
||||
const skipTeamMembers = useCallback((skipped: boolean) => {
|
||||
if (skipped === form.teamMembers.skipped) {
|
||||
return;
|
||||
}
|
||||
setForm({
|
||||
...form,
|
||||
teamMembers: {
|
||||
...form.teamMembers,
|
||||
skipped,
|
||||
},
|
||||
});
|
||||
}, [form]);
|
||||
|
||||
const getInviteMembersAnimationClass = useCallback(() => {
|
||||
if (currentStep === WizardSteps.InviteMembers) {
|
||||
return 'enter';
|
||||
} else if (mostRecentStep === WizardSteps.InviteMembers) {
|
||||
return 'exit';
|
||||
}
|
||||
return '';
|
||||
}, [currentStep]);
|
||||
|
||||
let previous: React.ReactNode = (
|
||||
<div
|
||||
onClick={goPrevious}
|
||||
onKeyUp={goPrevious}
|
||||
tabIndex={0}
|
||||
className='PreparingWorkspace__previous'
|
||||
>
|
||||
<i className='icon-chevron-up'/>
|
||||
<FormattedMessage
|
||||
id={'onboarding_wizard.previous'}
|
||||
defaultMessage='Previous'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
if (currentStep === firstShowablePage) {
|
||||
previous = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='PreparingWorkspace PreparingWorkspaceContainer'>
|
||||
{submissionState === SubmissionStates.SubmitFail && submitError && (
|
||||
@@ -291,17 +401,49 @@ const PreparingWorkspace = (props: Props) => {
|
||||
transitionSpeed={Animations.PAGE_SLIDE}
|
||||
/>
|
||||
<div className='PreparingWorkspacePageContainer'>
|
||||
<Organization
|
||||
onPageView={onPageViews[WizardSteps.Organization]}
|
||||
show={shouldShowPage(WizardSteps.Organization)}
|
||||
next={makeNext(WizardSteps.Organization)}
|
||||
transitionDirection={getTransitionDirection(WizardSteps.Organization)}
|
||||
organization={form.organization || ''}
|
||||
setOrganization={(organization: Form['organization']) => {
|
||||
setForm({
|
||||
...form,
|
||||
organization,
|
||||
});
|
||||
}}
|
||||
setInviteId={(inviteId: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
teamMembers: {
|
||||
...form.teamMembers,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className='child-page'
|
||||
createTeam={createTeam}
|
||||
updateTeam={updateTeam}
|
||||
/>
|
||||
|
||||
<Plugins
|
||||
isSelfHosted={isSelfHosted}
|
||||
onPageView={onPageViews[WizardSteps.Plugins]}
|
||||
previous={previous}
|
||||
next={() => {
|
||||
const pluginChoices = {...form.plugins};
|
||||
delete pluginChoices.skipped;
|
||||
setSubmissionState(SubmissionStates.UserRequested);
|
||||
if (!isSelfHosted) {
|
||||
setSubmissionState(SubmissionStates.UserRequested);
|
||||
}
|
||||
makeNext(WizardSteps.Plugins)(pluginChoices);
|
||||
skipPlugins(false);
|
||||
}}
|
||||
skip={() => {
|
||||
setSubmissionState(SubmissionStates.UserRequested);
|
||||
if (!isSelfHosted) {
|
||||
setSubmissionState(SubmissionStates.UserRequested);
|
||||
}
|
||||
makeNext(WizardSteps.Plugins, true)();
|
||||
skipPlugins(true);
|
||||
}}
|
||||
@@ -319,12 +461,40 @@ const PreparingWorkspace = (props: Props) => {
|
||||
transitionDirection={getTransitionDirection(WizardSteps.Plugins)}
|
||||
className='child-page'
|
||||
/>
|
||||
<InviteMembers
|
||||
onPageView={onPageViews[WizardSteps.InviteMembers]}
|
||||
next={() => {
|
||||
skipTeamMembers(false);
|
||||
const inviteMembersTracking = {
|
||||
inviteCount: form.teamMembers.invites.length,
|
||||
};
|
||||
setSubmissionState(SubmissionStates.UserRequested);
|
||||
makeNext(WizardSteps.InviteMembers)(inviteMembersTracking);
|
||||
}}
|
||||
skip={() => {
|
||||
skipTeamMembers(true);
|
||||
setSubmissionState(SubmissionStates.UserRequested);
|
||||
makeNext(WizardSteps.InviteMembers, true)();
|
||||
}}
|
||||
previous={previous}
|
||||
show={shouldShowPage(WizardSteps.InviteMembers)}
|
||||
transitionDirection={getTransitionDirection(WizardSteps.InviteMembers)}
|
||||
disableEdits={submissionState !== SubmissionStates.Presubmit && submissionState !== SubmissionStates.SubmitFail}
|
||||
className='child-page'
|
||||
teamInviteId={team?.invite_id || form.teamMembers.inviteId}
|
||||
configSiteUrl={configSiteUrl}
|
||||
formUrl={form.url}
|
||||
browserSiteUrl={browserSiteUrl}
|
||||
/>
|
||||
<LaunchingWorkspace
|
||||
onPageView={onPageViews[WizardSteps.LaunchingWorkspace]}
|
||||
show={currentStep === WizardSteps.LaunchingWorkspace}
|
||||
transitionDirection={getTransitionDirection(WizardSteps.LaunchingWorkspace)}
|
||||
/>
|
||||
</div>
|
||||
<div className={`PreparingWorkspace__invite-members-illustration ${getInviteMembersAnimationClass()}`}>
|
||||
<InviteMembersIllustration/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,5 +4,4 @@
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
import deepFreeze from 'mattermost-redux/utils/deep_freeze';
|
||||
|
||||
export const WizardSteps = {
|
||||
Organization: 'Organization',
|
||||
Plugins: 'Plugins',
|
||||
InviteMembers: 'InviteMembers',
|
||||
LaunchingWorkspace: 'LaunchingWorkspace',
|
||||
} as const;
|
||||
|
||||
@@ -20,8 +22,12 @@ export const Animations = {
|
||||
|
||||
export function mapStepToNextName(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'admin_onboarding_next_organization';
|
||||
case WizardSteps.Plugins:
|
||||
return 'admin_onboarding_next_plugins';
|
||||
case WizardSteps.InviteMembers:
|
||||
return 'admin_onboarding_next_invite_members';
|
||||
case WizardSteps.LaunchingWorkspace:
|
||||
return 'admin_onboarding_next_transitioning_out';
|
||||
default:
|
||||
@@ -31,8 +37,12 @@ export function mapStepToNextName(step: WizardStep): string {
|
||||
|
||||
export function mapStepToPrevious(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'admin_onboarding_previous_organization';
|
||||
case WizardSteps.Plugins:
|
||||
return 'admin_onboarding_previous_plugins';
|
||||
case WizardSteps.InviteMembers:
|
||||
return 'admin_onboarding_previous_invite_members';
|
||||
case WizardSteps.LaunchingWorkspace:
|
||||
return 'admin_onboarding_previous_transitioning_out';
|
||||
default:
|
||||
@@ -42,8 +52,12 @@ export function mapStepToPrevious(step: WizardStep): string {
|
||||
|
||||
export function mapStepToPageView(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'pageview_admin_onboarding_organization';
|
||||
case WizardSteps.Plugins:
|
||||
return 'pageview_admin_onboarding_plugins';
|
||||
case WizardSteps.InviteMembers:
|
||||
return 'pageview_admin_onboarding_invite_members';
|
||||
case WizardSteps.LaunchingWorkspace:
|
||||
return 'pageview_admin_onboarding_transitioning_out';
|
||||
default:
|
||||
@@ -53,8 +67,12 @@ export function mapStepToPageView(step: WizardStep): string {
|
||||
|
||||
export function mapStepToSubmitFail(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'admin_onboarding_organization_submit_fail';
|
||||
case WizardSteps.Plugins:
|
||||
return 'admin_onboarding_plugins_submit_fail';
|
||||
case WizardSteps.InviteMembers:
|
||||
return 'admin_onboarding_invite_members_submit_fail';
|
||||
case WizardSteps.LaunchingWorkspace:
|
||||
return 'admin_onboarding_transitioning_out_submit_fail';
|
||||
default:
|
||||
@@ -64,8 +82,12 @@ export function mapStepToSubmitFail(step: WizardStep): string {
|
||||
|
||||
export function mapStepToSkipName(step: WizardStep): string {
|
||||
switch (step) {
|
||||
case WizardSteps.Organization:
|
||||
return 'admin_onboarding_skip_organization';
|
||||
case WizardSteps.Plugins:
|
||||
return 'admin_onboarding_skip_plugins';
|
||||
case WizardSteps.InviteMembers:
|
||||
return 'admin_onboarding_skip_invite_members';
|
||||
case WizardSteps.LaunchingWorkspace:
|
||||
return 'admin_onboarding_skip_transitioning_out';
|
||||
default:
|
||||
@@ -128,12 +150,14 @@ export type Form = {
|
||||
skipped: boolean;
|
||||
};
|
||||
teamMembers: {
|
||||
inviteId: string;
|
||||
invites: string[];
|
||||
skipped: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const emptyForm = deepFreeze({
|
||||
organization: '',
|
||||
inferredProtocol: null,
|
||||
urlSkipped: false,
|
||||
useCase: {
|
||||
@@ -156,6 +180,7 @@ export const emptyForm = deepFreeze({
|
||||
skipped: false,
|
||||
},
|
||||
teamMembers: {
|
||||
inviteId: '',
|
||||
invites: [],
|
||||
skipped: false,
|
||||
},
|
||||
@@ -165,7 +190,7 @@ export type PreparingWorkspacePageProps = {
|
||||
transitionDirection: AnimationReason;
|
||||
next?: () => void;
|
||||
skip?: () => void;
|
||||
previous?: JSX.Element;
|
||||
previous?: React.ReactNode;
|
||||
show: boolean;
|
||||
onPageView: () => void;
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ function getDefaultChannelId(state: GlobalState) {
|
||||
return selectedPost.exists ? selectedPost.channel_id : getCurrentChannelId(state);
|
||||
}
|
||||
|
||||
function checkUserInCall(state: GlobalState, userId: string) {
|
||||
export function checkUserInCall(state: GlobalState, userId: string) {
|
||||
let isUserInCall = false;
|
||||
|
||||
const calls = getCalls(state);
|
||||
Object.keys(calls).forEach((channelId) => {
|
||||
const usersInCall = calls[channelId];
|
||||
const usersInCall = calls[channelId] || [];
|
||||
|
||||
for (const user of usersInCall) {
|
||||
if (user.id === userId) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {General} from 'mattermost-redux/constants';
|
||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||
|
||||
import ProfilePopover from 'components/profile_popover/profile_popover';
|
||||
import {checkUserInCall} from 'components/profile_popover';
|
||||
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
|
||||
@@ -284,3 +285,52 @@ describe('components/ProfilePopover', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkUserInCall', () => {
|
||||
test('missing state', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {},
|
||||
} as any, 'userA')).toBe(false);
|
||||
});
|
||||
|
||||
test('call state missing', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {
|
||||
voiceConnectedProfiles: {
|
||||
channelID: null,
|
||||
},
|
||||
},
|
||||
} as any, 'userA')).toBe(false);
|
||||
});
|
||||
|
||||
test('user not in call', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {
|
||||
voiceConnectedProfiles: {
|
||||
channelID: [
|
||||
{
|
||||
id: 'userB',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as any, 'userA')).toBe(false);
|
||||
});
|
||||
|
||||
test('user in call', () => {
|
||||
expect(checkUserInCall({
|
||||
'plugins-com.mattermost.calls': {
|
||||
voiceConnectedProfiles: {
|
||||
channelID: [
|
||||
{
|
||||
id: 'userB',
|
||||
},
|
||||
{
|
||||
id: 'userA',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as any, 'userA')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import classNames from 'classnames';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {Theme, getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUser, isCurrentUserSystemAdmin, checkIsFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {setUrl} from 'mattermost-redux/actions/general';
|
||||
@@ -89,6 +89,8 @@ import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import WelcomePostRenderer from 'components/welcome_post_renderer';
|
||||
|
||||
import {getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {applyLuxonDefaults} from './effects';
|
||||
|
||||
import RootProvider from './root_provider';
|
||||
@@ -358,8 +360,8 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
const useCaseOnboarding = getUseCaseOnboarding(storeState);
|
||||
if (!useCaseOnboarding) {
|
||||
const myTeams = getMyTeams(storeState);
|
||||
if (myTeams.length > 0) {
|
||||
GlobalActions.redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {getFirstAdminSetupComplete} from 'mattermost-redux/actions/general';
|
||||
import {getCurrentUserId, isCurrentUserSystemAdmin, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
@@ -14,11 +13,7 @@ import {GlobalState} from 'types/store';
|
||||
import RootRedirect, {Props} from './root_redirect';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const useCaseOnboarding = getUseCaseOnboarding(state);
|
||||
let isElegibleForFirstAdmingOnboarding = useCaseOnboarding;
|
||||
if (isElegibleForFirstAdmingOnboarding) {
|
||||
isElegibleForFirstAdmingOnboarding = isCurrentUserSystemAdmin(state);
|
||||
}
|
||||
const isElegibleForFirstAdmingOnboarding = isCurrentUserSystemAdmin(state);
|
||||
return {
|
||||
currentUserId: getCurrentUserId(state),
|
||||
isElegibleForFirstAdmingOnboarding,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.SelfHostedExpansionRHSCard {
|
||||
display: flex;
|
||||
max-width: 280px;
|
||||
width: 280px;
|
||||
flex-direction: column;
|
||||
|
||||
&__Content {
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 24px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -13,6 +13,8 @@ import {ValueOf} from '@mattermost/types/utilities';
|
||||
import CreditCardSvg from 'components/common/svg_images_components/credit_card_svg';
|
||||
import IconMessage from 'components/purchase_modal/icon_message';
|
||||
|
||||
import './submitting_page.scss';
|
||||
|
||||
function useConvertProgressToWaitingExplanation(progress: ValueOf<typeof SelfHostedSignupProgress>, planName: string): React.ReactNode {
|
||||
const intl = useIntl();
|
||||
switch (progress) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.submitting {
|
||||
overflow: hidden;
|
||||
|
||||
.processing {
|
||||
margin-top: 163px;
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,9 @@
|
||||
}
|
||||
|
||||
.self_hosted_expansion_success {
|
||||
margin-top: 163px;
|
||||
overflow: hidden;
|
||||
|
||||
.selfHostedExpansionModal__success {
|
||||
margin-top: 163px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {useHistory} from 'react-router-dom';
|
||||
|
||||
import IconMessage from 'components/purchase_modal/icon_message';
|
||||
import PaymentSuccessStandardSvg from 'components/common/svg_images_components/payment_success_standard_svg';
|
||||
import BackgroundSvg from 'components/common/svg_images_components/background_svg';
|
||||
|
||||
import {ConsolePages} from 'utils/constants';
|
||||
|
||||
@@ -71,9 +70,6 @@ export default function SelfHostedExpansionSuccessPage(props: Props) {
|
||||
formattedButtonText={formattedButtonText}
|
||||
buttonHandler={props.onClose}
|
||||
/>
|
||||
<div className='background-svg'>
|
||||
<BackgroundSvg/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import {IntlProvider} from 'react-intl';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
import {act, screen} from '@testing-library/react';
|
||||
|
||||
import * as global_actions from 'actions/global_actions';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
import Signup from 'components/signup/signup';
|
||||
@@ -197,9 +195,6 @@ describe('components/signup/Signup', () => {
|
||||
mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser
|
||||
mockResolvedValueOnce({error: {server_error_id: 'api.user.login.not_verified.app_error'}}); // loginById
|
||||
|
||||
const mockRedirectUserToDefaultTeam = jest.fn();
|
||||
jest.spyOn(global_actions, 'redirectUserToDefaultTeam').mockImplementation(mockRedirectUserToDefaultTeam);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<IntlProvider {...intlProviderProps}>
|
||||
<BrowserRouter>
|
||||
@@ -228,7 +223,6 @@ describe('components/signup/Signup', () => {
|
||||
expect(wrapper.find('#input_name').first().props().disabled).toEqual(true);
|
||||
expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true);
|
||||
|
||||
expect(mockRedirectUserToDefaultTeam).not.toHaveBeenCalled();
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith('/should_verify_email?email=jdoe%40mm.com&teamname=teamName');
|
||||
});
|
||||
|
||||
@@ -238,9 +232,6 @@ describe('components/signup/Signup', () => {
|
||||
mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser
|
||||
mockResolvedValueOnce({}); // loginById
|
||||
|
||||
const mockRedirectUserToDefaultTeam = jest.fn();
|
||||
jest.spyOn(global_actions, 'redirectUserToDefaultTeam').mockImplementation(mockRedirectUserToDefaultTeam);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<IntlProvider {...intlProviderProps}>
|
||||
<BrowserRouter>
|
||||
@@ -268,8 +259,6 @@ describe('components/signup/Signup', () => {
|
||||
expect(wrapper.find(Input).first().props().disabled).toEqual(true);
|
||||
expect(wrapper.find('#input_name').first().props().disabled).toEqual(true);
|
||||
expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true);
|
||||
|
||||
expect(mockRedirectUserToDefaultTeam).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add user to team and redirect when team invite valid and logged in', async () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {getTeamInviteInfo} from 'mattermost-redux/actions/teams';
|
||||
import {createUser, loadMe, loadMeREST} from 'mattermost-redux/actions/users';
|
||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getUseCaseOnboarding, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
@@ -25,7 +25,6 @@ import {GlobalState} from 'types/store';
|
||||
|
||||
import {getGlobalItem} from 'selectors/storage';
|
||||
|
||||
import {redirectUserToDefaultTeam} from 'actions/global_actions';
|
||||
import {removeGlobalItem, setGlobalItem} from 'actions/storage';
|
||||
import {addUserToTeamFromInvite} from 'actions/team_actions';
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
@@ -104,7 +103,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
} = config;
|
||||
const {IsLicensed, Cloud} = useSelector(getLicense);
|
||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||
const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined));
|
||||
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
||||
|
||||
@@ -310,15 +308,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
} else if (inviteId) {
|
||||
getInviteInfo(inviteId);
|
||||
} else if (loggedIn) {
|
||||
if (useCaseOnboarding) {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first tiem onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
} else {
|
||||
redirectUserToDefaultTeam();
|
||||
}
|
||||
history.push('/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,14 +451,12 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
|
||||
if (redirectTo) {
|
||||
history.push(redirectTo);
|
||||
} else if (useCaseOnboarding) {
|
||||
} else {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first tiem onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
history.push('/');
|
||||
} else {
|
||||
redirectUserToDefaultTeam();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
const currentUser = getCurrentUser(state);
|
||||
const plugins = state.plugins.components.NeedsTeamComponent;
|
||||
const graphQLEnabled = isGraphQLEnabled(state);
|
||||
const disableRefetchingOnBrowserFocus = config.DisableRefetchingOnBrowserFocus === 'true';
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
@@ -46,6 +47,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
selectedThreadId: getSelectedThreadIdInCurrentTeam(state),
|
||||
mfaRequired: checkIfMFARequired(currentUser, license, config, ownProps.match.url),
|
||||
graphQLEnabled,
|
||||
disableRefetchingOnBrowserFocus,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ function TeamController(props: Props) {
|
||||
const wakeUpIntervalId = setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
if ((currentTime - lastTime.current) > WAKEUP_THRESHOLD) {
|
||||
console.log('computer woke up - fetching latest'); //eslint-disable-line no-console
|
||||
console.log('computer woke up - reconnecting'); //eslint-disable-line no-console
|
||||
reconnect();
|
||||
}
|
||||
lastTime.current = currentTime;
|
||||
@@ -92,12 +92,15 @@ function TeamController(props: Props) {
|
||||
props.markChannelAsReadOnFocus(props.currentChannelId);
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
if ((currentTime - blurTime.current) > UNREAD_CHECK_TIME_MILLISECONDS && props.currentTeamId) {
|
||||
if (props.graphQLEnabled) {
|
||||
props.fetchChannelsAndMembers(props.currentTeamId);
|
||||
} else {
|
||||
props.fetchMyChannelsAndMembersREST(props.currentTeamId);
|
||||
// Temporary flag to disable refetching of channel members on browser focus
|
||||
if (!props.disableRefetchingOnBrowserFocus) {
|
||||
const currentTime = Date.now();
|
||||
if ((currentTime - blurTime.current) > UNREAD_CHECK_TIME_MILLISECONDS && props.currentTeamId) {
|
||||
if (props.graphQLEnabled) {
|
||||
props.fetchChannelsAndMembers(props.currentTeamId);
|
||||
} else {
|
||||
props.fetchMyChannelsAndMembersREST(props.currentTeamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
|
||||
import {getTermsOfService, updateMyTermsOfServiceStatus} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
|
||||
@@ -26,9 +25,7 @@ type Actions = {
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
const useCaseOnboarding = getUseCaseOnboarding(state);
|
||||
return {
|
||||
useCaseOnboarding,
|
||||
termsEnabled: config.EnableCustomTermsOfService === 'true',
|
||||
emojiMap: getEmojiMap(state),
|
||||
};
|
||||
|
||||
@@ -27,7 +27,6 @@ describe('components/terms_of_service/TermsOfService', () => {
|
||||
location: {search: ''},
|
||||
termsEnabled: true,
|
||||
emojiMap: {} as EmojiMap,
|
||||
useCaseOnboarding: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -38,7 +38,6 @@ export interface TermsOfServiceProps {
|
||||
) => {data: UpdateMyTermsOfServiceStatusResponse};
|
||||
};
|
||||
emojiMap: EmojiMap;
|
||||
useCaseOnboarding: boolean;
|
||||
}
|
||||
|
||||
interface TermsOfServiceState {
|
||||
@@ -111,14 +110,12 @@ export default class TermsOfService extends React.PureComponent<TermsOfServicePr
|
||||
const redirectTo = query.get('redirect_to');
|
||||
if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) {
|
||||
getHistory().push(redirectTo);
|
||||
} else if (this.props.useCaseOnboarding) {
|
||||
} else {
|
||||
// need info about whether admin or not,
|
||||
// and whether admin has already completed
|
||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||
// let the default root component handle it.
|
||||
getHistory().push('/');
|
||||
} else {
|
||||
GlobalActions.redirectUserToDefaultTeam();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -165,7 +165,8 @@ const StyledCustomized = styled(Customize)`
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(var(--center-channel-text-rgb), 0.16);
|
||||
&:focus {
|
||||
border: 2px solid var(--button-bg);
|
||||
border: 1px solid var(--button-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
||||
|
||||
const [integrations, setIntegrations] = useState<Integration[]>();
|
||||
|
||||
const plugins: MarketplacePlugin[] = useSelector((state: GlobalState) => state.views.marketplace.plugins);
|
||||
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);
|
||||
@@ -130,13 +131,14 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
||||
const intg =
|
||||
availableIntegrations?.
|
||||
flatMap((integration) => {
|
||||
return plugins.reduce((acc: Integration[], curr) => {
|
||||
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: curr.installed_version !== '',
|
||||
installed,
|
||||
});
|
||||
|
||||
return acc;
|
||||
@@ -149,7 +151,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
||||
if (intg?.length) {
|
||||
setIntegrations(intg);
|
||||
}
|
||||
}, [plugins, availableIntegrations, pluginsEnabled]);
|
||||
}, [marketplacePlugins, availableIntegrations, loadedPlugins, pluginsEnabled]);
|
||||
|
||||
// building accordion items
|
||||
const accordionItemsData: AccordionItemType[] = [];
|
||||
@@ -204,7 +206,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => {
|
||||
)],
|
||||
});
|
||||
}
|
||||
if (integrations?.length && pluginsEnabled) {
|
||||
if (pluginsEnabled && integrations?.length) {
|
||||
accordionItemsData.push({
|
||||
id: 'integrations',
|
||||
icon: <i className='icon-power-plug-outline'/>,
|
||||
@@ -303,6 +305,7 @@ const StyledPreview = styled(Preview)`
|
||||
width: 387px;
|
||||
height: 416px;
|
||||
padding-right: 32px;
|
||||
margin-top: 17px;
|
||||
}
|
||||
|
||||
strong {
|
||||
|
||||
@@ -12,6 +12,7 @@ const Accordion = styled(LibAccordion)`
|
||||
.accordion-card {
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
color: var(--center-channel-color);
|
||||
|
||||
.accordion-card-header {
|
||||
@@ -46,7 +47,7 @@ const Accordion = styled(LibAccordion)`
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: 1px solid var(--denim-button-bg);
|
||||
border-color: var(--denim-button-bg);
|
||||
|
||||
.accordion-card-header {
|
||||
color: var(--denim-button-bg);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode, useEffect, useState} from 'react';
|
||||
import React, {ReactNode, useCallback, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import classnames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
@@ -110,6 +110,28 @@ const IntegrationsPreview = ({items, categoryId}: IntegrationPreviewSectionProps
|
||||
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'>
|
||||
@@ -119,15 +141,17 @@ const IntegrationsPreview = ({items, categoryId}: IntegrationPreviewSectionProps
|
||||
key={item.id}
|
||||
className={classnames('preview-integrations-plugins-item', {'preview-integrations-plugins-item__readonly': !item.installed && !pluginInstallationPossible})}
|
||||
>
|
||||
<div className='preview-integrations-plugins-item__icon'>
|
||||
<div className='preview-integrations-plugins-item__illustration'>
|
||||
<img src={item.icon}/>
|
||||
</div>
|
||||
<div className='preview-integrations-plugins-item__name'>
|
||||
{item.name}
|
||||
{item.name}<br/>
|
||||
<span className='preview-integrations-plugins-item__name-sub'>
|
||||
{makeIntegrationSubtext(item)}
|
||||
</span>
|
||||
</div>
|
||||
{item.installed &&
|
||||
<div className='icon-check-circle preview-integrations-plugins-item__icon_blue'/>}
|
||||
{!item.installed && <div className='icon-download-outline'/>}
|
||||
<div className='preview-integrations-plugins-item__icon icon-check-circle preview-integrations-plugins-item__icon_blue'/>}
|
||||
</div>);
|
||||
})}
|
||||
</div>
|
||||
@@ -205,6 +229,7 @@ const StyledPreviewSection = styled(PreviewSection)`
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 128px;
|
||||
height: 48px;
|
||||
flex-basis: 45%;
|
||||
@@ -215,7 +240,7 @@ const StyledPreviewSection = styled(PreviewSection)`
|
||||
opacity: 65%;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
&__illustration {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@@ -227,22 +252,30 @@ const StyledPreviewSection = styled(PreviewSection)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&_blue {
|
||||
color: var(--denim-button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 2;
|
||||
margin-top: 8px;
|
||||
color: var(--center-channel-text);
|
||||
font-family: 'Open Sans';
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 22px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,8 +297,8 @@ const StyledPreviewSection = styled(PreviewSection)`
|
||||
}
|
||||
|
||||
.icon-check-circle::before {
|
||||
margin-top: 8px;
|
||||
margin-right: 8px;
|
||||
margin-top: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.icon-download-outline::before {
|
||||
|
||||
@@ -895,6 +895,8 @@
|
||||
"admin.experimental.collapsedThreads.title": "Collapsed Reply Threads",
|
||||
"admin.experimental.defaultTheme.desc": "Set a default theme that applies to all new users on the system.",
|
||||
"admin.experimental.defaultTheme.title": "Default Theme:",
|
||||
"admin.experimental.disableRefetchingOnBrowserFocus.desc": "When true, Mattermost will not refetch channels and channel members when the browser regains focus. This may result in improved performance for users with many channels and channel members.",
|
||||
"admin.experimental.disableRefetchingOnBrowserFocus.title": "Disable data refetching on browser refocus:",
|
||||
"admin.experimental.emailBatchingBufferSize.desc": "Specify the maximum number of notifications batched into a single email.",
|
||||
"admin.experimental.emailBatchingBufferSize.example": "E.g.: \"256\"",
|
||||
"admin.experimental.emailBatchingBufferSize.title": "Email Batching Buffer Size:",
|
||||
@@ -3055,7 +3057,7 @@
|
||||
"cloud_billing.nudge_to_yearly.announcement_bar": "Monthly billing will be discontinued in {days} days . Switch to annual billing",
|
||||
"cloud_billing.nudge_to_yearly.contact_sales": "Contact sales",
|
||||
"cloud_billing.nudge_to_yearly.description": "Monthly billing will be discontinued on {date}. To keep your workspace, switch to annual billing.",
|
||||
"cloud_billing.nudge_to_yearly.learn_more": "Learn more",
|
||||
"cloud_billing.nudge_to_yearly.learn_more": "Update billing",
|
||||
"cloud_billing.nudge_to_yearly.title": "Action required: Switch to annual billing to keep your workspace.",
|
||||
"cloud_delinquency.banner.buttonText": "Update billing now",
|
||||
"cloud_delinquency.banner.end_user_notify_admin_button": "Notify admin",
|
||||
@@ -4318,10 +4320,26 @@
|
||||
"notify_here.question": "By using **@here** you are about to send notifications to up to **{totalMembers} other people**. Are you sure you want to do this?",
|
||||
"notify_here.question_timezone": "By using **@here** you are about to send notifications to up to **{totalMembers} other people** in **{timezones, number} {timezones, plural, one {timezone} other {timezones}}**. Are you sure you want to do this?",
|
||||
"numMembers": "{num, number} {num, plural, one {member} other {members}}",
|
||||
"onboarding_wizard.cloud_plugins.description": "Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we'll add them to your workspace. Additional set up may be needed later.",
|
||||
"onboarding_wizard.cloud_plugins.subtitle": "(almost there!)",
|
||||
"onboarding_wizard.cloud_plugins.title": "Welcome to Mattermost!",
|
||||
"onboarding_wizard.invite_members.copied_link": "Link Copied",
|
||||
"onboarding_wizard.invite_members.copy_link": "Copy Link",
|
||||
"onboarding_wizard.invite_members.copy_link_input": "team invite link",
|
||||
"onboarding_wizard.invite_members.description_link": "Collaboration is tough by yourself. Invite a few team members using the invitation link below.",
|
||||
"onboarding_wizard.invite_members.next_link": "Finish setup",
|
||||
"onboarding_wizard.invite_members.title": "Invite your team members",
|
||||
"onboarding_wizard.launching_workspace.description": "It’ll be ready in a moment",
|
||||
"onboarding_wizard.launching_workspace.title": "Launching your workspace now",
|
||||
"onboarding_wizard.next": "Continue",
|
||||
"onboarding_wizard.plugins.description": "Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we'll add them to your workspace. Additional set up may be needed later.",
|
||||
"onboarding_wizard.organization.description": "We’ll use this to help personalize your workspace.",
|
||||
"onboarding_wizard.organization.empty": "You must enter an organization name",
|
||||
"onboarding_wizard.organization.length": "Organization name must be between {min} and {max} characters",
|
||||
"onboarding_wizard.organization.other": "Invalid organization name: {reason}",
|
||||
"onboarding_wizard.organization.placeholder": "Organization name",
|
||||
"onboarding_wizard.organization.reserved": "Organization name may not <a>start with a reserved word</a>.",
|
||||
"onboarding_wizard.organization.team_api_error": "There was an error, please try again.",
|
||||
"onboarding_wizard.organization.title": "What’s the name of your organization?",
|
||||
"onboarding_wizard.plugins.github": "GitHub",
|
||||
"onboarding_wizard.plugins.github.tooltip": "Subscribe to repositories, stay up to date with reviews, assignments",
|
||||
"onboarding_wizard.plugins.gitlab": "GitLab",
|
||||
@@ -4329,13 +4347,14 @@
|
||||
"onboarding_wizard.plugins.jira": "Jira",
|
||||
"onboarding_wizard.plugins.jira.tooltip": "Create Jira tickets from messages in Mattermost, get notified of important updates in Jira",
|
||||
"onboarding_wizard.plugins.marketplace": "More tools can be added once your workspace is set up. To see all available integrations, <a>visit the Marketplace.</a>",
|
||||
"onboarding_wizard.plugins.subtitle": "(almost there!)",
|
||||
"onboarding_wizard.plugins.title": "Welcome to Mattermost!",
|
||||
"onboarding_wizard.plugins.todo": "To do",
|
||||
"onboarding_wizard.plugins.todo.tooltip": "A plugin to track Todo issues in a list and send you daily reminders about your Todo list",
|
||||
"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.skip": "Skip for now",
|
||||
"onboarding_wizard.previous": "Previous",
|
||||
"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",
|
||||
"onboarding_wizard.submit_error.generic": "Something went wrong. Please try again.",
|
||||
"onboardingTask.checklist.completed_subtitle": "We hope Mattermost is more familiar now.",
|
||||
"onboardingTask.checklist.completed_title": "Well done. You’ve completed all of the tasks!",
|
||||
@@ -5787,6 +5806,9 @@
|
||||
"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}",
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
import {AnyAction} from 'redux';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {ServerError} from '@mattermost/types/errors';
|
||||
import {
|
||||
Channel,
|
||||
ChannelNotifyProps,
|
||||
ChannelMembership,
|
||||
ChannelModerationPatch,
|
||||
ChannelsWithTotalCount,
|
||||
ChannelSearchOpts,
|
||||
ServerChannel,
|
||||
} from '@mattermost/types/channels';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {ChannelTypes, PreferenceTypes, UserTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
@@ -19,18 +31,12 @@ import {
|
||||
getRedirectChannelNameForTeam,
|
||||
isManuallyUnread,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig, getServerVersion} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {ActionFunc, ActionResult, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {getChannelsIdForTeam, getChannelByName} from 'mattermost-redux/utils/channel_utils';
|
||||
|
||||
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
import {Channel, ChannelNotifyProps, ChannelMembership, ChannelModerationPatch, ChannelsWithTotalCount, ChannelSearchOpts} from '@mattermost/types/channels';
|
||||
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
import {getChannelByName} from 'mattermost-redux/utils/channel_utils';
|
||||
|
||||
import {General, Preferences} from '../constants';
|
||||
|
||||
@@ -462,52 +468,33 @@ export function getChannelTimezones(channelId: string): ActionFunc {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc {
|
||||
export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc<{channels: ServerChannel[]; channelMembers: ChannelMembership[]}> {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
dispatch({
|
||||
type: ChannelTypes.CHANNELS_REQUEST,
|
||||
data: null,
|
||||
});
|
||||
|
||||
let channels;
|
||||
let channelMembers;
|
||||
const state = getState();
|
||||
const shouldFetchArchived = isMinimumServerVersion(getServerVersion(state), 5, 21);
|
||||
try {
|
||||
[channels, channelMembers] = await Promise.all([
|
||||
Client4.getMyChannels(teamId, shouldFetchArchived),
|
||||
Client4.getMyChannels(teamId),
|
||||
Client4.getMyChannelMembers(teamId),
|
||||
]);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch({type: ChannelTypes.CHANNELS_FAILURE, error});
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
return {error: error as ServerError};
|
||||
}
|
||||
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: ChannelTypes.RECEIVED_CHANNELS,
|
||||
teamId,
|
||||
data: channels,
|
||||
currentChannelId,
|
||||
},
|
||||
{
|
||||
type: ChannelTypes.CHANNELS_SUCCESS,
|
||||
},
|
||||
{
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
|
||||
data: channelMembers,
|
||||
sync: !shouldFetchArchived,
|
||||
channels,
|
||||
remove: getChannelsIdForTeam(state, teamId),
|
||||
currentUserId,
|
||||
currentChannelId,
|
||||
},
|
||||
]));
|
||||
|
||||
const roles = new Set<string>();
|
||||
for (const member of channelMembers) {
|
||||
for (const role of member.roles.split(' ')) {
|
||||
@@ -518,7 +505,7 @@ export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc {
|
||||
dispatch(loadRolesIfNeeded(roles));
|
||||
}
|
||||
|
||||
return {data: {channels, members: channelMembers}};
|
||||
return {data: {channels, channelMembers}};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export function getUsers(state: GlobalState): IDMappedObjects<UserProfile> {
|
||||
export function getCalls(state: GlobalState): Record<string, UserProfile[]> {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return state[CALLS_PLUGIN].voiceConnectedProfiles;
|
||||
return state[CALLS_PLUGIN].voiceConnectedProfiles || {};
|
||||
}
|
||||
|
||||
export function getCallsConfig(state: GlobalState): CallsConfig {
|
||||
|
||||
@@ -245,10 +245,6 @@ export function isCustomGroupsEnabled(state: GlobalState): boolean {
|
||||
return getConfig(state).EnableCustomGroups === 'true';
|
||||
}
|
||||
|
||||
export function getUseCaseOnboarding(state: GlobalState): boolean {
|
||||
return getFeatureFlagValue(state, 'UseCaseOnboarding') === 'true' && getLicense(state)?.Cloud === 'true';
|
||||
}
|
||||
|
||||
export function insightsAreEnabled(state: GlobalState): boolean {
|
||||
const isConfiguredForFeature = getConfig(state).InsightsEnabled === 'true';
|
||||
const featureIsEnabled = getFeatureFlagValue(state, 'InsightsEnabled') === 'true';
|
||||
|
||||
@@ -506,6 +506,11 @@ export const CloudProducts = {
|
||||
LEGACY: 'cloud-legacy',
|
||||
};
|
||||
|
||||
export const CloudBillingTypes = {
|
||||
INTERNAL: 'internal',
|
||||
LICENSED: 'licensed',
|
||||
};
|
||||
|
||||
export const SelfHostedProducts = {
|
||||
STARTER: 'starter',
|
||||
PROFESSIONAL: 'professional',
|
||||
@@ -1103,6 +1108,7 @@ export const DocLinks = {
|
||||
ONBOARD_SSO: 'https://docs.mattermost.com/onboard/sso-saml.html',
|
||||
TRUE_UP_REVIEW: 'https://mattermost.com/pl/true-up-documentation',
|
||||
SELF_HOSTED_BILLING: 'https://docs.mattermost.com/manage/self-hosted-billing.html',
|
||||
ABOUT_TEAMS: 'https://docs.mattermost.com/welcome/about-teams.html#team-url',
|
||||
};
|
||||
|
||||
export const LicenseLinks = {
|
||||
|
||||
@@ -41,6 +41,7 @@ export type Subscription = {
|
||||
is_free_trial: string;
|
||||
delinquent_since?: number;
|
||||
compliance_blocked?: string;
|
||||
billing_type?: string;
|
||||
}
|
||||
|
||||
export type Product = {
|
||||
|
||||
@@ -38,6 +38,7 @@ export type ClientConfig = {
|
||||
DefaultTheme: string;
|
||||
DiagnosticId: string;
|
||||
DiagnosticsEnabled: string;
|
||||
DisableRefetchingOnBrowserFocus: string;
|
||||
EmailLoginButtonBorderColor: string;
|
||||
EmailLoginButtonColor: string;
|
||||
EmailLoginButtonTextColor: string;
|
||||
@@ -525,7 +526,6 @@ export type EmailSettings = {
|
||||
LoginButtonColor: string;
|
||||
LoginButtonBorderColor: string;
|
||||
LoginButtonTextColor: string;
|
||||
EnableInactivityEmail: boolean;
|
||||
};
|
||||
|
||||
export type RateLimitSettings = {
|
||||
@@ -729,6 +729,7 @@ export type ExperimentalSettings = {
|
||||
EnableRemoteClusterService: boolean;
|
||||
EnableAppBar: boolean;
|
||||
PatchPluginsReactDOM: boolean;
|
||||
DisableRefetchingOnBrowserFocus: boolean;
|
||||
};
|
||||
|
||||
export type AnalyticsSettings = {
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type CompleteOnboardingRequest = {
|
||||
organization: string;
|
||||
install_plugins: string[];
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user