Mm 50013 - readd invite members screen cloud version (#22971)

* MM-50013-readd-invite-members-screen-cloud-version

* add the copy invite and fade in the skip buttons

* unify cloud and self hosted version

* add logic to send emails, and finish launching workspace

* fix styles

* remove unintended change

* fix translations

* fix unit tests

* fix styles

* fix styles

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Pablo Andrés Vélez Vidal 2023-04-26 18:09:22 +02:00 committed by GitHub
parent 81ef403230
commit 812689030b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 516 additions and 117 deletions

View File

@ -71,7 +71,145 @@ exports[`InviteMembers component should match snapshot 1`] = `
</div>
<div
class="PageLine PageLine--no-left"
style="margin-top: 50px; margin-left: 50px; height: calc(30vh);"
style="margin-top: 50px; margin-left: 50px; height: calc(35vh);"
/>
</div>
</div>
</div>
</div>
`;
exports[`InviteMembers component should match snapshot when it is cloud 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>
Who works with you?
</span>
</h1>
<p
class="PreparingWorkspaceDescription"
>
<span>
Collaboration is tough by yourself. Invite a few team members. Separate each email address with a space or comma.
</span>
</p>
<div
class="PreparingWorkspacePageBody"
>
<div
class="UsersEmailsInput empty no-selections css-2b097c-container"
>
<span
aria-live="assertive"
class="css-1laao21-a11yText"
>
<p
id="aria-selection-event"
>
 
</p>
<p
id="aria-context"
>
 
0 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
</p>
</span>
<div
class="users-emails-input__control users-emails-input__control--is-focused users-emails-input__control--menu-is-open css-1pahdxg-control"
>
<div
class="users-emails-input__value-container users-emails-input__value-container--is-multi css-1hwfws3"
>
<div
class="users-emails-input__placeholder css-1lxtzh0-placeholder"
>
Enter email addresses
</div>
<div
class="css-gftnqw-Input"
>
<div
class="users-emails-input__input"
style="display: inline-block;"
>
<input
aria-autocomplete="list"
aria-label="Invite People"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
id="react-select-2-input"
spellcheck="false"
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
<div
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
/>
</div>
</div>
</div>
</div>
<div
class="users-emails-input__menu css-26l3qy-menu"
>
<div
class="users-emails-input__menu-list users-emails-input__menu-list--is-multi css-4ljt47-MenuList"
/>
</div>
</div>
</div>
<div
class="InviteMembers__submit"
>
<button
class="primary-button"
disabled=""
>
<span>
Send invites
</span>
</button>
<div
class="InviteMembersLink"
>
<button
class="InviteMembersLink__button--single"
data-testid="shareLinkInputButton"
>
<i
class="icon icon-content-copy"
/>
<span>
Copy Link
</span>
</button>
</div>
</div>
<div
class="PageLine PageLine--no-left"
style="margin-top: 50px; margin-left: 50px; height: calc(35vh);"
/>
</div>
</div>

View File

@ -1,6 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/preparing-workspace/invite_members_link should match snapshot 1`] = `
<div>
<div
class="InviteMembersLink"
>
<button
class="InviteMembersLink__button--single"
data-testid="shareLinkInputButton"
>
<i
class="icon icon-content-copy"
/>
<span>
Copy Link
</span>
</button>
</div>
</div>
`;
exports[`components/preparing-workspace/invite_members_link should match snapshot when displayed including the input field 1`] = `
<div>
<div
class="InviteMembersLink"

View File

@ -1,23 +1,70 @@
@import 'utils/mixins';
// UX decision to show no more than about 5 1/4 lines of users/emails at a time.
$less-than-6-user-lines-height: 227px;
.InviteMembers-body {
display: flex;
// page width - channels preview width - progress dots width - people overlap width
max-width: calc(100vw - 600px - 120px - 30px);
.PreparingWorkspaceDescription span {
display: inline-block;
max-width: 530px;
}
.UsersEmailsInput {
max-width: 420px;
max-width: 540px;
&.no-selections {
// placeholder and input position is difficult to change.
// This overrides the default positioning of the input & placeholders
// to make the taller than normal input look ok when nothing has yet been selected
.users-emails-input__value-container {
margin-top: 6px;
}
}
.users-emails-input__control {
overflow: auto;
min-height: 108px;
max-height: $less-than-6-user-lines-height;
align-items: flex-start;
}
}
}
.InviteMembers {
&__submit {
display: flex;
width: 400px;
align-items: center;
justify-content: flex-start;
}
}
.InviteMembersLink__button--single {
width: fit-content;
min-width: 148px;
margin-right: 8px;
margin-left: 12px;
}
.fade-in-skip-button {
animation: fade-in 2s forwards;
opacity: 0;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@include simple-in-and-out-before("InviteMembers");
.ChannelsPreview--enter-from-after {

View File

@ -9,6 +9,7 @@ import InviteMembers from './invite_members';
describe('InviteMembers component', () => {
let defaultProps: ComponentProps<any>;
const setEmailsFn = jest.fn();
beforeEach(() => {
defaultProps = {
@ -21,8 +22,12 @@ describe('InviteMembers component', () => {
onPageView: jest.fn(),
previous: <div>{'Previous step'}</div>,
next: jest.fn(),
setEmails: setEmailsFn,
show: true,
transitionDirection: 'forward',
inferredProtocol: null,
isSelfHosted: true,
emails: [],
};
});
@ -32,6 +37,17 @@ describe('InviteMembers component', () => {
expect(container).toMatchSnapshot();
});
it('should match snapshot when it is cloud', () => {
const component = withIntl(
<InviteMembers
{...defaultProps}
isSelfHosted={false}
/>,
);
const {container} = render(component);
expect(container).toMatchSnapshot();
});
it('renders invite URL', () => {
const component = withIntl(<InviteMembers {...defaultProps}/>);
render(component);
@ -68,4 +84,16 @@ describe('InviteMembers component', () => {
fireEvent.click(button);
expect(defaultProps.next).toHaveBeenCalled();
});
it('shows send invites button when in cloud', () => {
const component = withIntl(
<InviteMembers
{...defaultProps}
isSelfHosted={false}
/>,
);
render(component);
const button = screen.getByRole('button', {name: 'Send invites'});
expect(button).toBeInTheDocument();
});
});

View File

@ -1,9 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo, useEffect} from 'react';
import React, {useState, useMemo, useEffect} from 'react';
import {CSSTransition} from 'react-transition-group';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, useIntl} from 'react-intl';
import {UserProfile} from '@mattermost/types/users';
import {t} from 'utils/i18n';
import {Constants} from 'utils/constants';
import UsersEmailsInput from 'components/widgets/inputs/users_emails_input';
import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps} from './steps';
@ -12,20 +19,30 @@ 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 InviteMembersLink from './invite_members_link';
import './invite_members.scss';
type Props = PreparingWorkspacePageProps & {
disableEdits: boolean;
className?: string;
teamInviteId?: string;
emails: Form['teamMembers']['invites'];
setEmails: (emails: Form['teamMembers']['invites']) => void;
teamInviteId: string;
formUrl: Form['url'];
configSiteUrl?: string;
browserSiteUrl: string;
inferredProtocol: 'http' | 'https' | null;
isSelfHosted: boolean;
show: boolean;
}
const InviteMembers = (props: Props) => {
const [email, setEmail] = useState('');
const [showSkipButton, setShowSkipButton] = useState(false);
const {formatMessage} = useIntl();
let className = 'InviteMembers-body';
if (props.className) {
className += ' ' + props.className;
@ -33,6 +50,30 @@ const InviteMembers = (props: Props) => {
useEffect(props.onPageView, []);
useEffect(() => {
setShowSkipButton(false);
const timer = setTimeout(() => {
setShowSkipButton(true);
}, 3000);
return () => clearTimeout(timer);
}, [props.show]);
const placeholder = formatMessage({
id: 'onboarding_wizard.invite_members.placeholder',
defaultMessage: 'Enter email addresses',
});
const errorProperties = {
showError: false,
errorMessageId: t(
'invitation_modal.invite_members.exceeded_max_add_members_batch',
),
errorMessageDefault: 'No more than **{text}** people can be invited at once',
errorMessageValues: {
text: Constants.MAX_ADD_MEMBERS_BATCH.toString(),
},
};
const inviteURL = useMemo(() => {
let urlBase = '';
if (props.configSiteUrl && !props.configSiteUrl.includes('localhost')) {
@ -45,14 +86,128 @@ const InviteMembers = (props: Props) => {
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.'
let suppressNoOptionsMessage = true;
if (props.emails?.length > Constants.MAX_ADD_MEMBERS_BATCH) {
errorProperties.showError = true;
// We want to suppress the no options message, unless the message that is going to be displayed
// is the max users warning
suppressNoOptionsMessage = false;
}
const cloudInviteMembersInput = (
<UsersEmailsInput
{...errorProperties}
usersLoader={() => Promise.resolve([])}
placeholder={placeholder}
ariaLabel={formatMessage({
id: 'invitation_modal.members.search_and_add.title',
defaultMessage: 'Invite People',
})}
onChange={(emails: Array<UserProfile | string>) => {
// There should not be any users found or passed,
// because the usersLoader should never return any.
// Filtering them out in case there are any
// and to resolve Typescript errors
props.setEmails(emails.filter((x) => typeof x === 'string') as string[]);
}}
value={props.emails}
onInputChange={setEmail}
inputValue={email}
emailInvitationsEnabled={true}
autoFocus={true}
validAddressMessageId={t('invitation_modal.members.users_emails_input.valid_email')}
validAddressMessageDefault={'Invite **{email}** as a team member'}
suppressNoOptionsMessage={suppressNoOptionsMessage}
/>
);
const inviteInteraction = <InviteMembersLink inviteURL={inviteURL}/>;
const inviteLink = (
<InviteMembersLink
inviteURL={inviteURL}
inputAndButtonStyle={props.isSelfHosted}
/>
);
const inviteMemberBodyContent = () => {
if (props.isSelfHosted) {
return (
<>
<Title>
<FormattedMessage
id={'onboarding_wizard.invite_members.title'}
defaultMessage='Invite your team members'
/>
</Title>
<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.'
/>
</Description>
<PageBody>
{inviteLink}
</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>
</>
);
}
return (
<>
<Title>
<FormattedMessage
id={'onboarding_wizard.invite_members_cloud.title'}
defaultMessage='Who works with you?'
/>
</Title>
<Description>
<FormattedMessage
id={'onboarding_wizard.invite_members.description'}
defaultMessage='Collaboration is tough by yourself. Invite a few team members. Separate each email address with a space or comma.'
/>
</Description>
<PageBody>
{cloudInviteMembersInput}
</PageBody>
<div className='InviteMembers__submit'>
<button
className='primary-button'
disabled={props.disableEdits || props.emails.length === 0}
onClick={props.next}
>
<FormattedMessage
id={'onboarding_wizard.invite_members.next'}
defaultMessage='Send invites'
/>
</button>
{inviteLink}
{showSkipButton &&
<button
className='link-style fade-in-skip-button'
onClick={props.skip}
>
<FormattedMessage
id={'onboarding_wizard.invite_members.skip'}
defaultMessage='Skip'
/>
</button>
}
</div>
</>
);
};
return (
<CSSTransition
@ -73,35 +228,12 @@ const InviteMembers = (props: Props) => {
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>
{inviteMemberBodyContent()}
<PageLine
style={{
marginTop: '50px',
marginLeft: '50px',
height: 'calc(30vh)',
height: 'calc(35vh)',
}}
noLeft={true}
/>

View File

@ -1,3 +1,5 @@
@import 'utils/mixins';
.InviteMembersLink {
display: flex;
@ -9,35 +11,21 @@
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);
background: var(--center-channel-color-rgb);
border-radius: 4px 0 0 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
font-size: 16px;
}
&__button {
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;
@ -48,4 +36,28 @@
fill: var(--button-bg);
}
}
&__button {
width: 180px;
height: 48px;
border: 1px solid var(--button-bg);
background: var(--center-channel-bg);
border-radius: 0 4px 4px 0;
color: var(--button-bg);
&:hover {
background: rgba(var(--button-bg-rgb), 0.08);
}
&:active {
background: rgba(var(--button-bg-rgb), 0.08);
}
}
&__button--single {
@include tertiary-button;
height: 40px;
border-radius: 4px;
}
}

View File

@ -21,8 +21,39 @@ describe('components/preparing-workspace/invite_members_link', () => {
expect(container).toMatchSnapshot();
});
it('should match snapshot when displayed including the input field', () => {
const component = withIntl(
<InviteMembersLink
inviteURL={inviteURL}
inputAndButtonStyle={true}
/>,
);
const {container} = render(component);
expect(container).toMatchSnapshot();
});
it('renders only with the button if the inputAndButton option is false', () => {
const component = withIntl(
<InviteMembersLink
inviteURL={inviteURL}
inputAndButtonStyle={false}
/>,
);
render(component);
const input = screen.queryByText(inviteURL);
expect(input).not.toBeInTheDocument();
const button = screen.getByRole('button', {name: /copy link/i});
expect(button).toBeInTheDocument();
});
it('renders an input field with the invite URL', () => {
const component = withIntl(<InviteMembersLink inviteURL={inviteURL}/>);
const component = withIntl(
<InviteMembersLink
inviteURL={inviteURL}
inputAndButtonStyle={true}
/>,
);
render(component);
const input = screen.getByDisplayValue(inviteURL);
expect(input).toBeInTheDocument();

View File

@ -11,30 +11,36 @@ import './invite_members_link.scss';
type Props = {
inviteURL: string;
inputAndButtonStyle?: boolean;
}
const InviteMembersLink = (props: Props) => {
const InviteMembersLink = ({
inviteURL,
inputAndButtonStyle = false,
}: Props) => {
const copyText = useCopyText({
trackCallback: () => trackEvent('first_admin_setup', 'admin_setup_click_copy_invite_link'),
text: props.inviteURL,
text: 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'
/>
{inputAndButtonStyle &&
<input
className='InviteMembersLink__input'
type='text'
readOnly={true}
value={inviteURL}
aria-label={intl.formatMessage({
id: 'onboarding_wizard.invite_members.copy_link_input',
defaultMessage: 'team invite link',
})}
data-testid='shareLinkInput'
/>
}
<button
className='InviteMembersLink__button'
className={`InviteMembersLink__button${inputAndButtonStyle ? '' : '--single'}`}
onClick={copyText.onClick}
data-testid='shareLinkInputButton'
>
@ -48,7 +54,7 @@ const InviteMembersLink = (props: Props) => {
</>
) : (
<>
<i className='icon icon-link-variant'/>
{inputAndButtonStyle ? <i className='icon icon-link-variant'/> : <i className='icon icon-content-copy'/>}
<FormattedMessage
id='onboarding_wizard.invite_members.copy_link'
defaultMessage='Copy Link'

View File

@ -10,7 +10,6 @@ import MultiSelectCards from 'components/common/multi_select_cards';
import GithubSVG from 'components/common/svg_images_components/github_svg';
import GitlabSVG from 'components/common/svg_images_components/gitlab_svg';
import CelebrateSVG from 'components/common/svg_images_components/celebrate_svg';
import JiraSVG from 'components/common/svg_images_components/jira_svg';
import ZoomSVG from 'components/common/svg_images_components/zoom_svg';
import TodoSVG from 'components/common/svg_images_components/todo_svg';
@ -47,32 +46,19 @@ const Plugins = (props: Props) => {
className += ' ' + props.className;
}
let title = (
const title = (
<FormattedMessage
id={'onboarding_wizard.cloud_plugins.title'}
defaultMessage='Welcome to Mattermost!'
id={'onboarding_wizard.self_hosted_plugins.title'}
defaultMessage='What tools do you use?'
/>
);
let description = (
const 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.'}
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.'}
/>
);
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
@ -95,16 +81,6 @@ const Plugins = (props: Props) => {
{props.previous}
<Title>
{title}
{!props.isSelfHosted && (
<div className='subtitle'>
<CelebrateSVG/>
<FormattedMessage
id={'onboarding_wizard.cloud_plugins.subtitle'}
defaultMessage='(almost there!)'
/>
</div>
)}
</Title>
<Description>{description}</Description>
<PageBody>

View File

@ -133,6 +133,7 @@
// centering
margin: 0 auto;
margin-left: 300px;
}
@media screen and (max-width: 768px) {

View File

@ -8,6 +8,7 @@ import {FormattedMessage, useIntl} from 'react-intl';
import {GeneralTypes} from 'mattermost-redux/action_types';
import {General} from 'mattermost-redux/constants';
import {sendEmailInvitesToTeamGracefully} from 'mattermost-redux/actions/teams';
import {getFirstAdminSetupComplete as getFirstAdminSetupCompleteAction} from 'mattermost-redux/actions/general';
import {ActionResult} from 'mattermost-redux/types/actions';
import {Team} from '@mattermost/types/teams';
@ -114,18 +115,19 @@ const PreparingWorkspace = (props: Props) => {
// In cloud instances created from portal,
// new admin user has a team in myTeams but not in currentTeam.
let team = currentTeam || myTeams?.[0];
const team = currentTeam || myTeams?.[0];
const config = useSelector(getConfig);
const pluginsEnabled = config.PluginsEnabled === 'true';
const showOnMountTimeout = useRef<NodeJS.Timeout>();
const configSiteUrl = config.SiteURL;
const isConfigSiteUrlDefault = Boolean(config.SiteURL && config.SiteURL === Constants.DEFAULT_SITE_URL);
const isSelfHosted = useSelector(getLicense).Cloud !== 'true';
const stepOrder = [
isSelfHosted && WizardSteps.Organization,
pluginsEnabled && WizardSteps.Plugins,
isSelfHosted && WizardSteps.InviteMembers,
WizardSteps.InviteMembers,
WizardSteps.LaunchingWorkspace,
].filter((x) => Boolean(x)) as WizardStep[];
@ -225,16 +227,15 @@ const PreparingWorkspace = (props: Props) => {
const sendFormStart = Date.now();
setSubmissionState(SubmissionStates.Submitting);
if (form.organization && !isSelfHosted) {
if (!form.teamMembers.skipped && !isConfigSiteUrlDefault && !isSelfHosted) {
try {
const {error, newTeam} = await createTeam(form.organization);
if (error !== null) {
redirectWithError(WizardSteps.Organization, genericSubmitError);
const inviteResult = await dispatch(sendEmailInvitesToTeamGracefully(team.id, form.teamMembers.invites));
if ((inviteResult as ActionResult).error) {
redirectWithError(WizardSteps.InviteMembers, genericSubmitError);
return;
}
team = newTeam as Team;
} catch (e) {
redirectWithError(WizardSteps.Organization, genericSubmitError);
redirectWithError(WizardSteps.InviteMembers, genericSubmitError);
return;
}
}
@ -435,16 +436,10 @@ const PreparingWorkspace = (props: Props) => {
next={() => {
const pluginChoices = {...form.plugins};
delete pluginChoices.skipped;
if (!isSelfHosted) {
setSubmissionState(SubmissionStates.UserRequested);
}
makeNext(WizardSteps.Plugins)(pluginChoices);
skipPlugins(false);
}}
skip={() => {
if (!isSelfHosted) {
setSubmissionState(SubmissionStates.UserRequested);
}
makeNext(WizardSteps.Plugins, true)();
skipPlugins(true);
}}
@ -489,6 +484,18 @@ const PreparingWorkspace = (props: Props) => {
configSiteUrl={configSiteUrl}
formUrl={form.url}
browserSiteUrl={browserSiteUrl}
emails={form.teamMembers.invites}
setEmails={(emails: string[]) => {
setForm({
...form,
teamMembers: {
...form.teamMembers,
invites: emails,
},
});
}}
inferredProtocol={form.inferredProtocol}
isSelfHosted={isSelfHosted}
/>
<LaunchingWorkspace
onPageView={onPageViews[WizardSteps.LaunchingWorkspace]}

View File

@ -4318,14 +4318,16 @@
"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_cloud.title": "Who works with you?",
"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": "Collaboration is tough by yourself. Invite a few team members. Separate each email address with a space or comma.",
"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": "Send invites",
"onboarding_wizard.invite_members.next_link": "Finish setup",
"onboarding_wizard.invite_members.placeholder": "Enter email addresses",
"onboarding_wizard.invite_members.skip": "Skip",
"onboarding_wizard.invite_members.title": "Invite your team members",
"onboarding_wizard.launching_workspace.description": "Itll be ready in a moment",
"onboarding_wizard.launching_workspace.title": "Launching your workspace now",

View File

@ -19,7 +19,6 @@ body {
&.admin-onboarding {
background-image: url('images/admin-onboarding-background.jpg');
background-position: 50%;
background-repeat: no-repeat;
background-size: cover;
}