diff --git a/e2e-tests/playwright/support/server/default_config.ts b/e2e-tests/playwright/support/server/default_config.ts
index 2d8cb12c58..3e8e5ed881 100644
--- a/e2e-tests/playwright/support/server/default_config.ts
+++ b/e2e-tests/playwright/support/server/default_config.ts
@@ -665,7 +665,6 @@ const defaultServerConfig: AdminConfig = {
BoardsFeatureFlags: '',
BoardsDataRetention: false,
NormalizeLdapDNs: false,
- UseCaseOnboarding: true,
GraphQL: false,
InsightsEnabled: true,
CommandPalette: false,
diff --git a/server/channels/api4/system_test.go b/server/channels/api4/system_test.go
index 5921e32802..25574e4400 100644
--- a/server/channels/api4/system_test.go
+++ b/server/channels/api4/system_test.go
@@ -892,6 +892,7 @@ func TestCompleteOnboarding(t *testing.T) {
req := &model.CompleteOnboardingRequest{
InstallPlugins: []string{"testplugin2"},
+ Organization: "my-org",
}
t.Run("as a regular user", func(t *testing.T) {
diff --git a/server/channels/app/onboarding.go b/server/channels/app/onboarding.go
index 2dd85749d9..3b76aefe53 100644
--- a/server/channels/app/onboarding.go
+++ b/server/channels/app/onboarding.go
@@ -28,6 +28,24 @@ func (a *App) markAdminOnboardingComplete(c *request.Context) *model.AppError {
}
func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
+ isCloud := a.Srv().License() != nil && *a.Srv().License().Features.Cloud
+
+ if !isCloud && request.Organization == "" {
+ mlog.Error("No organization name provided for self hosted onboarding")
+ return model.NewAppError("CompleteOnboarding", "api.error_no_organization_name_provided_for_self_hosted_onboarding", nil, "", http.StatusBadRequest)
+ }
+
+ if request.Organization != "" {
+ err := a.Srv().Store().System().SaveOrUpdate(&model.System{
+ Name: model.SystemOrganizationName,
+ Value: request.Organization,
+ })
+ if err != nil {
+ // don't block onboarding because of that.
+ a.Log().Error("failed to save organization name", mlog.Err(err))
+ }
+ }
+
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
if pluginsEnvironment == nil {
return a.markAdminOnboardingComplete(c)
diff --git a/server/channels/app/onboarding_test.go b/server/channels/app/onboarding_test.go
new file mode 100644
index 0000000000..cf8462cf28
--- /dev/null
+++ b/server/channels/app/onboarding_test.go
@@ -0,0 +1,30 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+package app
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/mattermost/mattermost-server/server/v8/channels/app/request"
+ mm_model "github.com/mattermost/mattermost-server/server/v8/model"
+)
+
+func TestOnboardingSavesOrganizationName(t *testing.T) {
+ th := Setup(t)
+ defer th.TearDown()
+
+ err := th.App.CompleteOnboarding(&request.Context{}, &mm_model.CompleteOnboardingRequest{
+ Organization: "Mattermost In Tests",
+ })
+ require.Nil(t, err)
+ defer func() {
+ th.App.Srv().Store().System().PermanentDeleteByName(mm_model.SystemOrganizationName)
+ }()
+
+ sys, storeErr := th.App.Srv().Store().System().GetByName(mm_model.SystemOrganizationName)
+ require.NoError(t, storeErr)
+ require.Equal(t, "Mattermost In Tests", sys.Value)
+}
diff --git a/server/i18n/en.json b/server/i18n/en.json
index 598462a448..40f626291f 100644
--- a/server/i18n/en.json
+++ b/server/i18n/en.json
@@ -1777,6 +1777,10 @@
"id": "api.error_get_first_admin_visit_marketplace_status",
"translation": "Error trying to retrieve the first admin visit marketplace status from the store."
},
+ {
+ "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding",
+ "translation": "Error no organization name provided for self hosted onboarding."
+ },
{
"id": "api.error_set_first_admin_complete_setup",
"translation": "Error trying to save first admin complete setup in the store."
diff --git a/server/model/onboarding.go b/server/model/onboarding.go
index 797bea7c1d..0fe5e91ffa 100644
--- a/server/model/onboarding.go
+++ b/server/model/onboarding.go
@@ -10,6 +10,7 @@ import (
// CompleteOnboardingRequest describes parameters of the requested plugin.
type CompleteOnboardingRequest struct {
+ Organization string `json:"organization"` // Organization is the name of the organization
InstallPlugins []string `json:"install_plugins"` // InstallPlugins is a list of plugins to be installed
}
diff --git a/server/model/system.go b/server/model/system.go
index fbc2aaa684..24b4fce9c9 100644
--- a/server/model/system.go
+++ b/server/model/system.go
@@ -16,6 +16,7 @@ const (
SystemAsymmetricSigningKeyKey = "AsymmetricSigningKey"
SystemPostActionCookieSecretKey = "PostActionCookieSecret"
SystemInstallationDateKey = "InstallationDate"
+ SystemOrganizationName = "OrganizationName"
SystemFirstServerRunTimestampKey = "FirstServerRunTimestamp"
SystemClusterEncryptionKey = "ClusterEncryptionKey"
SystemUpgradedFromTeId = "UpgradedFromTE"
diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx
index cb22542fa4..6ac09280ab 100644
--- a/webapp/channels/src/actions/global_actions.tsx
+++ b/webapp/channels/src/actions/global_actions.tsx
@@ -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;
}
diff --git a/webapp/channels/src/components/do_verify_email/do_verify_email.tsx b/webapp/channels/src/components/do_verify_email/do_verify_email.tsx
index 7b03d9e7d5..0e81ead2c7 100644
--- a/webapp/channels/src/components/do_verify_email/do_verify_email.tsx
+++ b/webapp/channels/src/components/do_verify_email/do_verify_email.tsx
@@ -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;
}
diff --git a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts
index a59ff532cc..5a2ac01c35 100644
--- a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts
+++ b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts
@@ -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),
};
}
diff --git a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx
index effe92c1ad..aa1ac2e833 100644
--- a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx
+++ b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx
@@ -34,7 +34,6 @@ describe('components/channel_header/components/UserGuideDropdown', () => {
},
pluginMenuItems: [],
isFirstAdmin: false,
- useCaseOnboarding: false,
};
test('should match snapshot', () => {
diff --git a/webapp/channels/src/components/login/login.tsx b/webapp/channels/src/components/login/login.tsx
index bed62d7eed..c0e154e561 100644
--- a/webapp/channels/src/components/login/login.tsx
+++ b/webapp/channels/src/components/login/login.tsx
@@ -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);
@@ -631,14 +630,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();
}
};
diff --git a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap
new file mode 100644
index 0000000000..4aa2442f0a
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`InviteMembers component should match snapshot 1`] = `
+
+
+
+
+
+
+ Previous step
+
+
+
+ Invite your team members
+
+
+
+
+ Collaboration is tough by yourself. Invite a few team members using the invitation link below.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap
new file mode 100644
index 0000000000..0610646391
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components/preparing-workspace/invite_members_link should match snapshot 1`] = `
+
+
+
+
+
+
+`;
diff --git a/webapp/channels/src/components/preparing_workspace/__snapshots__/organization_status.test.tsx.snap b/webapp/channels/src/components/preparing_workspace/__snapshots__/organization_status.test.tsx.snap
new file mode 100644
index 0000000000..cec545b0bd
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/__snapshots__/organization_status.test.tsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components/preparing-workspace/organization_status should match snapshot 1`] = `
+
+`;
diff --git a/webapp/channels/src/components/preparing_workspace/index.tsx b/webapp/channels/src/components/preparing_workspace/index.tsx
index a3ab4aa606..c454fd28ac 100644
--- a/webapp/channels/src/components/preparing_workspace/index.tsx
+++ b/webapp/channels/src/components/preparing_workspace/index.tsx
@@ -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, Actions>({
+ updateTeam,
createTeam,
getProfiles,
checkIfTeamExists,
diff --git a/webapp/channels/src/components/preparing_workspace/invite_members.scss b/webapp/channels/src/components/preparing_workspace/invite_members.scss
new file mode 100644
index 0000000000..dc914d42b1
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/invite_members.scss
@@ -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);
+ }
+}
diff --git a/webapp/channels/src/components/preparing_workspace/invite_members.test.tsx b/webapp/channels/src/components/preparing_workspace/invite_members.test.tsx
new file mode 100644
index 0000000000..54fe45f374
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/invite_members.test.tsx
@@ -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;
+
+ 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: {'Previous step'}
,
+ next: jest.fn(),
+ show: true,
+ transitionDirection: 'forward',
+ };
+ });
+
+ it('should match snapshot', () => {
+ const component = withIntl();
+ const {container} = render(component);
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders invite URL', () => {
+ const component = withIntl();
+ 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();
+ render(component);
+ const button = screen.getByRole('button', {name: 'Finish setup'});
+ expect(button).toBeInTheDocument();
+ });
+
+ it('button is disabled when disableEdits is true', () => {
+ const component = withIntl(
+ ,
+ );
+ render(component);
+ const button = screen.getByRole('button', {name: 'Finish setup'});
+ expect(button).toBeDisabled();
+ });
+
+ it('invokes next prop on button click', () => {
+ const component = withIntl();
+ render(component);
+ const button = screen.getByRole('button', {name: 'Finish setup'});
+ fireEvent.click(button);
+ expect(defaultProps.next).toHaveBeenCalled();
+ });
+});
diff --git a/webapp/channels/src/components/preparing_workspace/invite_members.tsx b/webapp/channels/src/components/preparing_workspace/invite_members.tsx
new file mode 100644
index 0000000000..a018c2a446
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/invite_members.tsx
@@ -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 = (
+
+ );
+
+ const inviteInteraction = ;
+
+ return (
+
+
+
+
+ {props.previous}
+
+
+
+
+ {description}
+
+
+ {inviteInteraction}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default InviteMembers;
diff --git a/webapp/channels/src/components/preparing_workspace/invite_members_illustration.tsx b/webapp/channels/src/components/preparing_workspace/invite_members_illustration.tsx
new file mode 100644
index 0000000000..26b28e9b6f
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/invite_members_illustration.tsx
@@ -0,0 +1,838 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {SVGProps} from 'react';
+
+const InviteMembersIllustration = (props: SVGProps) => (
+
+);
+
+export default InviteMembersIllustration;
diff --git a/webapp/channels/src/components/preparing_workspace/invite_members_link.scss b/webapp/channels/src/components/preparing_workspace/invite_members_link.scss
new file mode 100644
index 0000000000..09b229f264
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/invite_members_link.scss
@@ -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);
+ }
+ }
+}
diff --git a/webapp/channels/src/components/preparing_workspace/invite_members_link.test.tsx b/webapp/channels/src/components/preparing_workspace/invite_members_link.test.tsx
new file mode 100644
index 0000000000..d74b81d493
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/invite_members_link.test.tsx
@@ -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();
+
+ const {container} = render(component);
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders an input field with the invite URL', () => {
+ const component = withIntl();
+ render(component);
+ const input = screen.getByDisplayValue(inviteURL);
+ expect(input).toBeInTheDocument();
+ });
+
+ it('renders a button to copy the invite URL', () => {
+ const component = withIntl();
+ 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();
+ 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();
+ 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);
+ });
+});
diff --git a/webapp/channels/src/components/preparing_workspace/invite_members_link.tsx b/webapp/channels/src/components/preparing_workspace/invite_members_link.tsx
new file mode 100644
index 0000000000..f6491809ca
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/invite_members_link.tsx
@@ -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 (
+
+
+
+
+ );
+};
+
+export default InviteMembersLink;
diff --git a/webapp/channels/src/components/preparing_workspace/mixins.scss b/webapp/channels/src/components/preparing_workspace/mixins.scss
new file mode 100644
index 0000000000..b3ca03bce8
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/webapp/channels/src/components/preparing_workspace/organization.scss b/webapp/channels/src/components/preparing_workspace/organization.scss
new file mode 100644
index 0000000000..c063010404
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/organization.scss
@@ -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");
diff --git a/webapp/channels/src/components/preparing_workspace/organization.tsx b/webapp/channels/src/components/preparing_workspace/organization.tsx
new file mode 100644
index 0000000000..684c6dc4d9
--- /dev/null
+++ b/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();
+ const validation = teamNameToUrl(props.organization || '');
+ const teamApiError = useRef(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) => {
+ 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 (
+
+
+
+
+
+
+ {props.previous}
+
+
+
+
+
+
+
+ handleOnChange(e)}
+ onKeyUp={onNext}
+ autoFocus={true}
+ ref={inputRef as unknown as any}
+ />
+ {triedNext ? : null}
+
+
+
+
+
+
+
+ );
+};
+export default Organization;
diff --git a/webapp/channels/src/components/preparing_workspace/organization_status.test.tsx b/webapp/channels/src/components/preparing_workspace/organization_status.test.tsx
new file mode 100644
index 0000000000..e7d65bfd6b
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/organization_status.test.tsx
@@ -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();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it('should render no error message when error prop is null', () => {
+ const {queryByText, container} = render();
+ 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();
+ 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();
+ 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();
+ const {getByText} = render(component);
+ expect(getByText(/Organization name must be between 2 and 64 characters/i)).toBeInTheDocument();
+ });
+});
diff --git a/webapp/channels/src/components/preparing_workspace/organization_status.tsx b/webapp/channels/src/components/preparing_workspace/organization_status.tsx
new file mode 100644
index 0000000000..d695a2ad26
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/organization_status.tsx
@@ -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 = (
+
+ );
+ break;
+ case TeamApiError:
+ children = (
+
+ );
+ break;
+ case BadUrlReasons.Length:
+ children = (
+
+ );
+ break;
+ case BadUrlReasons.Reserved:
+ children = (
+ (
+
+ {chunks}
+
+ ),
+ }}
+ />
+ );
+ break;
+ default:
+ children = (
+
+ );
+ break;
+ }
+ }
+ return {children}
;
+};
+
+export default OrganizationStatus;
diff --git a/webapp/channels/src/components/preparing_workspace/page_line.scss b/webapp/channels/src/components/preparing_workspace/page_line.scss
new file mode 100644
index 0000000000..12801e1f67
--- /dev/null
+++ b/webapp/channels/src/components/preparing_workspace/page_line.scss
@@ -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;
+ }
+}
diff --git a/webapp/channels/src/components/preparing_workspace/page_line.tsx b/webapp/channels/src/components/preparing_workspace/page_line.tsx
new file mode 100644
index 0000000000..ebbb9ee024
--- /dev/null
+++ b/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;
+ noLeft?: boolean;
+}
+const PageLine = (props: Props) => {
+ let className = 'PageLine';
+ if (props.noLeft) {
+ className += ' PageLine--no-left';
+ }
+ const styles: Record = {};
+ 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 (
+
+ );
+};
+
+export default PageLine;
diff --git a/webapp/channels/src/components/preparing_workspace/plugins.scss b/webapp/channels/src/components/preparing_workspace/plugins.scss
index fa74dc5718..0a5465564e 100644
--- a/webapp/channels/src/components/preparing_workspace/plugins.scss
+++ b/webapp/channels/src/components/preparing_workspace/plugins.scss
@@ -4,6 +4,9 @@
margin-top: 24px;
}
+.plugins-skip-btn {
+ margin-left: 8px;
+}
// preempt cards wrapping
@media screen and (max-width: 900px) {
.Plugins-body {
diff --git a/webapp/channels/src/components/preparing_workspace/plugins.tsx b/webapp/channels/src/components/preparing_workspace/plugins.tsx
index b3b1168015..caf04e794e 100644
--- a/webapp/channels/src/components/preparing_workspace/plugins.tsx
+++ b/webapp/channels/src/components/preparing_workspace/plugins.tsx
@@ -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 = (
+
+ );
+ let description = (
+
+ );
+ if (props.isSelfHosted) {
+ title = (
+
+ );
+ description = (
+
+ );
+ }
+
return (
{
>
+
{props.previous}
-
-
-
-
-
+ {title}
+ {!props.isSelfHosted && (
+
+
+
+
+
+ )}
-
-
-
+ {description}
{
/>
+
diff --git a/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss b/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss
index c91dd0a1fe..99187c301b 100644
--- a/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss
+++ b/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss
@@ -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 {
diff --git a/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx b/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx
index d554090d68..268e21c55e 100644
--- a/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx
+++ b/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx
@@ -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) => 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();
+ 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(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