feat(auth): collect the workspace logo on the sign-up creation step (#21723)
## What & why A single, consistent **workspace-creation step** for both multi-workspace and single-workspace self-host — collecting **name + logo** (and the **subdomain** in multi-workspace) — which **removes the duplicate name/logo prompt** that previously reappeared on the workspace subdomain (reported after #21641). ## Changes **One creation form for both modes** - With 0 workspaces, both multi-workspace and single-workspace route to the shared `SignInUpWorkspaceCreationForm`; `SignInUp` renders it for the `WorkspaceCreation` step regardless of domain/scope. - The subdomain field shows only in multi-workspace; single-workspace keeps its fixed address. **Logo on the creation step** - New scoped `uploadNewWorkspaceLogo(workspaceId, file)` mutation: the creator sets a logo on their just-created `PENDING_CREATION` workspace via the workspace-agnostic token (membership enforced — only the creator is a member at that point), reusing `uploadWorkspacePicture`. Upload size is capped via `settings.storage.maxFileSize` (also applied to the existing logo / profile-picture uploads). - The picked file is held locally (object-URL preview, revoked on unmount) and uploaded right after creation (non-fatal on failure). **Onboarding step → pure activation loader** - The old "Create your workspace" form (name + logo) is removed. The onboarding step now activates the pending workspace on mount and shows the loader, with a **Retry** action on failure. ## Testing - typecheck (front + server) ✅; oxlint + oxfmt clean on changed files ✅ - Unit tests: `auth.resolver.spec`, `useWorkspaceSubdomainField`, `SignInUpWorkspaceCreationForm` (multi + single-workspace), `useAuth` ✅ - Metadata GraphQL + `twenty-client-sdk` schema regenerated. Follow-up to #21641. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Xw37hR5seiCyWnppG9z4op --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -301,13 +301,13 @@ export const PageChangeEffect = () => {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(location, AppPath.CreateWorkspace): {
|
||||
case isMatchingLocation(location, AppPath.WorkspaceActivation): {
|
||||
resetFocusStackToFocusItem({
|
||||
focusStackItem: {
|
||||
focusId: PageFocusId.CreateWorkspace,
|
||||
focusId: PageFocusId.WorkspaceActivation,
|
||||
componentInstance: {
|
||||
componentType: FocusComponentType.PAGE,
|
||||
componentInstanceId: PageFocusId.CreateWorkspace,
|
||||
componentInstanceId: PageFocusId.WorkspaceActivation,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysWithModifiers: false,
|
||||
|
||||
@@ -48,9 +48,9 @@ const Authorize = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const CreateWorkspace = lazy(() =>
|
||||
import('~/pages/onboarding/CreateWorkspace').then((module) => ({
|
||||
default: module.CreateWorkspace,
|
||||
const WorkspaceActivation = lazy(() =>
|
||||
import('~/pages/onboarding/WorkspaceActivation').then((module) => ({
|
||||
default: module.WorkspaceActivation,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -148,10 +148,10 @@ export const useCreateAppRouter = (
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.CreateWorkspace}
|
||||
path={AppPath.WorkspaceActivation}
|
||||
element={
|
||||
<LazyRoute fallback={null}>
|
||||
<CreateWorkspace />
|
||||
<WorkspaceActivation />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -78,7 +78,7 @@ export const Logo = ({
|
||||
{isUsingDefaultLogo ? (
|
||||
<UndecoratedLink
|
||||
to={AppPath.SignInUp}
|
||||
onClick={redirectToDefaultDomain}
|
||||
onClick={() => redirectToDefaultDomain()}
|
||||
>
|
||||
<StyledPrimaryLogo
|
||||
style={{ backgroundImage: `url(${primaryLogoUrl})` }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
|
||||
export const ONBOARDING_PATHS = [
|
||||
AppPath.CreateWorkspace,
|
||||
AppPath.WorkspaceActivation,
|
||||
AppPath.CreateProfile,
|
||||
AppPath.SyncEmails,
|
||||
AppPath.InviteTeam,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPLOAD_NEW_WORKSPACE_LOGO = gql`
|
||||
mutation UploadNewWorkspaceLogo($workspaceId: String!, $file: Upload!) {
|
||||
uploadNewWorkspaceLogo(workspaceId: $workspaceId, file: $file) {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -32,7 +32,6 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
|
||||
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
|
||||
import { loginTokenState } from '@/auth/states/loginTokenState';
|
||||
import {
|
||||
SignInUpStep,
|
||||
@@ -77,8 +76,6 @@ export const useAuth = () => {
|
||||
const { loadCurrentUser } = useLoadCurrentUser();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { createWorkspace } = useSignUpInNewWorkspace();
|
||||
|
||||
const setSignInUpStep = useSetAtomState(signInUpStepState);
|
||||
const { redirect } = useRedirect();
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
@@ -131,16 +128,18 @@ export const useAuth = () => {
|
||||
async (
|
||||
availableWorkspaces: Parameters<typeof countAvailableWorkspaces>[0],
|
||||
email: string,
|
||||
{ newTab = true }: { newTab?: boolean } = {},
|
||||
) => {
|
||||
const availableWorkspacesCount =
|
||||
countAvailableWorkspaces(availableWorkspaces);
|
||||
|
||||
if (availableWorkspacesCount === 0) {
|
||||
if (!isMultiWorkspaceEnabled) {
|
||||
return await createWorkspace({ newTab });
|
||||
}
|
||||
// The in-app "Create Workspace" entry point redirects here with this
|
||||
// signal so an existing user with workspaces lands on the creation form
|
||||
// instead of the workspace selection step.
|
||||
const wantsToCreateNewWorkspace =
|
||||
new URLSearchParams(window.location.search).get('action') ===
|
||||
'create-new-workspace';
|
||||
|
||||
if (availableWorkspacesCount === 0 || wantsToCreateNewWorkspace) {
|
||||
await apolloClient.query({
|
||||
query: GetWorkspaceCreationDefaultsDocument,
|
||||
});
|
||||
@@ -166,13 +165,7 @@ export const useAuth = () => {
|
||||
|
||||
setSignInUpStep(SignInUpStep.WorkspaceSelection);
|
||||
},
|
||||
[
|
||||
apolloClient,
|
||||
createWorkspace,
|
||||
isMultiWorkspaceEnabled,
|
||||
redirectToWorkspaceDomain,
|
||||
setSignInUpStep,
|
||||
],
|
||||
[apolloClient, redirectToWorkspaceDomain, setSignInUpStep],
|
||||
);
|
||||
|
||||
const handleGetLoginTokenFromCredentials = useCallback(
|
||||
@@ -264,7 +257,6 @@ export const useAuth = () => {
|
||||
await navigateAfterMultiWorkspaceSignInUp(
|
||||
user.availableWorkspaces,
|
||||
user.email,
|
||||
{ newTab: false },
|
||||
);
|
||||
},
|
||||
[
|
||||
@@ -363,7 +355,6 @@ export const useAuth = () => {
|
||||
await navigateAfterMultiWorkspaceSignInUp(
|
||||
user.availableWorkspaces,
|
||||
user.email,
|
||||
{ newTab: false },
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -418,7 +409,6 @@ export const useAuth = () => {
|
||||
await navigateAfterMultiWorkspaceSignInUp(
|
||||
user.availableWorkspaces,
|
||||
user.email,
|
||||
{ newTab: false },
|
||||
);
|
||||
},
|
||||
[
|
||||
|
||||
+31
-36
@@ -11,7 +11,6 @@ import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboar
|
||||
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/internal/SignInUpWithCredentials';
|
||||
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
|
||||
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
|
||||
import { SignInUpWorkspaceCreationForm } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm';
|
||||
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import {
|
||||
@@ -218,42 +217,38 @@ export const SignInUpGlobalScopeForm = () => {
|
||||
</StyledWorkspaceContainer>
|
||||
</StyledOnboardingContentContainer>
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.WorkspaceCreation && (
|
||||
<SignInUpWorkspaceCreationForm />
|
||||
{signInUpStep !== SignInUpStep.WorkspaceSelection && (
|
||||
<StyledOnboardingContentContainer>
|
||||
{authProviders.google && (
|
||||
<SignInUpWithGoogle
|
||||
action="list-available-workspaces"
|
||||
isGlobalScope
|
||||
/>
|
||||
)}
|
||||
{authProviders.microsoft && (
|
||||
<SignInUpWithMicrosoft
|
||||
action="list-available-workspaces"
|
||||
isGlobalScope
|
||||
/>
|
||||
)}
|
||||
{(authProviders.google || authProviders.microsoft) && (
|
||||
<HorizontalSeparator />
|
||||
)}
|
||||
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FormProvider {...form}>
|
||||
<SignInUpWithCredentials isGlobalScope />
|
||||
</FormProvider>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<StyledForgotPasswordLinkContainer>
|
||||
<ClickToActionLink
|
||||
onClick={handleResetPassword(form.getValues('email'))}
|
||||
>
|
||||
<Trans>Forgot your password?</Trans>
|
||||
</ClickToActionLink>
|
||||
</StyledForgotPasswordLinkContainer>
|
||||
)}
|
||||
</StyledOnboardingContentContainer>
|
||||
)}
|
||||
{signInUpStep !== SignInUpStep.WorkspaceSelection &&
|
||||
signInUpStep !== SignInUpStep.WorkspaceCreation && (
|
||||
<StyledOnboardingContentContainer>
|
||||
{authProviders.google && (
|
||||
<SignInUpWithGoogle
|
||||
action="list-available-workspaces"
|
||||
isGlobalScope
|
||||
/>
|
||||
)}
|
||||
{authProviders.microsoft && (
|
||||
<SignInUpWithMicrosoft
|
||||
action="list-available-workspaces"
|
||||
isGlobalScope
|
||||
/>
|
||||
)}
|
||||
{(authProviders.google || authProviders.microsoft) && (
|
||||
<HorizontalSeparator />
|
||||
)}
|
||||
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FormProvider {...form}>
|
||||
<SignInUpWithCredentials isGlobalScope />
|
||||
</FormProvider>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<StyledForgotPasswordLinkContainer>
|
||||
<ClickToActionLink
|
||||
onClick={handleResetPassword(form.getValues('email'))}
|
||||
>
|
||||
<Trans>Forgot your password?</Trans>
|
||||
</ClickToActionLink>
|
||||
</StyledForgotPasswordLinkContainer>
|
||||
)}
|
||||
</StyledOnboardingContentContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
-1
@@ -23,7 +23,6 @@ export const SignInUpGlobalScopeFormEffect = () => {
|
||||
await navigateAfterMultiWorkspaceSignInUp(
|
||||
user.availableWorkspaces,
|
||||
user.email,
|
||||
{ newTab: false },
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+88
-37
@@ -2,14 +2,17 @@ import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer';
|
||||
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
|
||||
import { useWorkspaceSubdomainField } from '@/auth/sign-in-up/hooks/useWorkspaceSubdomainField';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||
import { ImageInput } from '@/ui/input/components/ImageInput';
|
||||
import { InputHint } from '@/ui/input/components/InputHint';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { styled } from '@linaria/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
@@ -47,8 +50,15 @@ export const SignInUpWorkspaceCreationForm = () => {
|
||||
const { t } = useLingui();
|
||||
const { createWorkspace } = useSignUpInNewWorkspace();
|
||||
const { frontDomain } = useAtomStateValue(domainConfigurationState);
|
||||
const isMultiWorkspaceEnabled = useAtomStateValue(
|
||||
isMultiWorkspaceEnabledState,
|
||||
);
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [logo, setLogo] = useState<File | undefined>(undefined);
|
||||
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const {
|
||||
workspaceName,
|
||||
@@ -60,10 +70,37 @@ export const SignInUpWorkspaceCreationForm = () => {
|
||||
handleWorkspaceNameChange,
|
||||
handleSubdomainChange,
|
||||
applySuggestion,
|
||||
} = useWorkspaceSubdomainField();
|
||||
} = useWorkspaceSubdomainField({
|
||||
isSubdomainEnabled: isMultiWorkspaceEnabled,
|
||||
});
|
||||
|
||||
const isContinueDisabled =
|
||||
workspaceName.trim() === '' || !isAvailable || isSubmitting;
|
||||
workspaceName.trim() === '' ||
|
||||
isSubmitting ||
|
||||
(isMultiWorkspaceEnabled && !isAvailable);
|
||||
|
||||
const handleLogoUpload = (file: File) => {
|
||||
if (!isDefined(file)) {
|
||||
return;
|
||||
}
|
||||
setLogo(file);
|
||||
setLogoPreviewUrl(URL.createObjectURL(file));
|
||||
};
|
||||
|
||||
const handleLogoRemove = () => {
|
||||
setLogo(undefined);
|
||||
setLogoPreviewUrl(undefined);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(logoPreviewUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return () => {
|
||||
URL.revokeObjectURL(logoPreviewUrl);
|
||||
};
|
||||
}, [logoPreviewUrl]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isContinueDisabled) {
|
||||
@@ -74,8 +111,8 @@ export const SignInUpWorkspaceCreationForm = () => {
|
||||
try {
|
||||
await createWorkspace({
|
||||
displayName: workspaceName.trim(),
|
||||
subdomain,
|
||||
newTab: false,
|
||||
...(isMultiWorkspaceEnabled ? { subdomain } : {}),
|
||||
logo,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@@ -104,8 +141,18 @@ export const SignInUpWorkspaceCreationForm = () => {
|
||||
return (
|
||||
<StyledOnboardingContentContainer>
|
||||
<SubTitle>
|
||||
{t`Pick a name and a web address for your new workspace.`}
|
||||
{isMultiWorkspaceEnabled
|
||||
? t`Pick a name and a web address for your new workspace.`
|
||||
: t`Pick a name and a logo for your new workspace.`}
|
||||
</SubTitle>
|
||||
<StyledSection>
|
||||
<InputLabel>{t`Workspace logo`}</InputLabel>
|
||||
<ImageInput
|
||||
picture={logoPreviewUrl}
|
||||
onUpload={handleLogoUpload}
|
||||
onRemove={handleLogoRemove}
|
||||
/>
|
||||
</StyledSection>
|
||||
<StyledSection>
|
||||
<TextInput
|
||||
autoFocus
|
||||
@@ -117,37 +164,41 @@ export const SignInUpWorkspaceCreationForm = () => {
|
||||
fullWidth
|
||||
/>
|
||||
</StyledSection>
|
||||
<StyledSection>
|
||||
<TextInput
|
||||
label={t`Workspace address`}
|
||||
value={subdomain}
|
||||
placeholder={t`apple`}
|
||||
onChange={handleSubdomainChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
rightAdornment={
|
||||
isNonEmptyString(frontDomain) ? `.${frontDomain}` : undefined
|
||||
}
|
||||
error={subdomainError}
|
||||
noErrorHelper={status === 'unavailable' || !isDefined(subdomainError)}
|
||||
fullWidth
|
||||
/>
|
||||
{status === 'checking' && <InputHint>{t`Checking…`}</InputHint>}
|
||||
{status === 'available' && (
|
||||
<StyledAvailableHint>
|
||||
{t`This address is available`}
|
||||
</StyledAvailableHint>
|
||||
)}
|
||||
{status === 'unavailable' && (
|
||||
<StyledUnavailableHint>
|
||||
{subdomainError}
|
||||
{isDefined(suggestion) && (
|
||||
<ClickToActionLink onClick={applySuggestion}>
|
||||
{t`Use ${suggestion} instead`}
|
||||
</ClickToActionLink>
|
||||
)}
|
||||
</StyledUnavailableHint>
|
||||
)}
|
||||
</StyledSection>
|
||||
{isMultiWorkspaceEnabled && (
|
||||
<StyledSection>
|
||||
<TextInput
|
||||
label={t`Workspace address`}
|
||||
value={subdomain}
|
||||
placeholder={t`apple`}
|
||||
onChange={handleSubdomainChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
rightAdornment={
|
||||
isNonEmptyString(frontDomain) ? `.${frontDomain}` : undefined
|
||||
}
|
||||
error={subdomainError}
|
||||
noErrorHelper={
|
||||
status === 'unavailable' || !isDefined(subdomainError)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
{status === 'checking' && <InputHint>{t`Checking…`}</InputHint>}
|
||||
{status === 'available' && (
|
||||
<StyledAvailableHint>
|
||||
{t`This address is available`}
|
||||
</StyledAvailableHint>
|
||||
)}
|
||||
{status === 'unavailable' && (
|
||||
<StyledUnavailableHint>
|
||||
{subdomainError}
|
||||
{isDefined(suggestion) && (
|
||||
<ClickToActionLink onClick={applySuggestion}>
|
||||
{t`Use ${suggestion} instead`}
|
||||
</ClickToActionLink>
|
||||
)}
|
||||
</StyledUnavailableHint>
|
||||
)}
|
||||
</StyledSection>
|
||||
)}
|
||||
<StyledButtonContainer>
|
||||
<MainButton
|
||||
title={t`Continue`}
|
||||
|
||||
+141
-51
@@ -6,6 +6,7 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { ThemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { SignInUpWorkspaceCreationForm } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import {
|
||||
jotaiStore,
|
||||
resetJotaiStore,
|
||||
@@ -26,8 +27,15 @@ jest.mock('@/auth/sign-in-up/hooks/useWorkspaceSubdomainField', () => ({
|
||||
useWorkspaceSubdomainField: () => useWorkspaceSubdomainFieldMock(),
|
||||
}));
|
||||
|
||||
global.URL.createObjectURL = jest.fn(() => 'blob:logo-preview');
|
||||
global.URL.revokeObjectURL = jest.fn();
|
||||
|
||||
dynamicActivate(SOURCE_LOCALE);
|
||||
|
||||
const setMultiWorkspaceEnabled = (isEnabled: boolean) => {
|
||||
jotaiStore.set(isMultiWorkspaceEnabledState.atom, isEnabled);
|
||||
};
|
||||
|
||||
const renderForm = () =>
|
||||
render(
|
||||
<JotaiProvider store={jotaiStore}>
|
||||
@@ -56,73 +64,155 @@ describe('SignInUpWorkspaceCreationForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps Continue disabled until a workspace name is entered', () => {
|
||||
useWorkspaceSubdomainFieldMock.mockReturnValue({
|
||||
workspaceName: '',
|
||||
subdomain: '',
|
||||
status: 'idle',
|
||||
errorMessage: undefined,
|
||||
suggestion: undefined,
|
||||
isAvailable: false,
|
||||
handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
|
||||
handleSubdomainChange: handleSubdomainChangeMock,
|
||||
applySuggestion: applySuggestionMock,
|
||||
describe('multi-workspace', () => {
|
||||
beforeEach(() => {
|
||||
setMultiWorkspaceEnabled(true);
|
||||
});
|
||||
|
||||
renderForm();
|
||||
it('keeps Continue disabled until a workspace name is entered', () => {
|
||||
useWorkspaceSubdomainFieldMock.mockReturnValue({
|
||||
workspaceName: '',
|
||||
subdomain: '',
|
||||
status: 'idle',
|
||||
errorMessage: undefined,
|
||||
suggestion: undefined,
|
||||
isAvailable: false,
|
||||
handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
|
||||
handleSubdomainChange: handleSubdomainChangeMock,
|
||||
applySuggestion: applySuggestionMock,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
|
||||
});
|
||||
renderForm();
|
||||
|
||||
it('creates the workspace with the chosen name and address in the same tab', async () => {
|
||||
createWorkspaceMock.mockResolvedValue(undefined);
|
||||
|
||||
renderForm();
|
||||
|
||||
const continueButton = screen.getByRole('button', { name: 'Continue' });
|
||||
|
||||
expect(continueButton).toBeEnabled();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(continueButton);
|
||||
expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
|
||||
});
|
||||
|
||||
expect(createWorkspaceMock).toHaveBeenCalledWith({
|
||||
displayName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
newTab: false,
|
||||
it('creates the workspace with the chosen name and address in the same tab', async () => {
|
||||
createWorkspaceMock.mockResolvedValue(undefined);
|
||||
|
||||
renderForm();
|
||||
|
||||
const continueButton = screen.getByRole('button', { name: 'Continue' });
|
||||
|
||||
expect(continueButton).toBeEnabled();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(continueButton);
|
||||
});
|
||||
|
||||
expect(createWorkspaceMock).toHaveBeenCalledWith({
|
||||
displayName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
logo: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the picked logo file when creating the workspace', async () => {
|
||||
createWorkspaceMock.mockResolvedValue(undefined);
|
||||
|
||||
const { container } = renderForm();
|
||||
|
||||
const fileInput = container.querySelector(
|
||||
'input[type="file"]',
|
||||
) as HTMLInputElement;
|
||||
const logoFile = new File(['logo'], 'logo.png', { type: 'image/png' });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(fileInput, { target: { files: [logoFile] } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
});
|
||||
|
||||
expect(createWorkspaceMock).toHaveBeenCalledWith({
|
||||
displayName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
logo: logoFile,
|
||||
});
|
||||
});
|
||||
|
||||
it('routes name edits back through the field hook', () => {
|
||||
renderForm();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Workspace name'), {
|
||||
target: { value: 'Acme' },
|
||||
});
|
||||
|
||||
expect(handleWorkspaceNameChangeMock).toHaveBeenCalledWith('Acme');
|
||||
});
|
||||
|
||||
it('offers a one-click suggestion when the address is taken', () => {
|
||||
useWorkspaceSubdomainFieldMock.mockReturnValue({
|
||||
workspaceName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
status: 'unavailable',
|
||||
errorMessage: undefined,
|
||||
suggestion: 'apple-2',
|
||||
isAvailable: false,
|
||||
handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
|
||||
handleSubdomainChange: handleSubdomainChangeMock,
|
||||
applySuggestion: applySuggestionMock,
|
||||
});
|
||||
|
||||
renderForm();
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
|
||||
|
||||
fireEvent.click(screen.getByText('Use apple-2 instead'));
|
||||
|
||||
expect(applySuggestionMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('routes name edits back through the field hook', () => {
|
||||
renderForm();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Workspace name'), {
|
||||
target: { value: 'Acme' },
|
||||
describe('single-workspace', () => {
|
||||
beforeEach(() => {
|
||||
setMultiWorkspaceEnabled(false);
|
||||
});
|
||||
|
||||
expect(handleWorkspaceNameChangeMock).toHaveBeenCalledWith('Acme');
|
||||
});
|
||||
it('hides the workspace address field', () => {
|
||||
renderForm();
|
||||
|
||||
it('offers a one-click suggestion when the address is taken', () => {
|
||||
useWorkspaceSubdomainFieldMock.mockReturnValue({
|
||||
workspaceName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
status: 'unavailable',
|
||||
errorMessage: undefined,
|
||||
suggestion: 'apple-2',
|
||||
isAvailable: false,
|
||||
handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
|
||||
handleSubdomainChange: handleSubdomainChangeMock,
|
||||
applySuggestion: applySuggestionMock,
|
||||
expect(screen.getByLabelText('Workspace name')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText('Workspace address'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
renderForm();
|
||||
it('enables Continue based on the name only and creates without a subdomain', async () => {
|
||||
createWorkspaceMock.mockResolvedValue(undefined);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
|
||||
// An unavailable subdomain status must not block submission when the
|
||||
// address field is hidden.
|
||||
useWorkspaceSubdomainFieldMock.mockReturnValue({
|
||||
workspaceName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
status: 'unavailable',
|
||||
errorMessage: undefined,
|
||||
suggestion: 'apple-2',
|
||||
isAvailable: false,
|
||||
handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
|
||||
handleSubdomainChange: handleSubdomainChangeMock,
|
||||
applySuggestion: applySuggestionMock,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Use apple-2 instead'));
|
||||
renderForm();
|
||||
|
||||
expect(applySuggestionMock).toHaveBeenCalledTimes(1);
|
||||
const continueButton = screen.getByRole('button', { name: 'Continue' });
|
||||
|
||||
expect(continueButton).toBeEnabled();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(continueButton);
|
||||
});
|
||||
|
||||
expect(createWorkspaceMock).toHaveBeenCalledWith({
|
||||
displayName: 'Apple',
|
||||
logo: undefined,
|
||||
});
|
||||
expect(createWorkspaceMock.mock.calls[0][0]).not.toHaveProperty(
|
||||
'subdomain',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+16
@@ -94,6 +94,22 @@ describe('useWorkspaceSubdomainField', () => {
|
||||
expect(result.current.isAvailable).toBe(true);
|
||||
});
|
||||
|
||||
it('skips availability checks when the subdomain field is disabled', () => {
|
||||
const { result } = renderHook(
|
||||
() => useWorkspaceSubdomainField({ isSubdomainEnabled: false }),
|
||||
{ wrapper: createWrapper([]) },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.handleWorkspaceNameChange('Apple');
|
||||
});
|
||||
|
||||
// The name is still tracked, but no subdomain is derived and no lookup runs.
|
||||
expect(result.current.workspaceName).toBe('Apple');
|
||||
expect(result.current.subdomain).toBe('');
|
||||
expect(result.current.status).toBe('idle');
|
||||
});
|
||||
|
||||
it('reports an unavailable manual address and offers a suggestion', async () => {
|
||||
const { result } = renderHook(() => useWorkspaceSubdomainField(), {
|
||||
wrapper: createWrapper([
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { useBuildSearchParamsFromUrlSyncedStates } from '@/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates';
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@@ -24,6 +25,7 @@ import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
@@ -33,6 +35,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
const [signInUpStep, setSignInUpStep] = useAtomState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useAtomState(signInUpModeState);
|
||||
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
|
||||
const isMultiWorkspaceEnabled = useAtomStateValue(
|
||||
isMultiWorkspaceEnabledState,
|
||||
);
|
||||
const { isCaptchaReady } = useCaptcha();
|
||||
const setLastAuthenticatedMethod = useSetAtomState(
|
||||
lastAuthenticatedMethodState,
|
||||
@@ -156,7 +161,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
if (
|
||||
!isInviteMode &&
|
||||
signInUpMode === SignInUpMode.SignUp &&
|
||||
!isOnAWorkspace
|
||||
(!isOnAWorkspace || !isMultiWorkspaceEnabled)
|
||||
) {
|
||||
return await signUpWithCredentials(
|
||||
data.email.toLowerCase().trim(),
|
||||
@@ -198,6 +203,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
enqueueErrorSnackBar,
|
||||
buildSearchParamsFromUrlSyncedStates,
|
||||
isOnAWorkspace,
|
||||
isMultiWorkspaceEnabled,
|
||||
setLastAuthenticatedMethod,
|
||||
t,
|
||||
],
|
||||
|
||||
@@ -1,43 +1,82 @@
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { SignUpInNewWorkspaceDocument } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
SignUpInNewWorkspaceDocument,
|
||||
UploadNewWorkspaceLogoDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const useSignUpInNewWorkspace = () => {
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const { getAuthTokensFromLoginToken } = useAuth();
|
||||
const isMultiWorkspaceEnabled = useAtomStateValue(
|
||||
isMultiWorkspaceEnabledState,
|
||||
);
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [signUpInNewWorkspaceMutation] = useMutation(
|
||||
SignUpInNewWorkspaceDocument,
|
||||
);
|
||||
const [uploadNewWorkspaceLogoMutation] = useMutation(
|
||||
UploadNewWorkspaceLogoDocument,
|
||||
);
|
||||
|
||||
const createWorkspace = async ({
|
||||
displayName,
|
||||
subdomain,
|
||||
newTab = true,
|
||||
logo,
|
||||
}: {
|
||||
displayName?: string;
|
||||
subdomain?: string;
|
||||
newTab?: boolean;
|
||||
logo?: File;
|
||||
} = {}) => {
|
||||
try {
|
||||
const { data } = await signUpInNewWorkspaceMutation({
|
||||
variables: { input: { displayName, subdomain } },
|
||||
});
|
||||
assertIsDefinedOrThrow(data?.signUpInNewWorkspace);
|
||||
|
||||
const workspaceId = data.signUpInNewWorkspace.workspace.id;
|
||||
|
||||
if (isDefined(logo)) {
|
||||
try {
|
||||
await uploadNewWorkspaceLogoMutation({
|
||||
variables: { workspaceId, file: logo },
|
||||
});
|
||||
} catch (logoUploadError) {
|
||||
enqueueErrorSnackBar(
|
||||
CombinedGraphQLErrors.is(logoUploadError)
|
||||
? { apolloError: logoUploadError }
|
||||
: {
|
||||
message:
|
||||
logoUploadError instanceof Error
|
||||
? logoUploadError.message
|
||||
: t`Workspace logo upload failed`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const loginToken = data.signUpInNewWorkspace.loginToken.token;
|
||||
|
||||
if (!isMultiWorkspaceEnabled) {
|
||||
return await getAuthTokensFromLoginToken(loginToken);
|
||||
}
|
||||
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls),
|
||||
AppPath.Verify,
|
||||
{
|
||||
loginToken: data.signUpInNewWorkspace.loginToken.token,
|
||||
},
|
||||
newTab ? '_blank' : '_self',
|
||||
{ loginToken },
|
||||
'_self',
|
||||
);
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar(
|
||||
|
||||
+4
-2
@@ -21,7 +21,9 @@ export type SubdomainFieldStatus =
|
||||
|
||||
const AVAILABILITY_CHECK_DEBOUNCE_MS = 400;
|
||||
|
||||
export const useWorkspaceSubdomainField = () => {
|
||||
export const useWorkspaceSubdomainField = ({
|
||||
isSubdomainEnabled = true,
|
||||
}: { isSubdomainEnabled?: boolean } = {}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
const subdomainSchema = useMemo(() => getSubdomainValidationSchema(), []);
|
||||
|
||||
@@ -118,7 +120,7 @@ export const useWorkspaceSubdomainField = () => {
|
||||
const handleWorkspaceNameChange = (name: string) => {
|
||||
setWorkspaceName(name);
|
||||
|
||||
if (isManuallyEdited) {
|
||||
if (!isSubdomainEnabled || isManuallyEdited) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('isValidReturnToPath', () => {
|
||||
});
|
||||
|
||||
it('should return false for onboarding paths', () => {
|
||||
expect(isValidReturnToPath('/create/workspace')).toBe(false);
|
||||
expect(isValidReturnToPath('/workspace-activation')).toBe(false);
|
||||
expect(isValidReturnToPath('/create/profile')).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
+12
-1
@@ -12,7 +12,10 @@ export const useRedirectToDefaultDomain = () => {
|
||||
const store = useStore();
|
||||
|
||||
const { redirect } = useRedirect();
|
||||
const redirectToDefaultDomain = () => {
|
||||
const redirectToDefaultDomain = (options?: {
|
||||
pathname?: string;
|
||||
searchParams?: Record<string, string>;
|
||||
}) => {
|
||||
const url = new URL(window.location.href);
|
||||
if (url.hostname !== defaultDomain) {
|
||||
setLastAuthenticateWorkspaceDomain(null);
|
||||
@@ -25,6 +28,14 @@ export const useRedirectToDefaultDomain = () => {
|
||||
url.searchParams.set('returnToPath', returnToPath);
|
||||
}
|
||||
|
||||
if (isNonEmptyString(options?.pathname)) {
|
||||
url.pathname = options.pathname;
|
||||
}
|
||||
|
||||
Object.entries(options?.searchParams ?? {}).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value);
|
||||
});
|
||||
|
||||
url.hostname = defaultDomain;
|
||||
redirect(url.toString());
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ export const MinimalMetadataGater = ({ children }: React.PropsWithChildren) => {
|
||||
isMatchingLocation(location, AppPath.SignInUp) ||
|
||||
isMatchingLocation(location, AppPath.Invite) ||
|
||||
isMatchingLocation(location, AppPath.ResetPassword) ||
|
||||
isMatchingLocation(location, AppPath.CreateWorkspace) ||
|
||||
isMatchingLocation(location, AppPath.WorkspaceActivation) ||
|
||||
isMatchingLocation(location, AppPath.PlanRequired) ||
|
||||
isMatchingLocation(location, AppPath.PlanRequiredSuccess) ||
|
||||
isMatchingLocation(location, AppPath.Authorize);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export enum PageFocusId {
|
||||
Settings = 'settings',
|
||||
CreateWorkspace = 'create-workspace',
|
||||
WorkspaceActivation = 'workspace-activation',
|
||||
SignInUp = 'sign-in-up',
|
||||
CreateProfile = 'create-profile',
|
||||
InviteTeam = 'invite-team',
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ const testCases = [
|
||||
{ loc: AppPath.SignInUp, res: true },
|
||||
{ loc: AppPath.Invite, res: true },
|
||||
{ loc: AppPath.ResetPassword, res: true },
|
||||
{ loc: AppPath.CreateWorkspace, res: true },
|
||||
{ loc: AppPath.WorkspaceActivation, res: true },
|
||||
{ loc: AppPath.SyncEmails, res: true },
|
||||
{ loc: AppPath.InviteTeam, res: true },
|
||||
{ loc: AppPath.PlanRequired, res: true },
|
||||
|
||||
@@ -17,7 +17,7 @@ export const useShowAuthModal = () => {
|
||||
isMatchingLocation(location, AppPath.VerifyEmail) ||
|
||||
isMatchingLocation(location, AppPath.Verify) ||
|
||||
isMatchingLocation(location, AppPath.SignInUp) ||
|
||||
isMatchingLocation(location, AppPath.CreateWorkspace) ||
|
||||
isMatchingLocation(location, AppPath.WorkspaceActivation) ||
|
||||
isMatchingLocation(location, AppPath.PlanRequired) ||
|
||||
isMatchingLocation(location, AppPath.PlanRequiredSuccess) ||
|
||||
isMatchingLocation(location, AppPath.BookCallDecision) ||
|
||||
|
||||
+10
-28
@@ -6,8 +6,8 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
import { useRedirectToDefaultDomain } from '@/domain-manager/hooks/useRedirectToDefaultDomain';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
@@ -21,7 +21,6 @@ import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/s
|
||||
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
@@ -44,11 +43,7 @@ import {
|
||||
MenuItemSelectAvatar,
|
||||
UndecoratedLink,
|
||||
} from 'twenty-ui/navigation';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import {
|
||||
type AvailableWorkspace,
|
||||
SignUpInNewWorkspaceDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type AvailableWorkspace } from '~/generated-metadata/graphql';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
@@ -64,19 +59,15 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
const availableWorkspacesCount =
|
||||
countAvailableWorkspaces(availableWorkspaces);
|
||||
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
|
||||
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
const { signOut } = useAuth();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { colorScheme, colorSchemeList } = useColorScheme();
|
||||
const supportChat = useAtomStateValue(supportChatState);
|
||||
const isSupportChatConfigured =
|
||||
supportChat?.supportDriver === 'FRONT' &&
|
||||
isNonEmptyString(supportChat.supportFrontChatId);
|
||||
|
||||
const [signUpInNewWorkspaceMutation] = useMutation(
|
||||
SignUpInNewWorkspaceDocument,
|
||||
);
|
||||
|
||||
const setMultiWorkspaceDropdown = useSetAtomState(
|
||||
multiWorkspaceDropdownState,
|
||||
);
|
||||
@@ -94,23 +85,14 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// The workspace name (and logo + subdomain) are collected by the shared
|
||||
// creation form on the root domain, so we send the user there on the
|
||||
// WorkspaceCreation step instead of creating a nameless workspace on the fly.
|
||||
const createWorkspace = () => {
|
||||
signUpInNewWorkspaceMutation({
|
||||
onCompleted: async (data) => {
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls),
|
||||
AppPath.Verify,
|
||||
{
|
||||
loginToken: data.signUpInNewWorkspace.loginToken.token,
|
||||
},
|
||||
'_blank',
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueErrorSnackBar({
|
||||
...(CombinedGraphQLErrors.is(error) ? { apolloError: error } : {}),
|
||||
});
|
||||
},
|
||||
closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
|
||||
redirectToDefaultDomain({
|
||||
pathname: AppPath.SignInUp,
|
||||
searchParams: { action: 'create-new-workspace' },
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user