Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1149e99
feat(auth): single-step workspace creation with logo
FelixMalfait Jun 17, 2026
915cdcd
fix: address review findings on workspace creation flow
FelixMalfait Jun 17, 2026
b972fa9
refactor: remove redundant comment in navigateAfterMultiWorkspaceSign…
FelixMalfait Jun 17, 2026
afbd55f
refactor: remove redundant comment in CreateWorkspace
FelixMalfait Jun 17, 2026
a8a094d
fix(auth): route single-workspace sign-up through the creation form
FelixMalfait Jun 17, 2026
9072e15
refactor(auth): name workspaces at creation, make activation pure pro…
FelixMalfait Jun 17, 2026
038e48d
refactor: drop non-essential comments and simplify isContinueDisabled
FelixMalfait Jun 17, 2026
35d0ed5
Merge remote-tracking branch 'origin/main' into claude/workspace-crea…
FelixMalfait Jun 17, 2026
24a449f
refactor: remove dead WorkspaceCreation branch from SignInUpGlobalSco…
FelixMalfait Jun 17, 2026
a33c95b
refactor: drop dead newTab param from createWorkspace
FelixMalfait Jun 17, 2026
21993cf
Merge branch 'main' into claude/workspace-creation-logo-step-1
FelixMalfait Jun 18, 2026
171a2de
fix(auth): route authenticated users with no current workspace to Sig…
FelixMalfait Jun 18, 2026
b78635e
Merge branch 'main' into claude/workspace-creation-logo-step-1
FelixMalfait Jun 18, 2026
768b4eb
fix(workspace): reset activation status on failure so retry can recover
FelixMalfait Jun 18, 2026
58d9fbc
refactor(front): rename CreateWorkspace onboarding step to WorkspaceA…
FelixMalfait Jun 18, 2026
f3b9319
fix(workspace): make workspace activation safe to re-run
FelixMalfait Jun 18, 2026
43b06e6
Merge remote-tracking branch 'origin/main' into claude/workspace-crea…
FelixMalfait Jun 18, 2026
7956923
chore(client-sdk): regenerate metadata client for uploadNewWorkspaceLogo
FelixMalfait Jun 18, 2026
7893af8
fix(workspace): harden activation recovery and idempotency edges
FelixMalfait Jun 18, 2026
477a180
fix(front): type onNodeDragStop as OnNodeDrag to satisfy @xyflow types
FelixMalfait Jun 18, 2026
645ae10
feat(workspace): reclaim crashed activations; harden retry and SSO paths
FelixMalfait Jun 18, 2026
60ae751
fix(workspace): make activateWorkspace idempotent for the active state
FelixMalfait Jun 18, 2026
ee4a6c9
refactor(workspace): remove vestigial activation-time displayName
FelixMalfait Jun 18, 2026
d2186a9
refactor(workspace): retire activation displayName non-breakingly
FelixMalfait Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3394,6 +3394,7 @@ type Mutation {
signUp(email: String!, password: String!, captchaToken: String, locale: String, verifyEmailRedirectPath: String): AvailableWorkspacesAndAccessTokens!
signUpInWorkspace(email: String!, password: String!, workspaceId: UUID, workspaceInviteHash: String, workspacePersonalInviteToken: String, captchaToken: String, locale: String, verifyEmailRedirectPath: String): SignUp!
signUpInNewWorkspace(input: SignUpInNewWorkspaceInput): SignUp!
uploadNewWorkspaceLogo(workspaceId: String!, file: Upload!): FileWithSignedUrl!
generateTransientToken: TransientToken!
getAuthTokensFromLoginToken(loginToken: String!, origin: String!): AuthTokens!
authorizeApp(clientId: String!, codeChallenge: String, redirectUrl: String!, state: String, scope: String): AuthorizeApp!
Expand Down
2 changes: 2 additions & 0 deletions packages/twenty-client-sdk/src/metadata/generated/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2916,6 +2916,7 @@ export interface Mutation {
signUp: AvailableWorkspacesAndAccessTokens
signUpInWorkspace: SignUp
signUpInNewWorkspace: SignUp
uploadNewWorkspaceLogo: FileWithSignedUrl
generateTransientToken: TransientToken
getAuthTokensFromLoginToken: AuthTokens
authorizeApp: AuthorizeApp
Expand Down Expand Up @@ -6094,6 +6095,7 @@ export interface MutationGenqlSelection{
signUp?: (AvailableWorkspacesAndAccessTokensGenqlSelection & { __args: {email: Scalars['String'], password: Scalars['String'], captchaToken?: (Scalars['String'] | null), locale?: (Scalars['String'] | null), verifyEmailRedirectPath?: (Scalars['String'] | null)} })
signUpInWorkspace?: (SignUpGenqlSelection & { __args: {email: Scalars['String'], password: Scalars['String'], workspaceId?: (Scalars['UUID'] | null), workspaceInviteHash?: (Scalars['String'] | null), workspacePersonalInviteToken?: (Scalars['String'] | null), captchaToken?: (Scalars['String'] | null), locale?: (Scalars['String'] | null), verifyEmailRedirectPath?: (Scalars['String'] | null)} })
signUpInNewWorkspace?: (SignUpGenqlSelection & { __args?: {input?: (SignUpInNewWorkspaceInput | null)} })
uploadNewWorkspaceLogo?: (FileWithSignedUrlGenqlSelection & { __args: {workspaceId: Scalars['String'], file: Scalars['Upload']} })
generateTransientToken?: TransientTokenGenqlSelection
getAuthTokensFromLoginToken?: (AuthTokensGenqlSelection & { __args: {loginToken: Scalars['String'], origin: Scalars['String']} })
authorizeApp?: (AuthorizeAppGenqlSelection & { __args: {clientId: Scalars['String'], codeChallenge?: (Scalars['String'] | null), redirectUrl: Scalars['String'], state?: (Scalars['String'] | null), scope?: (Scalars['String'] | null)} })
Expand Down
13 changes: 13 additions & 0 deletions packages/twenty-client-sdk/src/metadata/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8454,6 +8454,19 @@ export default {
]
}
],
"uploadNewWorkspaceLogo": [
131,
{
"workspaceId": [
1,
"String!"
],
"file": [
356,
"Upload!"
]
}
],
"generateTransientToken": [
250
],
Expand Down
16 changes: 16 additions & 0 deletions packages/twenty-front/src/generated-metadata/graphql.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/twenty-front/src/modules/auth/components/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const Logo = ({
{isUsingDefaultLogo ? (
<UndecoratedLink
to={AppPath.SignInUp}
onClick={redirectToDefaultDomain}
onClick={() => redirectToDefaultDomain()}
>
<StyledPrimaryLogo
style={{ backgroundImage: `url(${primaryLogoUrl})` }}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
`;
26 changes: 8 additions & 18 deletions packages/twenty-front/src/modules/auth/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
});
Expand All @@ -166,13 +165,7 @@ export const useAuth = () => {

setSignInUpStep(SignInUpStep.WorkspaceSelection);
},
[
apolloClient,
createWorkspace,
isMultiWorkspaceEnabled,
redirectToWorkspaceDomain,
setSignInUpStep,
],
[apolloClient, redirectToWorkspaceDomain, setSignInUpStep],
);

const handleGetLoginTokenFromCredentials = useCallback(
Expand Down Expand Up @@ -264,7 +257,6 @@ export const useAuth = () => {
await navigateAfterMultiWorkspaceSignInUp(
user.availableWorkspaces,
user.email,
{ newTab: false },
);
},
[
Expand Down Expand Up @@ -363,7 +355,6 @@ export const useAuth = () => {
await navigateAfterMultiWorkspaceSignInUp(
user.availableWorkspaces,
user.email,
{ newTab: false },
);
},
onError: (error) => {
Expand Down Expand Up @@ -418,7 +409,6 @@ export const useAuth = () => {
await navigateAfterMultiWorkspaceSignInUp(
user.availableWorkspaces,
user.email,
{ newTab: false },
);
},
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export const SignInUpGlobalScopeFormEffect = () => {
await navigateAfterMultiWorkspaceSignInUp(
user.availableWorkspaces,
user.email,
{ newTab: false },
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
undefined,
);

const {
workspaceName,
Expand All @@ -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) {
Expand All @@ -74,7 +111,8 @@ export const SignInUpWorkspaceCreationForm = () => {
try {
await createWorkspace({
displayName: workspaceName.trim(),
subdomain,
...(isMultiWorkspaceEnabled ? { subdomain } : {}),
logo,
newTab: false,
});
} finally {
Expand Down Expand Up @@ -104,8 +142,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
Expand All @@ -117,37 +165,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 && (
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
<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`}
Expand Down
Loading
Loading