3628 timebox separate user creation from workspace creation (#3737)

* Remove workspace schema creation from signUp

* Set user workspaceMember nullable

* Remove workspace creation

* Handle null workspace in tokens

* Update onboarding status

* Generate types

* Move createWorkspace to workspace resolver

* Create workspace after signup

* Update createWorkspace return type

* Update createWorkspace return type

* Create core.workspace at signup

* WIP

* Fix create workspace

* Fix create workspace

* Clean code

* Remove useless recoil set

* Simplify create workspace request

* Set currentWorkspace at login

* Fix tests

* Create a recoil value for is workspaceSchema created

* Rename createWorkspace to createWorkspaceSchema

* Code review returns

* Use AppPath when possible

* Try without state

* Fix

* Fixes

* Rename createWorkspaceSchema to activateWorkspace

* Remove defaultAvatarUrl from user

* Add defaultAvatarUrl to core user

This reverts commit 1701c30eb1.

* Add defaultAvatarUrl to core user

This reverts commit 1701c30eb1.

* Fix ci

* Fix tests

* Fix storybook

* Fix test

* Remove useless query

* Fix test

* Fix test

* Fix mock data

* Fix test

* Clean Mock Requests

* Fix tentative

* Revert "Clean Mock Requests"

This reverts commit 8aa20a3436.

* Fix

* Revert "Fix"

This reverts commit 2df7e9b656.

* Revert "Revert "Clean Mock Requests""

This reverts commit 3aefef8e96.

* Revert "Fix tentative"

This reverts commit 13e7748d6f.

* Update filename

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull 2024-02-09 12:06:11 +01:00 committed by GitHub
parent 3fc18aeec1
commit 7425223f83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 769 additions and 355 deletions

View File

@ -88,7 +88,7 @@ export const PageChangeEffect = () => {
) {
navigate(AppPath.PlanRequired);
} else if (
onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation &&
onboardingStatus === OnboardingStatus.OngoingWorkspaceActivation &&
!isMatchingLocation(AppPath.CreateWorkspace)
) {
navigate(AppPath.CreateWorkspace);
@ -101,7 +101,7 @@ export const PageChangeEffect = () => {
onboardingStatus === OnboardingStatus.Completed &&
isMatchingOnboardingRoute
) {
navigate('/');
navigate(AppPath.Index);
} else if (isMatchingLocation(AppPath.Invite)) {
const inviteHash =
matchPath({ path: '/invite/:workspaceInviteHash' }, location.pathname)

View File

@ -19,6 +19,10 @@ export type Scalars = {
Upload: any;
};
export type ActivateWorkspaceInput = {
displayName?: InputMaybe<Scalars['String']>;
};
export type Analytics = {
__typename?: 'Analytics';
/** Boolean that confirms query was dispatched */
@ -77,15 +81,6 @@ export type ClientConfig = {
telemetry: Telemetry;
};
export type CreateOneRefreshTokenInput = {
/** The record to create */
refreshToken: CreateRefreshTokenInput;
};
export type CreateRefreshTokenInput = {
expiresAt: Scalars['DateTime'];
};
export type CursorPaging = {
/** Paginate after opaque cursor */
after?: InputMaybe<Scalars['ConnectionCursor']>;
@ -223,6 +218,7 @@ export type LoginToken = {
export type Mutation = {
__typename?: 'Mutation';
activateWorkspace: Workspace;
challenge: LoginToken;
createEvent: Analytics;
createOneObject: Object;
@ -247,6 +243,11 @@ export type Mutation = {
};
export type MutationActivateWorkspaceArgs = {
data: ActivateWorkspaceInput;
};
export type MutationChallengeArgs = {
email: Scalars['String'];
password: Scalars['String'];
@ -259,11 +260,6 @@ export type MutationCreateEventArgs = {
};
export type MutationCreateOneRefreshTokenArgs = {
input: CreateOneRefreshTokenInput;
};
export type MutationDeleteOneObjectArgs = {
input: DeleteOneObjectInput;
};
@ -532,6 +528,7 @@ export type User = {
__typename?: 'User';
canImpersonate: Scalars['Boolean'];
createdAt: Scalars['DateTime'];
defaultAvatarUrl?: Maybe<Scalars['String']>;
defaultWorkspace: Workspace;
deletedAt?: Maybe<Scalars['DateTime']>;
disabled?: Maybe<Scalars['Boolean']>;
@ -545,7 +542,7 @@ export type User = {
passwordResetTokenExpiresAt?: Maybe<Scalars['DateTime']>;
supportUserHash?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
workspaceMember: WorkspaceMember;
workspaceMember?: Maybe<WorkspaceMember>;
};
export type UserEdge = {
@ -778,7 +775,7 @@ export type ImpersonateMutationVariables = Exact<{
}>;
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{
refreshToken: Scalars['String'];
@ -809,7 +806,7 @@ export type VerifyMutationVariables = Exact<{
}>;
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type CheckUserExistsQueryVariables = Exact<{
email: Scalars['String'];
@ -846,7 +843,7 @@ export type UploadImageMutationVariables = Exact<{
export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: string };
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } };
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@ -860,6 +857,18 @@ export type UploadProfilePictureMutationVariables = Exact<{
export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProfilePicture: string };
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } };
export type ActivateWorkspaceMutationVariables = Exact<{
input: ActivateWorkspaceInput;
}>;
export type ActivateWorkspaceMutation = { __typename?: 'Mutation', activateWorkspace: { __typename?: 'Workspace', id: string } };
export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
@ -1677,6 +1686,103 @@ export function useUploadProfilePictureMutation(baseOptions?: Apollo.MutationHoo
export type UploadProfilePictureMutationHookResult = ReturnType<typeof useUploadProfilePictureMutation>;
export type UploadProfilePictureMutationResult = Apollo.MutationResult<UploadProfilePictureMutation>;
export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>;
export const GetCurrentUserDocument = gql`
query GetCurrentUser {
currentUser {
id
firstName
lastName
email
canImpersonate
supportUserHash
workspaceMember {
id
name {
firstName
lastName
}
colorScheme
avatarUrl
locale
}
defaultWorkspace {
id
displayName
logo
domainName
inviteHash
allowImpersonation
subscriptionStatus
featureFlags {
id
key
value
workspaceId
}
}
}
}
`;
/**
* __useGetCurrentUserQuery__
*
* To run a query within a React component, call `useGetCurrentUserQuery` and pass it any options that fit your needs.
* When your component renders, `useGetCurrentUserQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetCurrentUserQuery({
* variables: {
* },
* });
*/
export function useGetCurrentUserQuery(baseOptions?: Apollo.QueryHookOptions<GetCurrentUserQuery, GetCurrentUserQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetCurrentUserQuery, GetCurrentUserQueryVariables>(GetCurrentUserDocument, options);
}
export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetCurrentUserQuery, GetCurrentUserQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetCurrentUserQuery, GetCurrentUserQueryVariables>(GetCurrentUserDocument, options);
}
export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQuery>;
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>;
export const ActivateWorkspaceDocument = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
id
}
}
`;
export type ActivateWorkspaceMutationFn = Apollo.MutationFunction<ActivateWorkspaceMutation, ActivateWorkspaceMutationVariables>;
/**
* __useActivateWorkspaceMutation__
*
* To run a mutation, you first call `useActivateWorkspaceMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useActivateWorkspaceMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [activateWorkspaceMutation, { data, loading, error }] = useActivateWorkspaceMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useActivateWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<ActivateWorkspaceMutation, ActivateWorkspaceMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ActivateWorkspaceMutation, ActivateWorkspaceMutationVariables>(ActivateWorkspaceDocument, options);
}
export type ActivateWorkspaceMutationHookResult = ReturnType<typeof useActivateWorkspaceMutation>;
export type ActivateWorkspaceMutationResult = Apollo.MutationResult<ActivateWorkspaceMutation>;
export type ActivateWorkspaceMutationOptions = Apollo.BaseMutationOptions<ActivateWorkspaceMutation, ActivateWorkspaceMutationVariables>;
export const DeleteCurrentWorkspaceDocument = gql`
mutation DeleteCurrentWorkspace {
deleteCurrentWorkspace {

View File

@ -66,16 +66,6 @@ describe('useOnboardingStatus', () => {
expect(result.current.onboardingStatus).toBe('ongoing_user_creation');
});
it('should return undefined when currentWorkspaceMember in undefined', async () => {
const { result } = renderHooks();
act(() => {
result.current.setTokenPair(tokenPair);
});
expect(result.current.onboardingStatus).toBe(undefined);
});
it('should return "incomplete"', async () => {
const { result } = renderHooks();
const {
@ -120,14 +110,9 @@ describe('useOnboardingStatus', () => {
expect(result.current.onboardingStatus).toBe('canceled');
});
it('should return "ongoing_workspace_creation"', async () => {
it('should return "ongoing_workspace_activation"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
const { setTokenPair, setBilling, setCurrentWorkspace } = result.current;
act(() => {
setTokenPair(tokenPair);
@ -135,12 +120,31 @@ describe('useOnboardingStatus', () => {
setCurrentWorkspace({
...currentWorkspace,
displayName: '',
subscriptionStatus: 'completed',
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember(currentWorkspaceMember);
});
expect(result.current.onboardingStatus).toBe('ongoing_workspace_creation');
expect(result.current.onboardingStatus).toBe(
'ongoing_workspace_activation',
);
});
it('should return "ongoing_workspace_activation"', async () => {
const { result } = renderHooks();
const { setTokenPair, setBilling, setCurrentWorkspace } = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
});
expect(result.current.onboardingStatus).toBe(
'ongoing_workspace_activation',
);
});
it('should return "ongoing_profile_creation"', async () => {
@ -157,7 +161,7 @@ describe('useOnboardingStatus', () => {
setBilling(billing);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'completed',
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember(currentWorkspaceMember);
});
@ -179,7 +183,7 @@ describe('useOnboardingStatus', () => {
setBilling(billing);
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'completed',
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,

View File

@ -89,13 +89,16 @@ export const useAuth = () => {
setTokenPair(verifyResult.data?.verify.tokens);
const user = verifyResult.data?.verify.user;
const workspaceMember = {
...user.workspaceMember,
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
};
const workspace = user.defaultWorkspace ?? null;
let workspaceMember = null;
setCurrentUser(user);
setCurrentWorkspaceMember(workspaceMember);
if (user.workspaceMember) {
workspaceMember = {
...user.workspaceMember,
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
};
setCurrentWorkspaceMember(workspaceMember);
}
const workspace = user.defaultWorkspace ?? null;
setCurrentWorkspace(workspace);
return {
user,

View File

@ -143,16 +143,15 @@ export const useSignInUp = () => {
billing?.isBillingEnabled &&
currentWorkspace.subscriptionStatus !== 'active'
) {
navigate('/plan-required');
navigate(AppPath.PlanRequired);
return;
}
if (currentWorkspace.displayName) {
navigate('/');
navigate(AppPath.Index);
return;
}
navigate('/create/workspace');
navigate(AppPath.CreateWorkspace);
} catch (err: any) {
enqueueSnackBar(err?.message, {
variant: 'error',

View File

@ -0,0 +1,11 @@
import { selector } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
export const isCurrentWorkspaceActiveSelector = selector({
key: 'isCurrentWorkspaceActiveSelector',
get: ({ get }) => {
const currentWorkspaceMember = get(currentWorkspaceMemberState);
return !!currentWorkspaceMember;
},
});

View File

@ -11,27 +11,24 @@ describe('getOnboardingStatus', () => {
currentWorkspace: null,
});
const unknownStatus = getOnboardingStatus({
const ongoingWorkspaceActivation = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: null,
currentWorkspace: null,
});
const ongoingWorkspaceCreation = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
displayName: null,
} as CurrentWorkspace,
});
const ongoingWorkspaceActivationPreviouslyActive = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: null,
currentWorkspace: {
id: '1',
displayName: 'My Workspace',
} as CurrentWorkspace,
});
const ongoingProfileCreation = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
@ -110,8 +107,10 @@ describe('getOnboardingStatus', () => {
});
expect(ongoingUserCreation).toBe('ongoing_user_creation');
expect(unknownStatus).toBe(undefined);
expect(ongoingWorkspaceCreation).toBe('ongoing_workspace_creation');
expect(ongoingWorkspaceActivation).toBe('ongoing_workspace_activation');
expect(ongoingWorkspaceActivationPreviouslyActive).toBe(
'ongoing_workspace_activation',
);
expect(ongoingProfileCreation).toBe('ongoing_profile_creation');
expect(completed).toBe('completed');
expect(incomplete).toBe('incomplete');

View File

@ -5,7 +5,7 @@ export enum OnboardingStatus {
Incomplete = 'incomplete',
Canceled = 'canceled',
OngoingUserCreation = 'ongoing_user_creation',
OngoingWorkspaceCreation = 'ongoing_workspace_creation',
OngoingWorkspaceActivation = 'ongoing_workspace_activation',
OngoingProfileCreation = 'ongoing_profile_creation',
Completed = 'completed',
}
@ -28,11 +28,6 @@ export const getOnboardingStatus = ({
return OnboardingStatus.OngoingUserCreation;
}
// if the user has not been fetched yet, we can't know the onboarding status
if (!currentWorkspaceMember) {
return undefined;
}
if (
isBillingEnabled &&
currentWorkspace?.subscriptionStatus === 'incomplete'
@ -44,9 +39,10 @@ export const getOnboardingStatus = ({
return OnboardingStatus.Canceled;
}
if (!currentWorkspace?.displayName) {
return OnboardingStatus.OngoingWorkspaceCreation;
if (!currentWorkspaceMember) {
return OnboardingStatus.OngoingWorkspaceActivation;
}
if (
!currentWorkspaceMember.name.firstName ||
!currentWorkspaceMember.name.lastName

View File

@ -3,6 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandType } from '@/command-menu/types/Command';
@ -11,7 +12,10 @@ import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWith
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockDefaultWorkspace } from '~/testing/mock-data/users';
import {
mockDefaultWorkspace,
mockedWorkspaceMemberData,
} from '~/testing/mock-data/users';
import { sleep } from '~/testing/sleep';
import { CommandMenu } from '../CommandMenu';
@ -24,10 +28,14 @@ const meta: Meta<typeof CommandMenu> = {
decorators: [
(Story) => {
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const { addToCommandMenu, setToIntitialCommandMenu, openCommandMenu } =
useCommandMenu();
setCurrentWorkspace(mockDefaultWorkspace);
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
useEffect(() => {
setToIntitialCommandMenu();

View File

@ -3,6 +3,7 @@ import { useRecoilState } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { AppPath } from '@/types/AppPath';
import {
IconCheckbox,
IconList,
@ -71,7 +72,7 @@ export const MobileNavigationBar = () => {
onClick: () => {
closeCommandMenu();
setIsNavigationDrawerOpen(false);
navigate('/tasks');
navigate(AppPath.TasksPage);
},
},
{

View File

@ -1,6 +1,6 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector';
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
@ -9,12 +9,14 @@ export const ObjectMetadataItemsProvider = ({
children,
}: React.PropsWithChildren) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isCurrentWorkspaceActive = useRecoilValue(
isCurrentWorkspaceActiveSelector,
);
return (
<>
<ObjectMetadataItemsLoadEffect />
{(!currentWorkspace || !!objectMetadataItems.length) && (
{(!isCurrentWorkspaceActive || !!objectMetadataItems.length) && (
<RelationPickerScope relationPickerScopeId="relation-picker">
{children}
</RelationPickerScope>

View File

@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector';
import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
import { useGetObjectOrderByField } from '@/object-metadata/hooks/useGetObjectOrderByField';
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
@ -40,7 +40,9 @@ export const useObjectMetadataItem = (
{ objectNameSingular }: ObjectMetadataItemIdentifier,
depth?: number,
) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isCurrentWorkspaceActive = useRecoilValue(
isCurrentWorkspaceActiveSelector,
);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
@ -52,7 +54,7 @@ export const useObjectMetadataItem = (
let objectMetadataItems = useRecoilValue(objectMetadataItemsState);
if (!currentWorkspace) {
if (!isCurrentWorkspaceActive) {
objectMetadataItem =
mockObjectMetadataItems.find(
(objectMetadataItem) =>

View File

@ -1,6 +1,6 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector';
import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@ -12,7 +12,9 @@ import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentif
export const useObjectMetadataItemOnly = ({
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isCurrentWorkspaceActive = useRecoilValue(
isCurrentWorkspaceActiveSelector,
);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
@ -24,7 +26,7 @@ export const useObjectMetadataItemOnly = ({
let objectMetadataItems = useRecoilValue(objectMetadataItemsState);
if (!currentWorkspace) {
if (!isCurrentWorkspaceActive) {
objectMetadataItem =
mockObjectMetadataItems.find(
(objectMetadataItem) =>

View File

@ -1,6 +1,6 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { isDefined } from '~/utils/isDefined';
@ -10,7 +10,9 @@ export const useObjectNamePluralFromSingular = ({
}: {
objectNameSingular: string;
}) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isCurrentWorkspaceActive = useRecoilValue(
isCurrentWorkspaceActiveSelector,
);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
@ -20,7 +22,7 @@ export const useObjectNamePluralFromSingular = ({
}),
);
if (!currentWorkspace) {
if (!isCurrentWorkspaceActive) {
objectMetadataItem =
mockObjectMetadataItems.find(
(objectMetadataItem) =>

View File

@ -1,6 +1,6 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { isDefined } from '~/utils/isDefined';
@ -10,7 +10,9 @@ export const useObjectNameSingularFromPlural = ({
}: {
objectNamePlural: string;
}) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isCurrentWorkspaceActive = useRecoilValue(
isCurrentWorkspaceActiveSelector,
);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
@ -20,7 +22,7 @@ export const useObjectNameSingularFromPlural = ({
}),
);
if (!currentWorkspace) {
if (!isCurrentWorkspaceActive) {
objectMetadataItem =
mockObjectMetadataItems.find(
(objectMetadataItem) =>

View File

@ -3,7 +3,7 @@ import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import {
@ -39,7 +39,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
);
describe('useFindManyRecords', () => {
it('should skip fetch if currentWorkspace is undefined', async () => {
it('should skip fetch if currentWorkspaceMember is undefined', async () => {
const { result } = renderHook(
() => useFindManyRecords({ objectNameSingular: 'person' }),
{
@ -56,12 +56,12 @@ describe('useFindManyRecords', () => {
const { result } = renderHook(
() => {
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
setCurrentWorkspace({
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
setCurrentWorkspaceMember({
id: '32219445-f587-4c40-b2b1-6d3205ed96da',
displayName: 'cool-workspace',
allowImpersonation: false,
subscriptionStatus: 'incomplete',
name: { firstName: 'John', lastName: 'Connor' },
});
const mockObjectMetadataItems = getObjectMetadataItemsMock();

View File

@ -4,7 +4,7 @@ import { isNonEmptyArray } from '@apollo/client/utilities';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
@ -65,12 +65,12 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
);
const { enqueueSnackBar } = useSnackBar();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { data, loading, error, fetchMore } = useQuery<
ObjectRecordQueryResult<T>
>(findManyRecordsQuery, {
skip: skip || !objectMetadataItem || !currentWorkspace,
skip: skip || !objectMetadataItem || !currentWorkspaceMember,
variables: {
filter,
limit,

View File

@ -10,6 +10,7 @@ import {
} from '@storybook/test';
import { useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
@ -17,7 +18,10 @@ import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/Componen
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockDefaultWorkspace } from '~/testing/mock-data/users';
import {
mockDefaultWorkspace,
mockedWorkspaceMemberData,
} from '~/testing/mock-data/users';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import {
@ -27,10 +31,14 @@ import {
const RelationWorkspaceSetterEffect = () => {
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
useEffect(() => {
setCurrentWorkspace(mockDefaultWorkspace);
}, [setCurrentWorkspace]);
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
}, [setCurrentWorkspace, setCurrentWorkspaceMember]);
return <></>;
};

View File

@ -1,6 +1,6 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentWorkspaceActiveSelector } from '@/auth/states/selectors/isCurrentWorkspaceActiveSelector';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
@ -13,8 +13,10 @@ import { useFindManyRecords } from '../../hooks/useFindManyRecords';
export const useLoadRecordIndexTable = (objectNameSingular: string) => {
const { setRecordTableData, setIsRecordTableInitialLoading } =
useRecordTable();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isCurrentWorkspaceActive = useRecoilValue(
isCurrentWorkspaceActiveSelector,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
@ -51,7 +53,7 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => {
});
return {
records: currentWorkspace ? records : signInBackgroundMockCompanies,
records: isCurrentWorkspaceActive ? records : signInBackgroundMockCompanies,
loading,
fetchMoreRecords,
queryStateIdentifier,

View File

@ -1,6 +0,0 @@
import { atomFamily } from 'recoil';
export const isCreateModeScopedState = atomFamily<boolean, string>({
key: 'isCreateModeScopedState',
default: false,
});

View File

@ -3,7 +3,7 @@ import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
@ -66,12 +66,12 @@ describe('useFilteredSearchEntityQuery', () => {
it('returns the correct result when everything is provided', async () => {
const { result } = renderHook(
() => {
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
setCurrentWorkspace({
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
setCurrentWorkspaceMember({
id: '32219445-f587-4c40-b2b1-6d3205ed96da',
displayName: 'cool-workspace',
allowImpersonation: false,
subscriptionStatus: 'incomplete',
name: { firstName: 'John', lastName: 'Connor' },
});
const mockObjectMetadataItems = getObjectMetadataItemsMock();

View File

@ -28,7 +28,6 @@ export const SignInBackgroundMockContainerEffect = ({
const {
setAvailableTableColumns,
setOnEntityCountChange,
setRecordTableData,
setTableColumns,
resetTableRowSelection,
} = useRecordTable({
@ -77,7 +76,6 @@ export const SignInBackgroundMockContainerEffect = ({
setAvailableFieldDefinitions,
objectMetadataItem,
setAvailableTableColumns,
setRecordTableData,
setTableColumns,
]);

View File

@ -5,7 +5,7 @@ import { useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
export const UserProvider = ({ children }: React.PropsWithChildren) => {
@ -13,21 +13,22 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => {
const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const { loading: queryLoading, data: queryData } = useQuery(
GET_CURRENT_USER_AND_VIEWS,
);
const { loading: queryLoading, data: queryData } = useQuery(GET_CURRENT_USER);
useEffect(() => {
if (!queryLoading) {
setIsLoading(false);
}
if (queryData?.currentUser?.workspaceMember) {
if (queryData?.currentUser) {
setCurrentUser(queryData.currentUser);
setCurrentWorkspace(queryData.currentUser.defaultWorkspace);
}
if (queryData?.currentUser?.workspaceMember) {
const workspaceMember = queryData.currentUser.workspaceMember;
setCurrentWorkspaceMember({
...workspaceMember,

View File

@ -0,0 +1,40 @@
// This query cannot be put in the graphQL folder because it cannot be generated by the graphQL codegen.
import { gql } from '@apollo/client';
export const GET_CURRENT_USER = gql`
query GetCurrentUser {
currentUser {
id
firstName
lastName
email
canImpersonate
supportUserHash
workspaceMember {
id
name {
firstName
lastName
}
colorScheme
avatarUrl
locale
}
defaultWorkspace {
id
displayName
logo
domainName
inviteHash
allowImpersonation
subscriptionStatus
featureFlags {
id
key
value
workspaceId
}
}
}
}
`;

View File

@ -1,103 +0,0 @@
// This query cannot be put in the graphQL folder because it cannot be generated by the graphQL codegen.
import { gql } from '@apollo/client';
export const GET_CURRENT_USER_AND_VIEWS = gql`
query GetCurrentUserAndViews {
currentUser {
id
firstName
lastName
email
canImpersonate
supportUserHash
workspaceMember {
id
name {
firstName
lastName
}
colorScheme
avatarUrl
locale
}
defaultWorkspace {
id
displayName
logo
domainName
inviteHash
allowImpersonation
subscriptionStatus
featureFlags {
id
key
value
workspaceId
}
}
}
views {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
id
createdAt
updatedAt
name
objectMetadataId
type
deletedAt
viewFilters {
edges {
cursor
node {
id
createdAt
updatedAt
fieldMetadataId
operand
value
displayValue
deletedAt
}
}
}
viewSorts {
edges {
cursor
node {
id
createdAt
updatedAt
fieldMetadataId
direction
deletedAt
}
}
}
viewFields {
edges {
cursor
node {
id
createdAt
updatedAt
fieldMetadataId
isVisible
size
position
deletedAt
}
}
}
}
}
}
}
`;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const ACTIVATE_WORKSPACE = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
id
}
}
`;

View File

@ -1,6 +1,5 @@
import { useCallback } from 'react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRecoilState } from 'recoil';
@ -54,7 +53,6 @@ const validationSchema = z
type Form = z.infer<typeof validationSchema>;
export const CreateProfile = () => {
const navigate = useNavigate();
const onboardingStatus = useOnboardingStatus();
const { enqueueSnackBar } = useSnackBar();
@ -114,8 +112,6 @@ export const CreateProfile = () => {
colorScheme: 'System',
}) as any,
);
navigate('/');
} catch (error: any) {
enqueueSnackBar(error?.message, {
variant: 'error',
@ -125,7 +121,6 @@ export const CreateProfile = () => {
[
currentWorkspaceMember?.id,
enqueueSnackBar,
navigate,
setCurrentWorkspaceMember,
updateOneRecord,
],

View File

@ -3,22 +3,24 @@ import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSetRecoilState } from 'recoil';
import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { AppPath } from '@/types/AppPath';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { useActivateWorkspaceMutation } from '~/generated/graphql';
const StyledContentContainer = styled.div`
width: 100%;
@ -46,9 +48,9 @@ export const CreateWorkspace = () => {
const { enqueueSnackBar } = useSnackBar();
const onboardingStatus = useOnboardingStatus();
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [activateWorkspace] = useActivateWorkspaceMutation();
const apolloMetadataClient = useApolloMetadataClient();
// Form
const {
@ -67,28 +69,25 @@ export const CreateWorkspace = () => {
const onSubmit: SubmitHandler<Form> = useCallback(
async (data) => {
try {
const result = await updateWorkspace({
const result = await activateWorkspace({
variables: {
input: {
displayName: data.name,
},
},
});
setCurrentWorkspace({
id: result.data?.updateWorkspace?.id ?? '',
displayName: data.name,
subscriptionStatus:
result.data?.updateWorkspace?.subscriptionStatus ?? 'incomplete',
allowImpersonation:
result.data?.updateWorkspace?.allowImpersonation ?? false,
refetchQueries: [GET_CURRENT_USER],
});
if (result.errors || !result.data?.updateWorkspace) {
await apolloMetadataClient?.refetchQueries({
include: [FIND_MANY_OBJECT_METADATA_ITEMS],
});
if (result.errors) {
throw result.errors ?? new Error('Unknown error');
}
setTimeout(() => {
navigate('/create/profile');
navigate(AppPath.CreateProfile);
}, 20);
} catch (error: any) {
enqueueSnackBar(error?.message, {
@ -96,7 +95,7 @@ export const CreateWorkspace = () => {
});
}
},
[enqueueSnackBar, navigate, setCurrentWorkspace, updateWorkspace],
[enqueueSnackBar, navigate, apolloMetadataClient, activateWorkspace],
);
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
@ -115,7 +114,7 @@ export const CreateWorkspace = () => {
[onSubmit],
);
if (onboardingStatus !== OnboardingStatus.OngoingWorkspaceCreation) {
if (onboardingStatus !== OnboardingStatus.OngoingWorkspaceActivation) {
return null;
}

View File

@ -6,8 +6,7 @@ import { useRecoilValue } from 'recoil';
import { useAuth } from '@/auth/hooks/useAuth';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AppPath } from '../../modules/types/AppPath';
import { AppPath } from '@/types/AppPath';
export const VerifyEffect = () => {
const [searchParams] = useSearchParams();

View File

@ -4,7 +4,7 @@ import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import {
PageDecorator,
PageDecoratorArgs,
@ -22,16 +22,13 @@ const meta: Meta<PageDecoratorArgs> = {
parameters: {
msw: {
handlers: [
graphql.query(
getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '',
() => {
return HttpResponse.json({
data: {
currentUser: mockedOnboardingUsersData[0],
},
});
},
),
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
data: {
currentUser: mockedOnboardingUsersData[0],
},
});
}),
graphqlMocks.handlers,
],
},

View File

@ -1,14 +1,18 @@
import { getOperationName } from '@apollo/client/utilities';
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw';
import { useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { CreateWorkspace } from '../CreateWorkspace';
@ -25,7 +29,18 @@ const meta: Meta<PageDecoratorArgs> = {
],
args: { routePath: AppPath.CreateWorkspace },
parameters: {
msw: graphqlMocks,
msw: {
handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
data: {
currentUser: mockedOnboardingUsersData[1],
},
});
}),
graphqlMocks.handlers,
],
},
},
};

View File

@ -4,7 +4,7 @@ import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import {
PageDecorator,
PageDecoratorArgs,
@ -22,22 +22,19 @@ const meta: Meta<PageDecoratorArgs> = {
parameters: {
msw: {
handlers: [
graphql.query(
getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '',
() => {
return HttpResponse.json({
data: {
currentUser: {
...mockedOnboardingUsersData[0],
defaultWorkspace: {
...mockedOnboardingUsersData[0].defaultWorkspace,
subscriptionStatus: 'incomplete',
},
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
data: {
currentUser: {
...mockedOnboardingUsersData[0],
defaultWorkspace: {
...mockedOnboardingUsersData[0].defaultWorkspace,
subscriptionStatus: 'incomplete',
},
},
});
},
),
},
});
}),
graphqlMocks.handlers,
],
},

View File

@ -4,7 +4,7 @@ import { fireEvent, within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import {
PageDecorator,
PageDecoratorArgs,
@ -22,16 +22,13 @@ const meta: Meta<PageDecoratorArgs> = {
parameters: {
msw: {
handlers: [
graphql.query(
getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '',
() => {
return HttpResponse.json({
data: {
currentUser: mockedOnboardingUsersData[0],
},
});
},
),
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
data: {
currentUser: mockedOnboardingUsersData[0],
},
});
}),
graphqlMocks.handlers,
],
},

View File

@ -6,10 +6,9 @@ import { useRecoilState, useSetRecoilState } from 'recoil';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { currentUserState } from '@/auth/states/currentUserState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { AppPath } from '@/types/AppPath';
import { useImpersonateMutation } from '~/generated/graphql';
import { AppPath } from '../../modules/types/AppPath';
export const ImpersonateEffect = () => {
const navigate = useNavigate();
const { userId } = useParams();

View File

@ -2,6 +2,7 @@ import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage';
import { AppPath } from '@/types/AppPath';
import { MainButton } from '@/ui/input/button/components/MainButton';
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
import { StyledEmptyTextContainer } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
@ -49,7 +50,7 @@ export const NotFound = () => {
<MainButton
title="Back to content"
fullWidth
onClick={() => navigate('/')}
onClick={() => navigate(AppPath.Index)}
/>
</StyledButtonContainer>
</StyledErrorContainer>

View File

@ -1,10 +1,6 @@
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
@ -32,10 +28,6 @@ export const RecordIndexPage = () => {
objectNamePlural,
});
const onboardingStatus = useOnboardingStatus();
const navigate = useNavigate();
const { findObjectMetadataItemByNamePlural } =
useObjectMetadataItemForSettings();
@ -44,15 +36,6 @@ export const RecordIndexPage = () => {
findObjectMetadataItemByNamePlural(objectNamePlural)?.icon,
);
useEffect(() => {
if (
!isNonEmptyString(objectNamePlural) &&
onboardingStatus === OnboardingStatus.Completed
) {
navigate('/');
}
}, [objectNamePlural, navigate, onboardingStatus]);
const { createOneRecord: createOneObject } = useCreateOneRecord({
objectNameSingular,
});

View File

@ -4,7 +4,7 @@ import { graphql, HttpResponse } from 'msw';
import { CREATE_EVENT } from '@/analytics/graphql/queries/createEvent';
import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { mockedActivities } from '~/testing/mock-data/activities';
import { mockedCompaniesData } from '~/testing/mock-data/companies';
@ -22,22 +22,10 @@ const metadataGraphql = graphql.link(`${REACT_APP_SERVER_BASE_URL}/metadata`);
export const graphqlMocks = {
handlers: [
graphql.query(getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '', () => {
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
data: {
currentUser: mockedUsersData[0],
views: {
edges: mockedViewsData.map((view) => ({
node: view,
cursor: null,
})),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
},
});
}),

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ type MockedUser = Pick<
| '__typename'
| 'supportUserHash'
> & {
workspaceMember: WorkspaceMember;
workspaceMember: WorkspaceMember | null;
locale: string;
defaultWorkspace: Workspace;
};
@ -34,7 +34,7 @@ export const mockDefaultWorkspace: Workspace = {
updatedAt: '2023-04-26T10:23:42.33625+00:00',
};
const workspaceMember: WorkspaceMember = {
export const mockedWorkspaceMemberData: WorkspaceMember = {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
colorScheme: 'Light',
avatarUrl,
@ -59,7 +59,7 @@ export const mockedUsersData: Array<MockedUser> = [
canImpersonate: false,
supportUserHash:
'a95afad9ff6f0b364e2a3fd3e246a1a852c22b6e55a3ca33745a86c201f9c10d',
workspaceMember,
workspaceMember: mockedWorkspaceMemberData,
defaultWorkspace: mockDefaultWorkspace,
locale: 'en',
},
@ -73,7 +73,7 @@ export const mockedUsersData: Array<MockedUser> = [
supportUserHash:
'54ac3986035961724cdb9a7a30c70e6463a4b68f0ecd2014c727171a82144b74',
workspaceMember: {
...workspaceMember,
...mockedWorkspaceMemberData,
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6c',
name: {
firstName: 'Felix',
@ -97,7 +97,7 @@ export const mockedOnboardingUsersData: Array<MockedUser> = [
supportUserHash:
'4fb61d34ed3a4aeda2476d4b308b5162db9e1809b2b8277e6fdc6efc4a609254',
workspaceMember: {
...workspaceMember,
...mockedWorkspaceMemberData,
id: 'd454f075-c72f-4ebe-bac7-d28e75e74a23',
name: {
firstName: '',
@ -116,7 +116,7 @@ export const mockedOnboardingUsersData: Array<MockedUser> = [
firstName: '',
lastName: '',
canImpersonate: false,
workspaceMember,
workspaceMember: null,
defaultWorkspace: {
...mockDefaultWorkspace,
displayName: '',

View File

@ -101,6 +101,10 @@ export class AuthResolver {
@AuthUser() user: User,
): Promise<TransientToken | void> {
const workspaceMember = await this.userService.loadWorkspaceMember(user);
if (!workspaceMember) {
return;
}
const transientToken = await this.tokenService.generateTransientToken(
workspaceMember.id,
user.defaultWorkspace.id,

View File

@ -7,11 +7,11 @@ import {
import { InjectRepository } from '@nestjs/typeorm';
import { HttpService } from '@nestjs/axios';
import FileType from 'file-type';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { render } from '@react-email/components';
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
import FileType from 'file-type';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
@ -29,11 +29,11 @@ import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { UserService } from 'src/core/user/services/user.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { getImageBufferFromUrl } from 'src/utils/image';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { EmailService } from 'src/integrations/email/email.service';
import { UpdatePassword } from 'src/core/auth/dto/update-password.entity';
import { getImageBufferFromUrl } from 'src/utils/image';
import { TokenService } from './token.service';
@ -135,18 +135,8 @@ export class AuthService {
});
workspace = await this.workspaceRepository.save(workspaceToCreate);
await this.workspaceManagerService.init(workspace.id);
}
const userToCreate = this.userRepository.create({
email: email,
firstName: firstName,
lastName: lastName,
canImpersonate: false,
passwordHash,
defaultWorkspace: workspace,
});
const user = await this.userRepository.save(userToCreate);
let imagePath: string | undefined = undefined;
if (picture) {
@ -166,9 +156,18 @@ export class AuthService {
imagePath = paths[0];
}
await this.userService.createWorkspaceMember(user, imagePath);
return user;
const userToCreate = this.userRepository.create({
email: email,
firstName: firstName,
lastName: lastName,
defaultAvatarUrl: imagePath,
canImpersonate: false,
passwordHash,
defaultWorkspace: workspace,
});
return await this.userRepository.save(userToCreate);
}
async verify(email: string): Promise<Verify> {
@ -189,7 +188,11 @@ export class AuthService {
// passwordHash is hidden for security reasons
user.passwordHash = '';
user.workspaceMember = await this.userService.loadWorkspaceMember(user);
const workspaceMember = await this.userService.loadWorkspaceMember(user);
if (workspaceMember) {
user.workspaceMember = workspaceMember;
}
const accessToken = await this.tokenService.generateAccessToken(user.id);
const refreshToken = await this.tokenService.generateRefreshToken(user.id);

View File

@ -21,11 +21,23 @@ export class UserService extends TypeOrmQueryService<User> {
}
async loadWorkspaceMember(user: User) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
const dataSourcesMetadata =
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
user.defaultWorkspace.id,
);
if (!dataSourcesMetadata.length) {
return;
}
if (dataSourcesMetadata.length > 1) {
throw new Error(
`user '${user.id}' default workspace '${user.defaultWorkspace.id}' has multiple data source metadata`,
);
}
const dataSourceMetadata = dataSourcesMetadata[0];
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
@ -33,6 +45,10 @@ export class UserService extends TypeOrmQueryService<User> {
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${user.id}'`,
);
if (!workspaceMembers.length) {
return;
}
assert(workspaceMembers.length === 1, 'WorkspaceMember not found');
const userWorkspaceMember = new WorkspaceMember();
@ -63,7 +79,7 @@ export class UserService extends TypeOrmQueryService<User> {
);
}
async createWorkspaceMember(user: User, avatarUrl?: string) {
async createWorkspaceMember(user: User) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
user.defaultWorkspace.id,
@ -77,7 +93,7 @@ export class UserService extends TypeOrmQueryService<User> {
("nameFirstName", "nameLastName", "colorScheme", "userId", "userEmail", "avatarUrl")
VALUES ('${user.firstName}', '${user.lastName}', 'Light', '${
user.id
}', '${user.email}', '${avatarUrl ?? ''}')`,
}', '${user.email}', '${user.defaultAvatarUrl ?? ''}')`,
);
}

View File

@ -34,6 +34,10 @@ export class User {
@Column()
email: string;
@Field({ nullable: true })
@Column({ nullable: true })
defaultAvatarUrl: string;
@Field()
@Column({ default: false })
emailVerified: boolean;
@ -81,6 +85,6 @@ export class User {
})
refreshTokens: RefreshToken[];
@Field(() => WorkspaceMember, { nullable: false })
@Field(() => WorkspaceMember, { nullable: true })
workspaceMember: WorkspaceMember;
}

View File

@ -55,9 +55,11 @@ export class UserResolver {
}
@ResolveField(() => WorkspaceMember, {
nullable: false,
nullable: true,
})
async workspaceMember(@Parent() user: User): Promise<WorkspaceMember> {
async workspaceMember(
@Parent() user: User,
): Promise<WorkspaceMember | undefined> {
return this.userService.loadWorkspaceMember(user);
}

View File

@ -0,0 +1,11 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsOptional, IsString } from 'class-validator';
@InputType()
export class ActivateWorkspaceInput {
@Field({ nullable: true })
@IsString()
@IsOptional()
displayName?: string;
}

View File

@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { UserService } from 'src/core/user/services/user.service';
import { WorkspaceService } from './workspace.service';
@ -21,6 +22,10 @@ describe('WorkspaceService', () => {
provide: WorkspaceManagerService,
useValue: {},
},
{
provide: UserService,
useValue: {},
},
],
}).compile();

View File

@ -1,4 +1,5 @@
import { InjectRepository } from '@nestjs/typeorm';
import { BadRequestException } from '@nestjs/common';
import assert from 'assert';
@ -7,16 +8,33 @@ import { Repository } from 'typeorm';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { User } from 'src/core/user/user.entity';
import { UserService } from 'src/core/user/services/user.service';
import { ActivateWorkspaceInput } from 'src/core/workspace/dtos/activate-workspace-input';
export class WorkspaceService extends TypeOrmQueryService<Workspace> {
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly userService: UserService,
) {
super(workspaceRepository);
}
async activateWorkspace(user: User, data: ActivateWorkspaceInput) {
if (!data.displayName || !data.displayName.length) {
throw new BadRequestException("'displayName' not provided");
}
await this.workspaceRepository.update(user.defaultWorkspace.id, {
displayName: data.displayName,
});
await this.workspaceManagerService.init(user.defaultWorkspace.id);
await this.userService.createWorkspaceMember(user);
return user.defaultWorkspace;
}
async deleteWorkspace(id: string, shouldDeleteCoreWorkspace = true) {
const workspace = await this.workspaceRepository.findOneBy({ id });

View File

@ -8,6 +8,7 @@ import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspac
import { WorkspaceResolver } from 'src/core/workspace/workspace.resolver';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
import { UserModule } from 'src/core/user/user.module';
import { Workspace } from './workspace.entity';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
@ -24,6 +25,7 @@ import { WorkspaceService } from './services/workspace.service';
'core',
),
WorkspaceManagerModule,
UserModule,
FileModule,
],
services: [WorkspaceService],

View File

@ -12,6 +12,9 @@ import { assert } from 'src/utils/assert';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { UpdateWorkspaceInput } from 'src/core/workspace/dtos/update-workspace-input';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { User } from 'src/core/user/user.entity';
import { AuthUser } from 'src/decorators/auth/auth-user.decorator';
import { ActivateWorkspaceInput } from 'src/core/workspace/dtos/activate-workspace-input';
import { Workspace } from './workspace.entity';
@ -35,6 +38,15 @@ export class WorkspaceResolver {
return workspace;
}
@Mutation(() => Workspace)
@UseGuards(JwtAuthGuard)
async activateWorkspace(
@Args('data') data: ActivateWorkspaceInput,
@AuthUser() user: User,
) {
return await this.workspaceService.activateWorkspace(user, data);
}
@Mutation(() => Workspace)
async updateWorkspace(
@Args('data') data: UpdateWorkspaceInput,