660536d6bb
## Summary
- **Fix create-profile modal not showing after workspace creation**:
After activating a workspace, `CreateWorkspace.onSubmit` called
`refreshObjectMetadataItems()` which only updated the
`objectMetadataItemsState` atom but never marked the metadata store as
ready (`metadataStoreState` stayed at `'empty'`). Since `MetadataGater`
excludes `CreateWorkspace` but not `CreateProfile` from its loading
check, navigating to `/create/profile` triggered the skeleton loader
instead of the modal. The fix adds the full metadata pipeline after
refresh — `updateDraft('objectMetadataItems')` + `applyChanges()` for
objects, and `fetchAndLoadIndexViews()` for views — so
`isAppMetadataReady` is `true` before navigation.
- **Fix invite-team "Skip" not persisting to server**: Clicking "Skip"
on the invite-team page called `setNextOnboardingStatus()` which only
updated the local Jotai atom. The early return for empty emails bypassed
`sendInvitation`, so the server never cleared the
`ONBOARDING_INVITE_TEAM_PENDING` user var. On page refresh,
`GetCurrentUser` returned `INVITE_TEAM` and the user was stuck. The fix
removes the early return so `sendInvitation({ emails: [] })` always runs
— the server handles empty arrays fine and clears the pending flag.
263 lines
8.4 KiB
TypeScript
263 lines
8.4 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { useCallback, useState } from 'react';
|
|
import { Controller, type SubmitHandler, useForm } from 'react-hook-form';
|
|
import { Key } from 'ts-key-enum';
|
|
import { z } from 'zod';
|
|
|
|
import { Logo } from '@/auth/components/Logo';
|
|
import { SubTitle } from '@/auth/components/SubTitle';
|
|
import { Title } from '@/auth/components/Title';
|
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
|
import { useFetchAndLoadIndexViews } from '@/metadata-store/hooks/useFetchAndLoadIndexViews';
|
|
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
|
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
|
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
|
|
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
import { TextInput } from '@/ui/input/components/TextInput';
|
|
import { ModalContent } from 'twenty-ui/layout';
|
|
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
|
|
import { ApolloError } from '@apollo/client';
|
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
import { isNonEmptyString } from '@sniptt/guards';
|
|
import { useStore } from 'jotai';
|
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { H2Title } from 'twenty-ui/display';
|
|
import { Loader } from 'twenty-ui/feedback';
|
|
import { MainButton } from 'twenty-ui/input';
|
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
import { useActivateWorkspaceMutation } from '~/generated-metadata/graphql';
|
|
|
|
const StyledContentContainer = styled.div`
|
|
width: 100%;
|
|
`;
|
|
|
|
const StyledSectionContainer = styled.div`
|
|
margin-top: ${themeCssVariables.spacing[8]};
|
|
`;
|
|
|
|
const StyledButtonContainer = styled.div`
|
|
margin-top: ${themeCssVariables.spacing[8]};
|
|
width: 200px;
|
|
`;
|
|
|
|
const StyledLoaderContainer = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: ${themeCssVariables.spacing[8]};
|
|
margin-top: ${themeCssVariables.spacing[8]};
|
|
width: 100%;
|
|
`;
|
|
|
|
enum PendingCreationLoaderStep {
|
|
None = 'none',
|
|
Step1 = 'step-1',
|
|
Step2 = 'step-2',
|
|
Step3 = 'step-3',
|
|
}
|
|
|
|
const StyledPendingCreationLoader = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: center;
|
|
width: 100%;
|
|
`;
|
|
|
|
export const CreateWorkspace = () => {
|
|
const { t } = useLingui();
|
|
const { enqueueErrorSnackBar } = useSnackBar();
|
|
const setNextOnboardingStatus = useSetNextOnboardingStatus();
|
|
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
|
const { updateDraft, applyChanges } = useMetadataStore();
|
|
const { fetchAndLoadIndexViews } = useFetchAndLoadIndexViews();
|
|
const store = useStore();
|
|
|
|
const { loadCurrentUser } = useLoadCurrentUser();
|
|
const [activateWorkspace] = useActivateWorkspaceMutation();
|
|
const [pendingCreationLoaderStep, setPendingCreationLoaderStep] = useState(
|
|
PendingCreationLoaderStep.None,
|
|
);
|
|
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
|
|
|
const validationSchema = z
|
|
.object({
|
|
name: z.string().min(1, { message: t`Name can not be empty` }),
|
|
})
|
|
.required();
|
|
|
|
type Form = z.infer<typeof validationSchema>;
|
|
|
|
// Form
|
|
const {
|
|
control,
|
|
handleSubmit,
|
|
formState: { isValid, isSubmitting },
|
|
} = useForm<Form>({
|
|
mode: 'onChange',
|
|
defaultValues: {
|
|
name: '',
|
|
},
|
|
resolver: zodResolver(validationSchema),
|
|
});
|
|
|
|
const onSubmit: SubmitHandler<Form> = useCallback(
|
|
async (data) => {
|
|
try {
|
|
setTimeout(() => {
|
|
setPendingCreationLoaderStep(PendingCreationLoaderStep.Step1);
|
|
}, 500);
|
|
setTimeout(() => {
|
|
setPendingCreationLoaderStep(PendingCreationLoaderStep.Step2);
|
|
}, 2000);
|
|
setTimeout(() => {
|
|
setPendingCreationLoaderStep(PendingCreationLoaderStep.Step3);
|
|
}, 5000);
|
|
|
|
const result = await activateWorkspace({
|
|
variables: {
|
|
input: {
|
|
displayName: data.name,
|
|
},
|
|
},
|
|
});
|
|
if (isDefined(result.errors)) {
|
|
throw result.errors ?? new Error(t`Unknown error`);
|
|
}
|
|
|
|
await refreshObjectMetadataItems();
|
|
|
|
const loadedObjects = store.get(objectMetadataItemsState.atom);
|
|
updateDraft('objectMetadataItems', loadedObjects);
|
|
applyChanges();
|
|
|
|
await fetchAndLoadIndexViews();
|
|
|
|
await loadCurrentUser();
|
|
setNextOnboardingStatus();
|
|
} catch (error: any) {
|
|
setPendingCreationLoaderStep(PendingCreationLoaderStep.None);
|
|
|
|
enqueueErrorSnackBar({
|
|
apolloError: error instanceof ApolloError ? error : undefined,
|
|
});
|
|
}
|
|
},
|
|
[
|
|
activateWorkspace,
|
|
enqueueErrorSnackBar,
|
|
loadCurrentUser,
|
|
refreshObjectMetadataItems,
|
|
updateDraft,
|
|
applyChanges,
|
|
store,
|
|
fetchAndLoadIndexViews,
|
|
setNextOnboardingStatus,
|
|
t,
|
|
],
|
|
);
|
|
|
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
if (event.key === Key.Enter) {
|
|
event.preventDefault();
|
|
handleSubmit(onSubmit)();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ModalContent isVerticallyCentered isHorizontallyCentered>
|
|
{pendingCreationLoaderStep !== PendingCreationLoaderStep.None && (
|
|
<>
|
|
<Logo
|
|
primaryLogo={
|
|
isNonEmptyString(currentWorkspace?.logo)
|
|
? currentWorkspace?.logo
|
|
: undefined
|
|
}
|
|
/>
|
|
<Title>
|
|
<Trans>Creating your workspace</Trans>
|
|
</Title>
|
|
<StyledPendingCreationLoader>
|
|
{pendingCreationLoaderStep === PendingCreationLoaderStep.Step1 && (
|
|
<SubTitle>
|
|
<Trans>Setting up your database...</Trans>
|
|
</SubTitle>
|
|
)}
|
|
{pendingCreationLoaderStep === PendingCreationLoaderStep.Step2 && (
|
|
<SubTitle>
|
|
<Trans>Creating your data model...</Trans>
|
|
</SubTitle>
|
|
)}
|
|
{pendingCreationLoaderStep === PendingCreationLoaderStep.Step3 && (
|
|
<SubTitle>
|
|
<Trans>Prefilling your workspace data...</Trans>
|
|
</SubTitle>
|
|
)}
|
|
</StyledPendingCreationLoader>
|
|
<StyledLoaderContainer>
|
|
<Loader color="gray" />
|
|
</StyledLoaderContainer>
|
|
</>
|
|
)}
|
|
{pendingCreationLoaderStep === PendingCreationLoaderStep.None && (
|
|
<>
|
|
<Title noMarginTop>
|
|
<Trans>Create your workspace</Trans>
|
|
</Title>
|
|
<SubTitle>
|
|
<Trans>
|
|
A shared environment where you will be able to manage your
|
|
customer relations with your team.
|
|
</Trans>
|
|
</SubTitle>
|
|
|
|
<StyledContentContainer>
|
|
<StyledSectionContainer>
|
|
<H2Title title={t`Workspace logo`} />
|
|
<WorkspaceLogoUploader />
|
|
</StyledSectionContainer>
|
|
<StyledSectionContainer>
|
|
<H2Title
|
|
title={t`Workspace name`}
|
|
description={t`The name of your organization`}
|
|
/>
|
|
<Controller
|
|
name="name"
|
|
control={control}
|
|
render={({
|
|
field: { onChange, onBlur, value },
|
|
fieldState: { error },
|
|
}) => (
|
|
<TextInput
|
|
autoFocus
|
|
value={value}
|
|
placeholder={t`Apple`}
|
|
onBlur={onBlur}
|
|
onChange={onChange}
|
|
error={error?.message}
|
|
onKeyDown={handleKeyDown}
|
|
fullWidth
|
|
/>
|
|
)}
|
|
/>
|
|
</StyledSectionContainer>
|
|
</StyledContentContainer>
|
|
<StyledButtonContainer>
|
|
<MainButton
|
|
title={t`Continue`}
|
|
onClick={handleSubmit(onSubmit)}
|
|
disabled={!isValid || isSubmitting}
|
|
Icon={() => isSubmitting && <Loader />}
|
|
fullWidth
|
|
/>
|
|
</StyledButtonContainer>
|
|
</>
|
|
)}
|
|
</ModalContent>
|
|
);
|
|
};
|