refactor: metadata store cleanup, SSE unification, mock metadata loading & login redirect fix (#18651)

## Summary
- **SSE unification**: Replaced 11 individual SSE effect components with
a single generic `MetadataStoreSSEEffect`
- **Metadata store cleanup**: Merged `metadataCollectionHashesState`
into `metadataStoreState` (currentCollectionHash / draftCollectionHash
per entity), moved `objectMetadataItemsSelector` to `object-metadata`
domain, converted `navigationMenuItemsState` to a derived selector
- **Naming clarity**: Renamed `isAppMetadataReadyState` →
`isMinimalMetadataReadyState`, `MetadataGater` → `MinimalMetadataGater`,
`useIsLogged` → `useHasAccessTokenPair`,
`patchMetadataStoreFromSSEEvent` now takes named object params
- **Mock metadata loading**: Added `generate-navigation-menu-items.ts`
script, rewrote `useLoadMockedMinimalMetadata` to load full
objects/fields/indexes/views/navItems from generated mock data, enabling
proper sign-in background rendering (table columns, view picker,
navigation)
- **Login/logout transitions**: `MinimalMetadataLoadEffect` manages
mocked↔real metadata transitions based on auth state,
`MainContextStoreProvider` computes context on auth pages for view
picker support
- **Login redirect fix**: `handleLoadWorkspaceAfterAuthentication` now
re-enables `isAppEffectRedirectEnabled` after `loadCurrentUser()`
completes, fixing the blocked post-login navigation
- **Dead code removal**: Deleted `useRefreshPageLayouts`,
`useApplyPageLayouts`, `useStaleMetadataEntities`,
`metadataCollectionHashesState`, and all individual SSE effects

## Test plan
- [x] Login from welcome page redirects to companies page
- [x] Logout transitions cleanly to mocked metadata on welcome page
- [x] Sign-in background shows table columns, view picker, and
navigation items
- [x] SSE events still update metadata store entries correctly
- [x] Navigation menu items persist across page refreshes
- [ ] CI: lint, typecheck, tests pass
This commit is contained in:
Charles Bochet
2026-03-16 00:38:11 +01:00
committed by GitHub
parent 06efee1eef
commit ba9aa41bba
161 changed files with 3106 additions and 4350 deletions
@@ -1,19 +1,23 @@
/* oxlint-disable no-console */
import { generateApiKeys } from './mock-data/generate-api-keys.js';
import { generateBillingPlans } from './mock-data/generate-billing-plans.js';
import { generateMetadata } from './mock-data/generate-metadata.js';
import { generateObjectMetadata } from './mock-data/generate-object-metadata.js';
import { generateMinimalMetadata } from './mock-data/generate-minimal-metadata.js';
import { generateRecordData } from './mock-data/generate-record-data.js';
import { generateRoles } from './mock-data/generate-roles.js';
import { generateNavigationMenuItems } from './mock-data/generate-navigation-menu-items.js';
import { generateViews } from './mock-data/generate-views.js';
import { authenticate } from './mock-data/utils.js';
const main = async () => {
const token = await authenticate();
const metadata = await generateMetadata(token);
const metadata = await generateObjectMetadata(token);
await generateMinimalMetadata(token);
await generateRecordData(token, metadata);
await generateRoles(token);
await generateViews(token);
await generateNavigationMenuItems(token);
try {
await generateBillingPlans(token);
@@ -0,0 +1,51 @@
/* oxlint-disable no-console */
import { graphqlRequest, writeGeneratedFile } from './utils.js';
const MINIMAL_METADATA_QUERY = `
query FindMinimalMetadata {
minimalMetadata {
objectMetadataItems {
id
nameSingular
namePlural
labelSingular
labelPlural
icon
isCustom
isActive
isSystem
isRemote
}
views {
id
type
key
objectMetadataId
}
collectionHashes {
collectionName
hash
}
}
}
`;
export const generateMinimalMetadata = async (token: string) => {
console.log('Fetching minimal metadata from /metadata ...');
const data = (await graphqlRequest(
'/metadata',
MINIMAL_METADATA_QUERY,
token,
)) as {
minimalMetadata: Record<string, unknown>;
};
writeGeneratedFile(
'metadata/minimal/mock-minimal-metadata.ts',
'mockedMinimalMetadata',
'MinimalMetadata',
"import { type MinimalMetadata } from '@/metadata-store/types/MinimalMetadata';",
data.minimalMetadata,
);
};
@@ -0,0 +1,50 @@
/* oxlint-disable no-console, lingui/no-unlocalized-strings */
import { graphqlRequest, writeGeneratedFile } from './utils.js';
const FIND_MANY_NAVIGATION_MENU_ITEMS_QUERY = `
query FindManyNavigationMenuItems {
navigationMenuItems {
id
userWorkspaceId
targetRecordId
targetObjectMetadataId
viewId
folderId
name
link
icon
color
position
applicationId
createdAt
updatedAt
targetRecordIdentifier {
id
labelIdentifier
imageIdentifier
}
}
}
`;
export const generateNavigationMenuItems = async (token: string) => {
console.log('Fetching navigation menu items from /metadata ...');
const data = (await graphqlRequest(
'/metadata',
FIND_MANY_NAVIGATION_MENU_ITEMS_QUERY,
token,
)) as {
navigationMenuItems: Record<string, unknown>[];
};
console.log(` Got ${data.navigationMenuItems.length} navigation menu items.`);
writeGeneratedFile(
'metadata/navigation-menu-items/mock-navigation-menu-items-data.ts',
'mockedNavigationMenuItems',
'NavigationMenuItem[]',
"import { type NavigationMenuItem } from '~/generated-metadata/graphql';",
data.navigationMenuItems,
);
};
@@ -141,7 +141,7 @@ const METADATA_QUERY = `
}
`;
export const generateMetadata = async (token: string) => {
export const generateObjectMetadata = async (token: string) => {
console.log('Fetching object metadata from /metadata ...');
const metadata = await graphqlRequest('/metadata', METADATA_QUERY, token);
@@ -831,6 +831,12 @@ export type ClientConfig = {
support: Support;
};
export type CollectionHash = {
__typename?: 'CollectionHash';
collectionName: AllMetadataName;
hash: Scalars['String'];
};
export type CommandMenuItem = {
__typename?: 'CommandMenuItem';
applicationId?: Maybe<Scalars['UUID']>;
@@ -2422,7 +2428,7 @@ export type MetadataEventWithQueryIds = {
export type MinimalMetadata = {
__typename?: 'MinimalMetadata';
collectionHashes: Scalars['JSON'];
collectionHashes: Array<CollectionHash>;
objectMetadataItems: Array<MinimalObjectMetadata>;
views: Array<MinimalView>;
};
@@ -1,4 +1,4 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
@@ -39,9 +39,9 @@ const setupMockIsMatchingLocation = (pathname: string) => {
);
};
jest.mock('@/auth/hooks/useIsLogged');
const setupMockIsLogged = (isLogged: boolean) => {
jest.mocked(useIsLogged).mockReturnValueOnce(isLogged);
jest.mock('@/auth/hooks/useHasAccessTokenPair');
const setupMockHasAccessTokenPair = (hasAccessTokenPair: boolean) => {
jest.mocked(useHasAccessTokenPair).mockReturnValueOnce(hasAccessTokenPair);
};
const defaultHomePagePath = '/objects/companies';
@@ -83,7 +83,7 @@ const setupMockState = (
// prettier-ignore
const testCases: {
loc: AppPath;
isLoggedIn: boolean;
hasAccessTokenPair: boolean;
isWorkspaceSuspended: boolean;
onboardingStatus: OnboardingStatus | undefined;
res: string | undefined;
@@ -93,248 +93,248 @@ const testCases: {
verifyEmailRedirectPath?: string;
returnToPath?: string;
}[] = [
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.Verify, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.Verify, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.SignInUp, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.SignInUp, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: '/plan-required' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.Invite, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/create/workspace' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.Invite, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: '/plan-required' },
{ loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.Invite, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/create/workspace' },
{ loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' },
{ loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' },
{ loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' },
{ loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: '/plan-required' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.ResetPassword, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/create/workspace' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: '/plan-required' },
{ loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.ResetPassword, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/create/workspace' },
{ loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' },
{ loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' },
{ loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' },
{ loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, verifyEmailRedirectPath: '/nextPath?key=value', res: '/nextPath?key=value' },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.VerifyEmail, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, verifyEmailRedirectPath: '/nextPath?key=value', res: undefined },
{ loc: AppPath.VerifyEmail, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, verifyEmailRedirectPath: '/nextPath?key=value', res: '/nextPath?key=value' },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, verifyEmailRedirectPath: '/nextPath?key=value', res: undefined },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.CreateWorkspace, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined },
{ loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.CreateWorkspace, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.CreateWorkspace, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined },
{ loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.CreateProfile, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: undefined },
{ loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.CreateProfile, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.CreateProfile, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: undefined },
{ loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.SyncEmails, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: undefined },
{ loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.SyncEmails, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.SyncEmails, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: undefined },
{ loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.InviteTeam, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: undefined },
{ loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.InviteTeam, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.InviteTeam, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: undefined },
{ loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.BookCallDecision, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
{ loc: AppPath.BookCallDecision, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.BookCallDecision, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.BookCallDecision, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined },
{ loc: AppPath.BookCallDecision, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.BookCallDecision, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.BookCallDecision, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.BookCallDecision, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: undefined },
{ loc: AppPath.BookCallDecision, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
{ loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.BookCallDecision, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined },
{ loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: undefined },
{ loc: AppPath.BookCallDecision, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.BookCall, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
{ loc: AppPath.BookCall, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.BookCall, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.BookCall, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined },
{ loc: AppPath.BookCall, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.BookCall, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.BookCall, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.BookCall, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: undefined },
{ loc: AppPath.BookCall, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
{ loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.BookCall, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined },
{ loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: undefined },
{ loc: AppPath.BookCall, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
{ loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.PlanRequired, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.PlanRequired, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
{ loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.PlanRequired, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
{ loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.Index, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.Index, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath },
{ loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.TasksPage, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.TasksPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.TasksPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.OpportunitiesPage, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.OpportunitiesPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.OpportunitiesPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.RecordIndexPage, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined, objectNamePluralFromParams: 'existing-object', objectNamePluralFromMetadata: 'existing-object' },
{ loc: AppPath.RecordIndexPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: AppPath.NotFound, objectNamePluralFromParams: 'non-existing-object', objectNamePluralFromMetadata: 'existing-object' },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined, objectNamePluralFromParams: 'existing-object', objectNamePluralFromMetadata: 'existing-object' },
{ loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: AppPath.NotFound, objectNamePluralFromParams: 'non-existing-object', objectNamePluralFromMetadata: 'existing-object' },
{ loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.RecordShowPage, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.RecordShowPage, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.RecordShowPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.SettingsCatchAll, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.SettingsCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.SettingsCatchAll, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.Authorize, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.Authorize, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.Authorize, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.NotFoundWildcard, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.NotFoundWildcard, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.NotFoundWildcard, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.NotFound, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.NotFound, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
{ loc: AppPath.NotFound, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
{ loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
{ loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
{ loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
{ loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
{ loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
// returnToPath: should redirect to saved path instead of defaultHomePagePath
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, returnToPath: '/authorize?clientId=abc', res: '/authorize?clientId=abc' },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, returnToPath: '/objects/tasks', res: '/objects/tasks' },
{ loc: AppPath.Index, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, returnToPath: '/settings/api-keys', res: '/settings/api-keys' },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, returnToPath: '/authorize?clientId=abc', res: '/authorize?clientId=abc' },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, returnToPath: '/objects/tasks', res: '/objects/tasks' },
{ loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, returnToPath: '/settings/api-keys', res: '/settings/api-keys' },
// isOnAWorkspace:false — on default domain, don't redirect to returnToPath or defaultHomePagePath from auth pages
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, isOnAWorkspace: false, res: undefined },
{ loc: AppPath.SignInUp, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, isOnAWorkspace: false, res: undefined },
{ loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, isOnAWorkspace: false, res: undefined },
{ loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, isOnAWorkspace: false, res: undefined },
];
describe('usePageChangeEffectNavigateLocation', () => {
@@ -344,7 +344,7 @@ describe('usePageChangeEffectNavigateLocation', () => {
loc,
onboardingStatus,
isWorkspaceSuspended,
isLoggedIn,
hasAccessTokenPair,
isOnAWorkspace,
objectNamePluralFromParams,
objectNamePluralFromMetadata,
@@ -355,7 +355,7 @@ describe('usePageChangeEffectNavigateLocation', () => {
setupMockIsMatchingLocation(loc);
setupMockOnboardingStatus(onboardingStatus);
setupMockIsWorkspaceActivationStatusEqualsTo(isWorkspaceSuspended);
setupMockIsLogged(isLoggedIn);
setupMockHasAccessTokenPair(hasAccessTokenPair);
setupMockIsOnAWorkspace(isOnAWorkspace ?? true);
setupMockUseParams(objectNamePluralFromParams);
setupMockState(
@@ -1,7 +1,7 @@
import { verifyEmailRedirectPathState } from '@/app/states/verifyEmailRedirectPathState';
import { ONBOARDING_PATHS } from '@/auth/constants/OnboardingPaths';
import { ONGOING_USER_CREATION_PATHS } from '@/auth/constants/OngoingUserCreationPaths';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { returnToPathState } from '@/auth/states/returnToPathState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
@@ -26,7 +26,7 @@ const readReturnToPathFromUrlSearchParams = (): string | null => {
};
export const usePageChangeEffectNavigateLocation = () => {
const isLoggedIn = useIsLogged();
const hasAccessTokenPair = useHasAccessTokenPair();
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const onboardingStatus = useOnboardingStatus();
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
@@ -54,7 +54,7 @@ export const usePageChangeEffectNavigateLocation = () => {
: readReturnToPathFromUrlSearchParams();
if (
(!isLoggedIn || (isLoggedIn && !isOnAWorkspace)) &&
(!hasAccessTokenPair || (hasAccessTokenPair && !isOnAWorkspace)) &&
!someMatchingLocationOf([
...ONGOING_USER_CREATION_PATHS,
AppPath.ResetPassword,
@@ -140,13 +140,13 @@ export const usePageChangeEffectNavigateLocation = () => {
...ONGOING_USER_CREATION_PATHS,
]) &&
!isMatchingLocation(location, AppPath.ResetPassword) &&
isLoggedIn &&
hasAccessTokenPair &&
isOnAWorkspace
) {
return resolvedReturnToPath ?? defaultHomePagePath;
}
if (isMatchingLocation(location, AppPath.Index) && isLoggedIn) {
if (isMatchingLocation(location, AppPath.Index) && hasAccessTokenPair) {
return resolvedReturnToPath ?? defaultHomePagePath;
}
@@ -1,12 +1,12 @@
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
import { CommandMenuConfirmationModalManager } from '@/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager';
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
import { MetadataGater } from '@/metadata-store/components/MetadataGater';
import { IsAppMetadataReadyEffect } from '@/metadata-store/effect-components/IsAppMetadataReadyEffect';
import { MinimalMetadataGater } from '@/metadata-store/components/MinimalMetadataGater';
import { IsMinimalMetadataReadyEffect } from '@/metadata-store/effect-components/IsMinimalMetadataReadyEffect';
import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider';
import { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect';
import { MetadataProviderInitialEffects } from '@/metadata-store/effect-components/MetadataProviderInitialEffects';
import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect';
import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
import { AuthProvider } from '@/auth/components/AuthProvider';
import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
@@ -31,7 +31,6 @@ import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
import { UserThemeProviderEffect } from '@/ui/theme/components/UserThemeProviderEffect';
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { LazyMetadataLoadEffect } from '@/users/components/LazyMetadataLoadEffect';
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
import { StrictMode } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
@@ -45,16 +44,15 @@ export const AppRouterProviders = () => {
<ApolloProvider>
<BaseThemeProvider>
<ClientConfigProviderEffect />
<UserMetadataProviderInitialEffect />
<MinimalMetadataLoadEffect />
<MetadataProviderInitialEffects />
<LazyMetadataLoadEffect />
<IsAppMetadataReadyEffect />
<IsMinimalMetadataReadyEffect />
<WorkspaceProviderEffect />
<ClientConfigProvider>
<CaptchaProvider>
<ChromeExtensionSidecarEffect />
<ChromeExtensionSidecarProvider>
<MetadataGater>
<MinimalMetadataGater>
<AuthProvider>
<ApolloCoreProvider>
<SSEProvider>
@@ -88,7 +86,7 @@ export const AppRouterProviders = () => {
</SSEProvider>
</ApolloCoreProvider>
</AuthProvider>
</MetadataGater>
</MinimalMetadataGater>
</ChromeExtensionSidecarProvider>
</CaptchaProvider>
</ClientConfigProvider>
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -13,7 +13,7 @@ export const VerifyLoginTokenEffect = () => {
const [searchParams] = useSearchParams();
const loginToken = searchParams.get('loginToken');
const isLogged = useIsLogged();
const hasAccessTokenPair = useHasAccessTokenPair();
const navigate = useNavigateApp();
const { verifyLoginToken } = useVerifyLogin();
@@ -28,7 +28,7 @@ export const VerifyLoginTokenEffect = () => {
if (isDefined(loginToken)) {
verifyLoginToken(loginToken);
} else if (!isLogged) {
} else if (!hasAccessTokenPair) {
navigate(AppPath.SignInUp);
}
// Verify only needs to run once at mount
@@ -30,12 +30,6 @@ jest.mock('@/domain-manager/hooks/useRedirect', () => ({
})),
}));
jest.mock('@/object-metadata/hooks/useRefreshObjectMetadataItems', () => ({
useRefreshObjectMetadataItems: jest.fn().mockImplementation(() => ({
refreshObjectMetadataItems: jest.fn(),
})),
}));
jest.mock('@/domain-manager/hooks/useOrigin', () => ({
useOrigin: jest.fn().mockImplementation(() => ({
origin: 'http://localhost',
@@ -1,27 +1,27 @@
import { act, renderHook } from '@testing-library/react';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
const renderHooks = () => {
const { result } = renderHook(() => {
const isLogged = useIsLogged();
const hasAccessTokenPair = useHasAccessTokenPair();
const setTokenPair = useSetAtomState(tokenPairState);
return {
isLogged,
hasAccessTokenPair,
setTokenPair,
};
});
return { result };
};
describe('useIsLogged', () => {
describe('useHasAccessTokenPair', () => {
it('should return correct value', async () => {
const { result } = renderHooks();
expect(result.current.isLogged).toBe(false);
expect(result.current.hasAccessTokenPair).toBe(false);
await act(async () => {
result.current.setTokenPair({
@@ -36,6 +36,6 @@ describe('useIsLogged', () => {
});
});
expect(result.current.isLogged).toBe(true);
expect(result.current.hasAccessTokenPair).toBe(true);
});
});
@@ -34,7 +34,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
import { useReloadWorkspaceMetadata } from '@/metadata-store/hooks/useReloadWorkspaceMetadata';
import { useLoadMockedMinimalMetadata } from '@/metadata-store/hooks/useLoadMockedMinimalMetadata';
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
import { loginTokenState } from '@/auth/states/loginTokenState';
import {
@@ -42,7 +42,6 @@ import {
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { coreViewsState } from '@/views/states/coreViewState';
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import {
countAvailableWorkspaces,
@@ -88,8 +87,7 @@ export const useAuth = () => {
const { loadCurrentUser } = useLoadCurrentUser();
const { clearSseClient } = useClearSseClient();
const { reloadWorkspaceMetadata, resetToMockedMetadata } =
useReloadWorkspaceMetadata();
const { loadMockedMinimalMetadata } = useLoadMockedMinimalMetadata();
const { createWorkspace } = useSignUpInNewWorkspace();
const setSignInUpStep = useSetAtomState(signInUpStepState);
@@ -163,18 +161,17 @@ export const useAuth = () => {
});
store.set(loginTokenState.atom, null);
store.set(signInUpStepState.atom, SignInUpStep.Init);
store.set(coreViewsState.atom, []);
await client.clearStore();
setLastAuthenticateWorkspaceDomain(null);
await resetToMockedMetadata();
await loadMockedMinimalMetadata();
navigate(AppPath.SignInUp);
store.set(isAppEffectRedirectEnabledState.atom, true);
}, [
clearSseClient,
client,
setLastAuthenticateWorkspaceDomain,
resetToMockedMetadata,
loadMockedMinimalMetadata,
navigate,
store,
]);
@@ -305,14 +302,10 @@ export const useAuth = () => {
setIsAppEffectRedirectEnabled(false);
await loadCurrentUser();
await reloadWorkspaceMetadata();
setIsAppEffectRedirectEnabled(true);
},
[
loadCurrentUser,
handleSetAuthTokens,
reloadWorkspaceMetadata,
setIsAppEffectRedirectEnabled,
],
[loadCurrentUser, handleSetAuthTokens, setIsAppEffectRedirectEnabled],
);
const handleGetAuthTokensFromLoginToken = useCallback(
@@ -1,7 +1,7 @@
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const useIsLogged = (): boolean => {
export const useHasAccessTokenPair = (): boolean => {
const [tokenPair] = useAtomState(tokenPairState);
return !!tokenPair;
};
@@ -1,5 +1,5 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { flatObjectMetadataItemsSelector } from '@/object-metadata/states/flatObjectMetadataItemsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
export const objectPermissionsFamilySelector = createAtomFamilySelector<
@@ -14,7 +14,7 @@ export const objectPermissionsFamilySelector = createAtomFamilySelector<
({ objectNameSingular }) =>
({ get }) => {
const currentUserWorkspace = get(currentUserWorkspaceState);
const objectMetadataItems = get(objectMetadataItemsSelector);
const objectMetadataItems = get(flatObjectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === objectNameSingular,
@@ -6,7 +6,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -38,6 +38,8 @@ const getViewId = (
return undefined;
};
const SIGN_IN_BACKGROUND_OBJECT_NAME_PLURAL = 'companies';
export const MainContextStoreProvider = () => {
const location = useLocation();
const isRecordIndexPage = isMatchingLocation(
@@ -46,16 +48,21 @@ export const MainContextStoreProvider = () => {
);
const isRecordShowPage = isMatchingLocation(location, AppPath.RecordShowPage);
const isSettingsPage = useIsSettingsPage();
const showAuthModal = useShowAuthModal();
const objectNamePlural = useParams().objectNamePlural ?? '';
const objectNamePluralFromParams = useParams().objectNamePlural ?? '';
const objectNameSingular = useParams().objectNameSingular ?? '';
const objectNamePlural = showAuthModal
? SIGN_IN_BACKGROUND_OBJECT_NAME_PLURAL
: objectNamePluralFromParams;
const [searchParams] = useSearchParams();
const viewIdQueryParamRaw = searchParams.get('viewId');
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const metadataStore = useAtomFamilyStateValue(metadataStoreState, 'views');
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
@@ -108,11 +115,11 @@ export const MainContextStoreProvider = () => {
firstAvailableViewId,
);
const showAuthModal = useShowAuthModal();
const shouldComputeContextStore =
(isRecordIndexPage || isRecordShowPage || isSettingsPage) &&
!showAuthModal &&
(isRecordIndexPage ||
isRecordShowPage ||
isSettingsPage ||
showAuthModal) &&
metadataStore.status === 'up-to-date';
if (!shouldComputeContextStore) {
@@ -1,9 +1,7 @@
import React from 'react';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { UserContext } from '@/users/contexts/UserContext';
import { useLocation } from 'react-router-dom';
@@ -11,10 +9,8 @@ import { AppPath } from 'twenty-shared/types';
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const MetadataGater = ({ children }: React.PropsWithChildren) => {
const isAppMetadataReady = useAtomStateValue(isAppMetadataReadyState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const isLoggedIn = useIsLogged();
export const MinimalMetadataGater = ({ children }: React.PropsWithChildren) => {
const isMinimalMetadataReady = useAtomStateValue(isMinimalMetadataReadyState);
const location = useLocation();
const { dateFormat, timeFormat, timeZone } = useDateTimeFormat();
@@ -22,11 +18,12 @@ export const MetadataGater = ({ children }: React.PropsWithChildren) => {
const isOnExcludedPath =
isMatchingLocation(location, AppPath.Verify) ||
isMatchingLocation(location, AppPath.VerifyEmail) ||
isMatchingLocation(location, AppPath.SignInUp) ||
isMatchingLocation(location, AppPath.Invite) ||
isMatchingLocation(location, AppPath.ResetPassword) ||
isMatchingLocation(location, AppPath.CreateWorkspace);
const shouldShowLoader =
(!isAppMetadataReady && isLoggedIn && !isOnExcludedPath) ||
objectMetadataItems.length === 0;
const shouldShowLoader = !isMinimalMetadataReady && !isOnExcludedPath;
if (shouldShowLoader) {
return <UserOrMetadataLoader />;
@@ -1,33 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const FieldMetadataSSEEffect = () => {
const queryId = 'field-metadata-sse-effect';
const store = useStore();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.fieldMetadata,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.fieldMetadata,
onMetadataOperationBrowserEvent: (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'fieldMetadataItems',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
},
});
return null;
};
@@ -1,7 +1,7 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -10,8 +10,8 @@ import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
export const IsAppMetadataReadyEffect = () => {
const isLoggedIn = useIsLogged();
export const IsMinimalMetadataReadyEffect = () => {
const hasAccessTokenPair = useHasAccessTokenPair();
const currentUser = useAtomStateValue(currentUserState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const metadataStore = useAtomFamilyStateValue(
@@ -23,7 +23,9 @@ export const IsAppMetadataReadyEffect = () => {
metadataStoreState,
'views',
);
const setIsAppMetadataReady = useSetAtomState(isAppMetadataReadyState);
const setIsMinimalMetadataReady = useSetAtomState(
isMinimalMetadataReadyState,
);
useEffect(() => {
const hasActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
@@ -31,23 +33,26 @@ export const IsAppMetadataReadyEffect = () => {
const areObjectsLoaded = metadataStore.status === 'up-to-date';
const areViewsLoaded = metadataStoreViews.status === 'up-to-date';
const isReady = !areObjectsLoaded
? false
: !hasAccessTokenPair ||
(isDefined(currentUser) && (!hasActiveWorkspace || areViewsLoaded));
if (!areObjectsLoaded) {
setIsAppMetadataReady(false);
setIsMinimalMetadataReady(false);
return;
}
const isReady =
!isLoggedIn ||
(isDefined(currentUser) && (!hasActiveWorkspace || areViewsLoaded));
setIsAppMetadataReady(isReady);
if (isReady) {
setIsMinimalMetadataReady(true);
}
}, [
isLoggedIn,
hasAccessTokenPair,
currentUser,
currentWorkspace,
metadataStore.status,
metadataStoreViews.status,
setIsAppMetadataReady,
setIsMinimalMetadataReady,
]);
return null;
@@ -1,11 +0,0 @@
import { ObjectMetadataProviderInitialEffect } from '@/metadata-store/effect-components/ObjectMetadataProviderInitialEffect';
import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect';
import { ViewMetadataProviderInitialEffect } from '@/metadata-store/effect-components/ViewMetadataProviderInitialEffect';
export const MetadataProviderInitialEffects = () => (
<>
<UserMetadataProviderInitialEffect />
<ObjectMetadataProviderInitialEffect />
<ViewMetadataProviderInitialEffect />
</>
);
@@ -0,0 +1,28 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { mapAllMetadataNameToEntityKey } from '@/metadata-store/utils/mapAllMetadataNameToEntityKey';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
export const MetadataStoreSSEEffect = () => {
const store = useStore();
useListenToMetadataOperationBrowserEvent({
onMetadataOperationBrowserEvent: (eventDetail) => {
const entityKey = mapAllMetadataNameToEntityKey(eventDetail.metadataName);
if (!isDefined(entityKey)) {
return;
}
patchMetadataStoreFromSSEEvent({
store,
entityKey,
operation: eventDetail.operation,
updatedCollectionHash: eventDetail.updatedCollectionHash,
});
},
});
return null;
};
@@ -1,62 +1,73 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { useLoadMinimalMetadata } from '@/metadata-store/hooks/useLoadMinimalMetadata';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { useLoadMockedMinimalMetadata } from '@/metadata-store/hooks/useLoadMockedMinimalMetadata';
import { useLoadStaleMetadataEntities } from '@/metadata-store/hooks/useLoadStaleMetadataEntities';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useStore } from 'jotai';
import { useEffect, useState } from 'react';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
type LoadedState = 'none' | 'mocked' | 'real';
const computeDesiredLoadState = (
hasAccessTokenPair: boolean,
isActiveWorkspace: boolean,
): LoadedState => {
if (hasAccessTokenPair && isActiveWorkspace) {
return 'real';
}
return 'mocked';
};
export const MinimalMetadataLoadEffect = () => {
const isLoggedIn = useIsLogged();
const hasAccessTokenPair = useHasAccessTokenPair();
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const store = useStore();
const [hasLoaded, setHasLoaded] = useState(false);
const [loadedState, setLoadedState] = useState<LoadedState>('none');
const { loadMinimalMetadata } = useLoadMinimalMetadata();
const { loadMockedMinimalMetadata } = useLoadMockedMinimalMetadata();
const { loadStaleMetadataEntities } = useLoadStaleMetadataEntities();
const isActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
const desiredLoadState = computeDesiredLoadState(
hasAccessTokenPair,
isActiveWorkspace,
);
useEffect(() => {
if (hasLoaded) {
if (!isCurrentUserLoaded || loadedState === desiredLoadState) {
return;
}
if (!isCurrentUserLoaded || !isLoggedIn) {
return;
}
setLoadedState(desiredLoadState);
if (!isWorkspaceActiveOrSuspended(currentWorkspace)) {
return;
}
const performLoad = async () => {
if (desiredLoadState === 'mocked') {
await loadMockedMinimalMetadata();
return;
}
const objectsEntry = store.get(
metadataStoreState.atomFamily('objectMetadataItems'),
);
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
const result = await loadMinimalMetadata();
const hasObjectsFromStorage = objectsEntry.status === 'up-to-date';
const hasViewsFromStorage = viewsEntry.status === 'up-to-date';
if (hasObjectsFromStorage && hasViewsFromStorage) {
setHasLoaded(true);
return;
}
const load = async () => {
await loadMinimalMetadata();
setHasLoaded(true);
if (result?.staleEntityKeys && result.staleEntityKeys.length > 0) {
await loadStaleMetadataEntities(result.staleEntityKeys);
}
};
load();
performLoad();
}, [
hasLoaded,
isCurrentUserLoaded,
isLoggedIn,
currentWorkspace,
store,
hasAccessTokenPair,
isActiveWorkspace,
desiredLoadState,
loadedState,
loadMinimalMetadata,
loadMockedMinimalMetadata,
loadStaleMetadataEntities,
]);
return null;
@@ -1,71 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { useApolloClient } from '@apollo/client/react';
import {
AllMetadataName,
FindManyNavigationMenuItemsDocument,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const NavigationMenuItemSSEEffect = () => {
const queryId = 'navigation-menu-item-sse-effect';
const store = useStore();
const { updateDraft, applyChanges } = useMetadataStore();
const client = useApolloClient();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.navigationMenuItem,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.navigationMenuItem,
onMetadataOperationBrowserEvent: async (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'navigationMenuItems',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
const result = await client.query({
query: FindManyNavigationMenuItemsDocument,
fetchPolicy: 'network-only',
});
if (!isDefined(result.data?.navigationMenuItems)) {
return;
}
const existingNavigationMenuItems = store.get(
navigationMenuItemsState.atom,
);
if (
!isDeeplyEqual(
existingNavigationMenuItems,
result.data.navigationMenuItems,
)
) {
store.set(
navigationMenuItemsState.atom,
result.data.navigationMenuItems,
);
updateDraft('navigationMenuItems', result.data.navigationMenuItems);
applyChanges();
}
},
});
return null;
};
@@ -1,61 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { useApolloClient } from '@apollo/client/react';
import {
AllMetadataName,
FindManyNavigationMenuItemsDocument,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const ObjectMetadataItemSSEEffect = () => {
const queryId = 'object-metadata-sse-effect';
const store = useStore();
const client = useApolloClient();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.objectMetadata,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.objectMetadata,
onMetadataOperationBrowserEvent: async (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'objectMetadataItems',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
const navigationMenuItemsResult = await client.query({
query: FindManyNavigationMenuItemsDocument,
fetchPolicy: 'network-only',
});
const existingNavigationMenuItems = store.get(
navigationMenuItemsState.atom,
);
if (
!isDeeplyEqual(
existingNavigationMenuItems,
navigationMenuItemsResult.data?.navigationMenuItems,
)
) {
store.set(
navigationMenuItemsState.atom,
navigationMenuItemsResult.data?.navigationMenuItems ?? [],
);
}
},
});
return null;
};
@@ -1,47 +0,0 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useEffect, useState } from 'react';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
export const ObjectMetadataProviderInitialEffect = () => {
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const [isInitialized, setIsInitialized] = useState(false);
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
useEffect(() => {
if (isInitialized) {
return;
}
if (!isCurrentUserLoaded) {
return;
}
const shouldLoadReal = isWorkspaceActiveOrSuspended(currentWorkspace);
const loadObjectMetadata = async () => {
if (shouldLoadReal) {
await refreshObjectMetadataItems();
} else {
await loadMockedObjectMetadataItems();
}
setIsInitialized(true);
};
loadObjectMetadata();
}, [
isInitialized,
isCurrentUserLoaded,
currentWorkspace,
refreshObjectMetadataItems,
loadMockedObjectMetadataItems,
]);
return null;
};
@@ -1,37 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useRefreshPageLayouts } from '@/page-layout/hooks/useRefreshPageLayouts';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const PageLayoutSSEEffect = () => {
const queryId = 'page-layout-metadata-sse-effect';
const store = useStore();
const { refreshPageLayouts } = useRefreshPageLayouts();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.pageLayout,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.pageLayout,
onMetadataOperationBrowserEvent: (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'pageLayouts',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
refreshPageLayouts();
},
});
return null;
};
@@ -1,37 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useRefreshPageLayouts } from '@/page-layout/hooks/useRefreshPageLayouts';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const PageLayoutTabSSEEffect = () => {
const queryId = 'page-layout-tab-sse-effect';
const store = useStore();
const { refreshPageLayouts } = useRefreshPageLayouts();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.pageLayoutTab,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.pageLayoutTab,
onMetadataOperationBrowserEvent: (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'pageLayoutTabs',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
refreshPageLayouts();
},
});
return null;
};
@@ -1,37 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useRefreshPageLayouts } from '@/page-layout/hooks/useRefreshPageLayouts';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const PageLayoutWidgetSSEEffect = () => {
const queryId = 'page-layout-widget-sse-effect';
const store = useStore();
const { refreshPageLayouts } = useRefreshPageLayouts();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.pageLayoutWidget,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.pageLayoutWidget,
onMetadataOperationBrowserEvent: (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'pageLayoutWidgets',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
refreshPageLayouts();
},
});
return null;
};
@@ -1,4 +1,4 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
@@ -27,7 +27,7 @@ import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
export const UserMetadataProviderInitialEffect = () => {
const isLoggedIn = useIsLogged();
const hasAccessTokenPair = useHasAccessTokenPair();
const currentUser = useAtomStateValue(currentUserState);
const store = useStore();
const [isInitialized, setIsInitialized] = useState(false);
@@ -65,7 +65,7 @@ export const UserMetadataProviderInitialEffect = () => {
[store],
);
const shouldSkipUserQuery = !isLoggedIn || isDefined(currentUser);
const shouldSkipUserQuery = !hasAccessTokenPair || isDefined(currentUser);
const { data: userQueryData, loading: userQueryLoading } = useQuery(
GetCurrentUserDocument,
@@ -79,7 +79,7 @@ export const UserMetadataProviderInitialEffect = () => {
return;
}
if (!isLoggedIn) {
if (!hasAccessTokenPair) {
setIsCurrentUserLoaded(true);
setIsInitialized(true);
return;
@@ -165,7 +165,7 @@ export const UserMetadataProviderInitialEffect = () => {
setIsInitialized(true);
}, [
isInitialized,
isLoggedIn,
hasAccessTokenPair,
userQueryLoading,
userQueryData?.currentUser,
setCurrentUser,
@@ -1,86 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewsState } from '@/views/states/coreViewState';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import {
AllMetadataName,
type CoreViewField,
} from '~/generated-metadata/graphql';
export const ViewFieldSSEEffect = () => {
const store = useStore();
const queryId = 'view-field-metadata-sse-effect';
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.viewField,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewField,
onMetadataOperationBrowserEvent: (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'viewFields',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
const coreViews = store.get(coreViewsState.atom);
let viewId: string | undefined;
switch (eventDetail.operation.type) {
case 'create': {
const createdViewField = eventDetail.operation
.createdRecord as unknown as CoreViewField;
viewId = createdViewField.viewId;
break;
}
case 'update': {
const updatedViewField = eventDetail.operation
.updatedRecord as unknown as CoreViewField;
viewId = updatedViewField.viewId;
break;
}
case 'delete': {
const deletedViewFieldId = eventDetail.operation
.deletedRecordId as string;
const view = coreViews.find((coreView) =>
coreView.viewFields.some(
(viewField) => viewField.id === deletedViewFieldId,
),
);
viewId = view?.id;
break;
}
}
if (!isDefined(viewId)) {
return;
}
const view = coreViews.find((coreView) => coreView.id === viewId);
if (!isDefined(view)) {
return;
}
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
},
});
return null;
};
@@ -1,73 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewsState } from '@/views/states/coreViewState';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const ViewFilterGroupSSEEffect = () => {
const store = useStore();
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
useListenToEventsForQuery({
queryId: 'view-filter-groups-sse-effect',
operationSignature: {
metadataName: AllMetadataName.viewFilterGroup,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewFilterGroup,
onMetadataOperationBrowserEvent: (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'viewFilterGroups',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
const coreViews = store.get(coreViewsState.atom);
let viewId: string | undefined;
switch (eventDetail.operation.type) {
case 'create': {
viewId = (eventDetail.operation.createdRecord as { viewId?: string })
.viewId;
break;
}
case 'update': {
viewId = (eventDetail.operation.updatedRecord as { viewId?: string })
.viewId;
break;
}
case 'delete': {
const deletedId = eventDetail.operation.deletedRecordId as string;
viewId = coreViews.find((view) =>
view.viewFilterGroups?.some(
(viewFilterGroup) => viewFilterGroup.id === deletedId,
),
)?.id;
break;
}
}
if (!isDefined(viewId)) {
return;
}
const view = coreViews.find((coreView) => coreView.id === viewId);
if (isDefined(view)) {
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
}
},
});
return null;
};
@@ -1,71 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewsState } from '@/views/states/coreViewState';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const ViewFilterSSEEffect = () => {
const store = useStore();
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
useListenToEventsForQuery({
queryId: 'view-filters-sse-effect',
operationSignature: {
metadataName: AllMetadataName.viewFilter,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewFilter,
onMetadataOperationBrowserEvent: (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'viewFilters',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
const coreViews = store.get(coreViewsState.atom);
let viewId: string | undefined;
switch (eventDetail.operation.type) {
case 'create': {
viewId = (eventDetail.operation.createdRecord as { viewId?: string })
.viewId;
break;
}
case 'update': {
viewId = (eventDetail.operation.updatedRecord as { viewId?: string })
.viewId;
break;
}
case 'delete': {
const deletedId = eventDetail.operation.deletedRecordId as string;
viewId = coreViews.find((view) =>
view.viewFilters.some((viewFilter) => viewFilter.id === deletedId),
)?.id;
break;
}
}
if (!isDefined(viewId)) {
return;
}
const view = coreViews.find((coreView) => coreView.id === viewId);
if (isDefined(view)) {
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
}
},
});
return null;
};
@@ -1,47 +0,0 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { useFetchAndLoadIndexViews } from '@/metadata-store/hooks/useFetchAndLoadIndexViews';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useEffect, useState } from 'react';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
export const ViewMetadataProviderInitialEffect = () => {
const isLoggedIn = useIsLogged();
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const [isInitialized, setIsInitialized] = useState(false);
const { fetchAndLoadIndexViews } = useFetchAndLoadIndexViews();
useEffect(() => {
if (isInitialized) {
return;
}
if (!isCurrentUserLoaded) {
return;
}
if (!isLoggedIn || !isWorkspaceActiveOrSuspended(currentWorkspace)) {
setIsInitialized(true);
return;
}
const initialize = async () => {
await fetchAndLoadIndexViews();
setIsInitialized(true);
};
initialize();
}, [
isInitialized,
isCurrentUserLoaded,
isLoggedIn,
currentWorkspace,
fetchAndLoadIndexViews,
]);
return null;
};
@@ -1,130 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
import { useGetObjectMetadataItemIdFromViewRelationEventDetail } from '@/metadata-store/hooks/useGetObjectMetadataItemIdFromViewRelationEventDetail';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { type ViewField } from '@/views/types/ViewField';
import { type ViewFilter } from '@/views/types/ViewFilter';
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { type ViewSort } from '@/views/types/ViewSort';
import { isDefined } from 'twenty-shared/utils';
import { useDebouncedCallback } from 'use-debounce';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const ViewRelationsSSEEffect = () => {
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const { getObjectMetadataItemIdFromViewRelationEventDetail } =
useGetObjectMetadataItemIdFromViewRelationEventDetail();
useListenToEventsForQuery({
queryId: 'view-filters-sse-effect',
operationSignature: {
metadataName: AllMetadataName.viewFilter,
variables: {},
},
});
useListenToEventsForQuery({
queryId: 'view-sorts-sse-effect',
operationSignature: {
metadataName: AllMetadataName.viewSort,
variables: {},
},
});
useListenToEventsForQuery({
queryId: 'view-fields-sse-effect',
operationSignature: {
metadataName: AllMetadataName.viewField,
variables: {},
},
});
useListenToEventsForQuery({
queryId: 'view-filter-groups-sse-effect',
operationSignature: {
metadataName: AllMetadataName.viewFilterGroup,
variables: {},
},
});
// TODO: replace this with detailed SSE optimistic effects
const debouncedRefreshCoreViewsByObjectMetadataId = useDebouncedCallback(
(objectMetadataId: string) => {
refreshCoreViewsByObjectMetadataId(objectMetadataId);
},
500,
{
leading: false,
trailing: true,
},
);
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewFilter,
onMetadataOperationBrowserEvent: (
detail: MetadataOperationBrowserEventDetail<ViewFilter>,
) => {
const objectMetadataItemId =
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
if (!isDefined(objectMetadataItemId)) {
return;
}
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewSort,
onMetadataOperationBrowserEvent: (
detail: MetadataOperationBrowserEventDetail<ViewSort>,
) => {
const objectMetadataItemId =
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
if (!isDefined(objectMetadataItemId)) {
return;
}
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewFilterGroup,
onMetadataOperationBrowserEvent: (
detail: MetadataOperationBrowserEventDetail<ViewFilterGroup>,
) => {
const objectMetadataItemId =
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
if (!isDefined(objectMetadataItemId)) {
return;
}
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewField,
onMetadataOperationBrowserEvent: (
detail: MetadataOperationBrowserEventDetail<ViewField>,
) => {
const objectMetadataItemId =
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
if (!isDefined(objectMetadataItemId)) {
return;
}
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
},
});
return null;
};
@@ -1,93 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithoutRelations } from '@/views/types/CoreViewWithoutRelations';
import { useStore } from 'jotai';
import { AllMetadataName } from '~/generated-metadata/graphql';
import { sleep } from '~/utils/sleep';
export const ViewSSEEffect = () => {
const queryId = 'views-sse-effect';
const store = useStore();
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.view,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.view,
onMetadataOperationBrowserEvent: async (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'views',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
switch (eventDetail.operation.type) {
case 'create': {
const createdView = eventDetail.operation
.createdRecord as unknown as CoreViewWithoutRelations;
store.set(coreViewsState.atom, (prevCoreViews) => [
...prevCoreViews,
{
...createdView,
viewFields: [],
viewFieldGroups: [],
viewFilterGroups: [],
viewSorts: [],
viewFilters: [],
viewGroups: [],
},
]);
await sleep(50);
refreshCoreViewsByObjectMetadataId(createdView.objectMetadataId);
break;
}
case 'update': {
const updatedView = eventDetail.operation
.updatedRecord as unknown as CoreViewWithoutRelations;
store.set(coreViewsState.atom, (prevCoreViews) =>
prevCoreViews.map((coreView) =>
coreView.id === updatedView.id
? {
...coreView,
...updatedView,
}
: coreView,
),
);
break;
}
case 'delete': {
const deletedViewId = eventDetail.operation.deletedRecordId as string;
store.set(coreViewsState.atom, (prevCoreViews) =>
prevCoreViews.filter((coreView) => coreView.id !== deletedViewId),
);
break;
}
default:
return;
}
},
});
return null;
};
@@ -1,71 +0,0 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewsState } from '@/views/states/coreViewState';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const ViewSortSSEEffect = () => {
const store = useStore();
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
useListenToEventsForQuery({
queryId: 'view-sorts-sse-effect',
operationSignature: {
metadataName: AllMetadataName.viewSort,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewSort,
onMetadataOperationBrowserEvent: (eventDetail) => {
patchMetadataStoreFromSSEEvent(
store,
'viewSorts',
eventDetail.operation,
eventDetail.updatedCollectionHash,
);
const coreViews = store.get(coreViewsState.atom);
let viewId: string | undefined;
switch (eventDetail.operation.type) {
case 'create': {
viewId = (eventDetail.operation.createdRecord as { viewId?: string })
.viewId;
break;
}
case 'update': {
viewId = (eventDetail.operation.updatedRecord as { viewId?: string })
.viewId;
break;
}
case 'delete': {
const deletedId = eventDetail.operation.deletedRecordId as string;
viewId = coreViews.find((view) =>
view.viewSorts.some((viewSort) => viewSort.id === deletedId),
)?.id;
break;
}
}
if (!isDefined(viewId)) {
return;
}
const view = coreViews.find((coreView) => coreView.id === viewId);
if (isDefined(view)) {
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
}
},
});
return null;
};
@@ -21,7 +21,10 @@ export const FIND_MINIMAL_METADATA = gql`
key
objectMetadataId
}
collectionHashes
collectionHashes {
collectionName
hash
}
}
}
`;
@@ -1,51 +0,0 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useSetIndexViews } from '@/metadata-store/hooks/useSetIndexViews';
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useApolloClient } from '@apollo/client/react';
import {
ViewType,
FindAllCoreViewsDocument,
} from '~/generated-metadata/graphql';
const INDEX_VIEW_TYPES = [ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR];
export const useFetchAndLoadIndexViews = () => {
const client = useApolloClient();
const { updateDraft, applyChanges } = useMetadataStore();
const { setIndexViews } = useSetIndexViews();
const fetchAndLoadIndexViews = useCallback(async () => {
const result = await client.query({
query: FindAllCoreViewsDocument,
variables: { viewTypes: INDEX_VIEW_TYPES },
fetchPolicy: 'network-only',
});
if (isDefined(result.data?.getCoreViews)) {
setIndexViews(result.data.getCoreViews);
const {
flatViews,
flatViewFields,
flatViewFilters,
flatViewSorts,
flatViewGroups,
flatViewFilterGroups,
flatViewFieldGroups,
} = splitViewWithRelated(result.data.getCoreViews);
updateDraft('views', flatViews);
updateDraft('viewFields', flatViewFields);
updateDraft('viewFilters', flatViewFilters);
updateDraft('viewSorts', flatViewSorts);
updateDraft('viewGroups', flatViewGroups);
updateDraft('viewFilterGroups', flatViewFilterGroups);
updateDraft('viewFieldGroups', flatViewFieldGroups);
applyChanges();
}
}, [client, setIndexViews, updateDraft, applyChanges]);
return { fetchAndLoadIndexViews };
};
@@ -1,39 +0,0 @@
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
import { useGetViewIdFromViewSSERelationEventDetail } from '@/metadata-store/hooks/useGetViewIdFromViewSSERelationEventDetail';
import { useGetViewById } from '@/views/hooks/useGetViewById';
import { type ViewField } from '@/views/types/ViewField';
import { type ViewFilter } from '@/views/types/ViewFilter';
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { type ViewSort } from '@/views/types/ViewSort';
import { isDefined } from 'twenty-shared/utils';
export const useGetObjectMetadataItemIdFromViewRelationEventDetail = () => {
const { getViewIdFromViewSSERelationEventDetail } =
useGetViewIdFromViewSSERelationEventDetail();
const { getViewById } = useGetViewById();
const getObjectMetadataItemIdFromViewRelationEventDetail = <
T extends ViewFilter | ViewFilterGroup | ViewField | ViewSort,
>(
eventDetail: MetadataOperationBrowserEventDetail<T>,
) => {
const viewId = getViewIdFromViewSSERelationEventDetail(eventDetail);
if (!isDefined(viewId)) {
return null;
}
const { view } = getViewById(viewId);
if (!isDefined(view)) {
return null;
}
return view.objectMetadataId;
};
return {
getObjectMetadataItemIdFromViewRelationEventDetail,
};
};
@@ -1,64 +0,0 @@
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { type ViewField } from '@/views/types/ViewField';
import { type ViewFilter } from '@/views/types/ViewFilter';
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { type ViewSort } from '@/views/types/ViewSort';
import { type Nullable } from 'twenty-shared/types';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const useGetViewIdFromViewSSERelationEventDetail = () => {
const coreViews = useAtomStateValue(coreViewsState);
const getViewIdFromViewSSERelationEventDetail = <
T extends ViewFilter | ViewFilterGroup | ViewField | ViewSort,
>(
eventDetail: MetadataOperationBrowserEventDetail<T>,
) => {
switch (eventDetail.operation.type) {
case 'create': {
return eventDetail.operation.createdRecord?.viewId as Nullable<string>;
}
case 'update': {
return eventDetail.operation.updatedRecord?.viewId as Nullable<string>;
}
case 'delete': {
const deletedRecordId = eventDetail.operation.deletedRecordId;
switch (eventDetail.metadataName) {
case AllMetadataName.viewFilter:
return coreViews.find((view) =>
view.viewFilters.some(
(viewFilter) => viewFilter.id === deletedRecordId,
),
)?.id;
case AllMetadataName.viewField:
return coreViews.find((view) =>
view.viewFields.some(
(viewField) => viewField.id === deletedRecordId,
),
)?.id;
case AllMetadataName.viewFilterGroup:
return coreViews.find((view) =>
view.viewFilterGroups?.some(
(viewFilterGroup) => viewFilterGroup.id === deletedRecordId,
),
)?.id;
case AllMetadataName.viewSort:
return coreViews.find((view) =>
view.viewSorts.some(
(viewSort) => viewSort.id === deletedRecordId,
),
)?.id;
default:
return null;
}
}
}
};
return {
getViewIdFromViewSSERelationEventDetail,
};
};
@@ -1,5 +1,4 @@
import { FIND_MINIMAL_METADATA } from '@/metadata-store/graphql/queries/findMinimalMetadata';
import { metadataCollectionHashesState } from '@/metadata-store/states/metadataCollectionHashesState';
import {
metadataStoreState,
type MetadataEntityKey,
@@ -30,51 +29,56 @@ export const useLoadMinimalMetadata = () => {
const { objectMetadataItems, views, collectionHashes } =
result.data.minimalMetadata;
const localHashes = store.get(metadataCollectionHashesState.atom);
const serverHashes: Partial<Record<MetadataEntityKey, string>> = {};
const staleEntityKeys: MetadataEntityKey[] = [];
if (isDefined(collectionHashes)) {
for (const [metadataName, hash] of Object.entries(
collectionHashes as Record<string, string>,
)) {
const entityKey = mapAllMetadataNameToEntityKey(metadataName);
for (const { collectionName, hash } of collectionHashes) {
const entityKey = mapAllMetadataNameToEntityKey(collectionName);
if (isDefined(entityKey)) {
serverHashes[entityKey] = hash;
if (!isDefined(entityKey)) {
continue;
}
const entry = store.get(metadataStoreState.atomFamily(entityKey));
if (entry.currentCollectionHash !== hash) {
staleEntityKeys.push(entityKey);
}
store.set(metadataStoreState.atomFamily(entityKey), (prev) => ({
...prev,
draftCollectionHash: hash,
}));
}
}
store.set(metadataCollectionHashesState.atom, serverHashes);
const staleEntityKeys: MetadataEntityKey[] = Object.entries(serverHashes)
.filter(
([entityKey, hash]) =>
localHashes[entityKey as MetadataEntityKey] !== hash,
)
.map(([entityKey]) => entityKey as MetadataEntityKey);
const objectsEntry = store.get(
metadataStoreState.atomFamily('objectMetadataItems'),
);
if (objectsEntry.status === 'empty') {
store.set(metadataStoreState.atomFamily('objectMetadataItems'), {
current: objectMetadataItems as unknown as FlatObjectMetadataItem[],
draft: [],
status: 'up-to-date',
});
store.set(
metadataStoreState.atomFamily('objectMetadataItems'),
(prev) => ({
...prev,
current: objectMetadataItems as unknown as FlatObjectMetadataItem[],
status: 'up-to-date',
currentCollectionHash: prev.draftCollectionHash,
draftCollectionHash: undefined,
}),
);
}
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
if (viewsEntry.status === 'empty') {
store.set(metadataStoreState.atomFamily('views'), {
store.set(metadataStoreState.atomFamily('views'), (prev) => ({
...prev,
current: views as unknown as FlatView[],
draft: [],
status: 'up-to-date',
});
currentCollectionHash: prev.draftCollectionHash,
draftCollectionHash: undefined,
}));
}
return { staleEntityKeys };
@@ -0,0 +1,64 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
import { useCallback } from 'react';
const MOCKED_COLLECTION_HASH = 'mocked';
export const useLoadMockedMinimalMetadata = () => {
const { updateDraft, applyChanges, resetMetadataStore } = useMetadataStore();
const loadMockedMinimalMetadata = useCallback(async () => {
resetMetadataStore();
const [
{ generatedMockObjectMetadataItems },
{ mockedCoreViews },
{ mockedNavigationMenuItems },
] = await Promise.all([
import('~/testing/utils/generatedMockObjectMetadataItems'),
import('~/testing/mock-data/generated/metadata/views/mock-views-data'),
import(
'~/testing/mock-data/generated/metadata/navigation-menu-items/mock-navigation-menu-items-data'
),
]);
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
updateDraft('objectMetadataItems', flatObjects, MOCKED_COLLECTION_HASH);
updateDraft('fieldMetadataItems', flatFields, MOCKED_COLLECTION_HASH);
updateDraft('indexMetadataItems', flatIndexes, MOCKED_COLLECTION_HASH);
const {
flatViews,
flatViewFields,
flatViewFilters,
flatViewSorts,
flatViewGroups,
flatViewFilterGroups,
flatViewFieldGroups,
} = splitViewWithRelated(mockedCoreViews);
updateDraft('views', flatViews, MOCKED_COLLECTION_HASH);
updateDraft('viewFields', flatViewFields, MOCKED_COLLECTION_HASH);
updateDraft('viewFilters', flatViewFilters, MOCKED_COLLECTION_HASH);
updateDraft('viewSorts', flatViewSorts, MOCKED_COLLECTION_HASH);
updateDraft('viewGroups', flatViewGroups, MOCKED_COLLECTION_HASH);
updateDraft(
'viewFilterGroups',
flatViewFilterGroups,
MOCKED_COLLECTION_HASH,
);
updateDraft('viewFieldGroups', flatViewFieldGroups, MOCKED_COLLECTION_HASH);
updateDraft(
'navigationMenuItems',
mockedNavigationMenuItems,
MOCKED_COLLECTION_HASH,
);
applyChanges();
}, [updateDraft, applyChanges, resetMetadataStore]);
return { loadMockedMinimalMetadata };
};
@@ -0,0 +1,206 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { type MetadataEntityKey } from '@/metadata-store/states/metadataStoreState';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { splitPageLayoutWithRelated } from '@/metadata-store/utils/splitPageLayoutWithRelated';
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
import { useApolloClient } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
FindAllCoreViewsDocument,
FindAllRecordPageLayoutsDocument,
FindFieldsWidgetCoreViewsDocument,
FindManyLogicFunctionsDocument,
FindManyNavigationMenuItemsDocument,
type ObjectMetadataItemsQuery,
ViewType,
} from '~/generated-metadata/graphql';
const OBJECTS_GROUP_KEYS: MetadataEntityKey[] = [
'objectMetadataItems',
'fieldMetadataItems',
'indexMetadataItems',
];
const VIEWS_GROUP_KEYS: MetadataEntityKey[] = [
'views',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
'viewFieldGroups',
];
const PAGE_LAYOUTS_GROUP_KEYS: MetadataEntityKey[] = [
'pageLayouts',
'pageLayoutTabs',
'pageLayoutWidgets',
];
const INDEX_VIEW_TYPES = [ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR];
const FIELDS_WIDGET_VIEW_TYPES = [ViewType.FIELDS_WIDGET];
const hasOverlap = (
staleKeys: MetadataEntityKey[],
groupKeys: MetadataEntityKey[],
): boolean => staleKeys.some((key) => groupKeys.includes(key));
export const useLoadStaleMetadataEntities = () => {
const client = useApolloClient();
const store = useStore();
const { updateDraft, applyChanges } = useMetadataStore();
const loadStaleMetadataEntities = useCallback(
async (staleEntityKeys: MetadataEntityKey[]) => {
if (staleEntityKeys.length === 0) {
return;
}
const fetchPromises: Promise<void>[] = [];
if (hasOverlap(staleEntityKeys, OBJECTS_GROUP_KEYS)) {
fetchPromises.push(
client
.query<ObjectMetadataItemsQuery>({
query: FIND_MANY_OBJECT_METADATA_ITEMS,
fetchPolicy: 'network-only',
})
.then((result) => {
const compositeObjects =
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: result.data,
});
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(compositeObjects);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
}),
);
}
if (hasOverlap(staleEntityKeys, VIEWS_GROUP_KEYS)) {
fetchPromises.push(
Promise.all([
client.query({
query: FindAllCoreViewsDocument,
variables: { viewTypes: INDEX_VIEW_TYPES },
fetchPolicy: 'network-only',
}),
client.query({
query: FindFieldsWidgetCoreViewsDocument,
variables: { viewTypes: FIELDS_WIDGET_VIEW_TYPES },
fetchPolicy: 'network-only',
}),
]).then(([indexViewsResult, fieldsWidgetViewsResult]) => {
const allViews = [
...(indexViewsResult.data?.getCoreViews ?? []),
...(fieldsWidgetViewsResult.data?.getCoreViews ?? []),
];
const {
flatViews,
flatViewFields,
flatViewFilters,
flatViewSorts,
flatViewGroups,
flatViewFilterGroups,
flatViewFieldGroups,
} = splitViewWithRelated(allViews);
updateDraft('views', flatViews);
updateDraft('viewFields', flatViewFields);
updateDraft('viewFilters', flatViewFilters);
updateDraft('viewSorts', flatViewSorts);
updateDraft('viewGroups', flatViewGroups);
updateDraft('viewFilterGroups', flatViewFilterGroups);
updateDraft('viewFieldGroups', flatViewFieldGroups);
}),
);
}
if (hasOverlap(staleEntityKeys, PAGE_LAYOUTS_GROUP_KEYS)) {
fetchPromises.push(
client
.query({
query: FindAllRecordPageLayoutsDocument,
fetchPolicy: 'network-only',
})
.then((result) => {
if (!isDefined(result.data?.getPageLayouts)) {
return;
}
const transformed =
result.data.getPageLayouts.map(transformPageLayout);
const {
flatPageLayouts,
flatPageLayoutTabs,
flatPageLayoutWidgets,
} = splitPageLayoutWithRelated(transformed);
updateDraft('pageLayouts', flatPageLayouts);
updateDraft('pageLayoutTabs', flatPageLayoutTabs);
updateDraft('pageLayoutWidgets', flatPageLayoutWidgets);
}),
);
}
if (staleEntityKeys.includes('logicFunctions')) {
fetchPromises.push(
client
.query({
query: FindManyLogicFunctionsDocument,
fetchPolicy: 'network-only',
})
.then((result) => {
if (!isDefined(result.data?.findManyLogicFunctions)) {
return;
}
store.set(
logicFunctionsState.atom,
result.data.findManyLogicFunctions,
);
updateDraft('logicFunctions', result.data.findManyLogicFunctions);
}),
);
}
if (staleEntityKeys.includes('navigationMenuItems')) {
fetchPromises.push(
client
.query({
query: FindManyNavigationMenuItemsDocument,
fetchPolicy: 'network-only',
})
.then((result) => {
if (!isDefined(result.data?.navigationMenuItems)) {
return;
}
updateDraft(
'navigationMenuItems',
result.data.navigationMenuItems,
);
}),
);
}
await Promise.all(fetchPromises);
applyChanges();
},
[client, store, updateDraft, applyChanges],
);
return { loadStaleMetadataEntities };
};
@@ -1,5 +1,4 @@
import { metadataCollectionHashesState } from '@/metadata-store/states/metadataCollectionHashesState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import {
ALL_METADATA_ENTITY_KEYS,
metadataStoreState,
@@ -37,8 +36,7 @@ export const resetMetadataStore = (store: JotaiStore) => {
store.set(metadataStoreState.atomFamily(key), EMPTY_ENTRY);
}
store.set(metadataCollectionHashesState.atom, {});
store.set(isAppMetadataReadyState.atom, false);
store.set(isMinimalMetadataReadyState.atom, false);
};
const changeMetadataEntityAsUpToDate = (
@@ -51,6 +49,9 @@ const changeMetadataEntityAsUpToDate = (
current: entry.draft,
draft: [],
status: 'up-to-date',
currentCollectionHash:
entry.draftCollectionHash ?? entry.currentCollectionHash,
draftCollectionHash: undefined,
});
};
@@ -58,7 +59,11 @@ export const useMetadataStore = () => {
const store = useStore();
const updateDraft = useCallback(
<K extends MetadataEntityKey>(key: K, data: MetadataEntityTypeMap[K][]) => {
<K extends MetadataEntityKey>(
key: K,
data: MetadataEntityTypeMap[K][],
collectionHash?: string,
) => {
const currentEntry = store.get(metadataStoreState.atomFamily(key));
if (
@@ -72,6 +77,9 @@ export const useMetadataStore = () => {
...prev,
draft: data,
status: 'draft-pending' as const,
...(collectionHash !== undefined
? { draftCollectionHash: collectionHash }
: {}),
}));
},
[store],
@@ -1,54 +0,0 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { coreViewsState } from '@/views/states/coreViewState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useReloadWorkspaceMetadata = () => {
const store = useStore();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const { updateDraft, applyChanges, resetMetadataStore } = useMetadataStore();
const reloadWorkspaceMetadata = useCallback(async () => {
resetMetadataStore();
await refreshObjectMetadataItems();
const loadedViews = store.get(coreViewsState.atom);
const {
flatViews,
flatViewFields,
flatViewFilters,
flatViewSorts,
flatViewGroups,
flatViewFilterGroups,
flatViewFieldGroups,
} = splitViewWithRelated(loadedViews);
updateDraft('views', flatViews);
updateDraft('viewFields', flatViewFields);
updateDraft('viewFilters', flatViewFilters);
updateDraft('viewSorts', flatViewSorts);
updateDraft('viewGroups', flatViewGroups);
updateDraft('viewFilterGroups', flatViewFilterGroups);
updateDraft('viewFieldGroups', flatViewFieldGroups);
applyChanges();
}, [
resetMetadataStore,
refreshObjectMetadataItems,
store,
updateDraft,
applyChanges,
]);
const resetToMockedMetadata = useCallback(async () => {
resetMetadataStore();
await loadMockedObjectMetadataItems();
}, [resetMetadataStore, loadMockedObjectMetadataItems]);
return { reloadWorkspaceMetadata, resetToMockedMetadata };
};
@@ -1,29 +0,0 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { ViewType } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useSetIndexViews = () => {
const store = useStore();
const setIndexViews = useCallback(
(indexViews: CoreViewWithRelations[]) => {
const existingCoreViews = store.get(coreViewsState.atom);
const existingFieldsWidgetViews = existingCoreViews.filter(
(view) => view.type === ViewType.FIELDS_WIDGET,
);
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
store.set(coreViewsState.atom, mergedViews);
}
},
[store],
);
return {
setIndexViews,
};
};
@@ -1,27 +0,0 @@
import { metadataCollectionHashesState } from '@/metadata-store/states/metadataCollectionHashesState';
import {
ALL_METADATA_ENTITY_KEYS,
type MetadataEntityKey,
} from '@/metadata-store/states/metadataStoreState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useMemo } from 'react';
// Returns the list of MetadataEntityKey whose local collection hash
// is missing or differs from the last known server collection hash.
export const useStaleMetadataEntities = (): MetadataEntityKey[] => {
const metadataCollectionHashes = useAtomStateValue(
metadataCollectionHashesState,
);
return useMemo(() => {
const staleKeys: MetadataEntityKey[] = [];
for (const entityKey of ALL_METADATA_ENTITY_KEYS) {
if (metadataCollectionHashes[entityKey] === undefined) {
staleKeys.push(entityKey);
}
}
return staleKeys;
}, [metadataCollectionHashes]);
};
@@ -1,6 +1,6 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const isAppMetadataReadyState = createAtomState<boolean>({
key: 'isAppMetadataReadyState',
export const isMinimalMetadataReadyState = createAtomState<boolean>({
key: 'isMinimalMetadataReadyState',
defaultValue: false,
});
@@ -1,11 +0,0 @@
import { type MetadataEntityKey } from '@/metadata-store/states/metadataStoreState';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const metadataCollectionHashesState = createAtomState<
Partial<Record<MetadataEntityKey, string>>
>({
key: 'metadataCollectionHashesState',
defaultValue: {},
useLocalStorage: true,
localStorageOptions: { getOnInit: true },
});
@@ -38,6 +38,8 @@ export type MetadataStoreItem = {
current: object[];
draft: object[];
status: MetadataEntityStoreStatus;
currentCollectionHash?: string;
draftCollectionHash?: string;
};
const METADATA_STORE_ITEM_INITIAL_VALUE: MetadataStoreItem = {
@@ -1,4 +1,3 @@
import { metadataCollectionHashesState } from '@/metadata-store/states/metadataCollectionHashesState';
import {
metadataStoreState,
type MetadataEntityKey,
@@ -22,19 +21,29 @@ type SSEEventOperation =
deletedRecordId: string;
};
export const patchMetadataStoreFromSSEEvent = (
store: JotaiStore,
entityKey: MetadataEntityKey,
operation: SSEEventOperation,
updatedCollectionHash?: string,
) => {
export const patchMetadataStoreFromSSEEvent = ({
store,
entityKey,
operation,
updatedCollectionHash,
}: {
store: JotaiStore;
entityKey: MetadataEntityKey;
operation: SSEEventOperation;
updatedCollectionHash?: string;
}) => {
const entry = store.get(metadataStoreState.atomFamily(entityKey));
const currentItems = entry.current as Array<{ id: string }>;
const hashUpdate = isDefined(updatedCollectionHash)
? { currentCollectionHash: updatedCollectionHash }
: {};
switch (operation.type) {
case 'create': {
store.set(metadataStoreState.atomFamily(entityKey), {
...entry,
...hashUpdate,
current: [...currentItems, operation.createdRecord],
});
break;
@@ -42,6 +51,7 @@ export const patchMetadataStoreFromSSEEvent = (
case 'update': {
store.set(metadataStoreState.atomFamily(entityKey), {
...entry,
...hashUpdate,
current: currentItems.map((item) =>
item.id === (operation.updatedRecord as { id: string }).id
? { ...item, ...operation.updatedRecord }
@@ -53,6 +63,7 @@ export const patchMetadataStoreFromSSEEvent = (
case 'delete': {
store.set(metadataStoreState.atomFamily(entityKey), {
...entry,
...hashUpdate,
current: currentItems.filter(
(item) => item.id !== operation.deletedRecordId,
),
@@ -60,13 +71,4 @@ export const patchMetadataStoreFromSSEEvent = (
break;
}
}
if (isDefined(updatedCollectionHash)) {
const currentHashes = store.get(metadataCollectionHashesState.atom);
store.set(metadataCollectionHashesState.atom, {
...currentHashes,
[entityKey]: updatedCollectionHash,
});
}
};
@@ -11,7 +11,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
@@ -38,7 +38,7 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
isNavigationMenuInEditModeState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
const objectMetadataItem =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
@@ -24,7 +24,7 @@ import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/util
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { useStore } from 'jotai';
@@ -42,7 +42,7 @@ export const useHandleAddToNavigationDrop = () => {
const { openNavigationMenuItemInSidePanel } =
useOpenNavigationMenuItemInSidePanel();
const { objectMetadataItems } = useObjectMetadataItems();
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const { getIcon } = useIcons();
const setIsNavigationMenuInEditMode = useSetAtomState(
isNavigationMenuInEditModeState,
@@ -6,7 +6,7 @@ import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -26,7 +26,7 @@ type NavigationMenuItemFolderEntry = Pick<
>;
export const useNavigationMenuItemsByFolder = () => {
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const { navigationMenuItems, workspaceNavigationMenuItems } =
@@ -2,7 +2,7 @@ import { isDefined } from 'twenty-shared/utils';
import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/utils/getObjectMetadataIdsInDraft';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
@@ -15,7 +15,7 @@ type NavigationMenuItemDraft = {
export const useNavigationMenuObjectMetadataFromDraft = (
currentDraft: NavigationMenuItemDraft[],
) => {
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
const objectMetadataIdsInWorkspace = getObjectMetadataIdsInDraft(
@@ -1,9 +1,12 @@
import { useCallback } from 'react';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { FindManyNavigationMenuItemsDocument } from '~/generated-metadata/graphql';
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import {
FindManyNavigationMenuItemsDocument,
type NavigationMenuItem,
} from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared/utils';
import { useStore } from 'jotai';
@@ -11,15 +14,16 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
const store = useStore();
const apolloCoreClient = useApolloCoreClient();
const cache = apolloCoreClient.cache;
const setNavigationMenuItems = useSetAtomState(navigationMenuItemsState);
const { updateDraft, applyChanges } = useMetadataStore();
const removeNavigationMenuItemsByTargetRecordIds = useCallback(
(targetRecordIds: string[]) => {
const targetRecordIdsSet = new Set(targetRecordIds);
const currentNavigationMenuItems = store.get(
navigationMenuItemsState.atom,
const entry = store.get(
metadataStoreState.atomFamily('navigationMenuItems'),
);
const currentNavigationMenuItems =
entry.current as unknown as NavigationMenuItem[];
const updatedNavigationMenuItems = currentNavigationMenuItems.filter(
(item) =>
@@ -27,7 +31,8 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
!targetRecordIdsSet.has(item.targetRecordId),
);
setNavigationMenuItems(updatedNavigationMenuItems);
updateDraft('navigationMenuItems', updatedNavigationMenuItems);
applyChanges();
cache.updateQuery(
{ query: FindManyNavigationMenuItemsDocument },
@@ -43,7 +48,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
},
);
},
[cache, setNavigationMenuItems, store],
[cache, store, updateDraft, applyChanges],
);
return {
@@ -2,12 +2,12 @@ import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/ho
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
export const useSelectedNavigationMenuItemEditItemObjectMetadata = () => {
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
const { objectMetadataItems } = useObjectMetadataItems();
@@ -7,7 +7,7 @@ import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -16,7 +16,7 @@ import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
export const useSortedNavigationMenuItems = () => {
const { navigationMenuItems, workspaceNavigationMenuItems } =
useNavigationMenuItemsData();
const coreViews = useAtomStateValue(coreViewsState).map(
const coreViews = useAtomStateValue(coreViewsSelector).map(
convertCoreViewToView,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
@@ -4,7 +4,7 @@ import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavig
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { isDefined } from 'twenty-shared/utils';
@@ -15,7 +15,7 @@ export const useWorkspaceNavigationMenuItems = (): {
} => {
const { workspaceNavigationMenuItems: rawWorkspaceNavigationMenuItems } =
useNavigationMenuItemsData();
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
@@ -10,7 +10,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { isDefined } from 'twenty-shared/utils';
@@ -35,7 +35,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
const { workspaceNavigationMenuItemsByFolder } =
useNavigationMenuItemsByFolder();
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -1,7 +1,14 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const navigationMenuItemsState = createAtomState<NavigationMenuItem[]>({
export const navigationMenuItemsState = createAtomSelector<
NavigationMenuItem[]
>({
key: 'navigationMenuItemsState',
defaultValue: [],
get: ({ get }) => {
const entry = get(metadataStoreState, 'navigationMenuItems');
return entry.current as unknown as NavigationMenuItem[];
},
});
@@ -1,14 +1,12 @@
import { renderHook, waitFor } from '@testing-library/react';
import { createElement, useEffect, type ReactNode } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { coreViewsState } from '@/views/states/coreViewState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { renderHook, waitFor } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { createElement, useEffect, type ReactNode } from 'react';
import { AppPath } from 'twenty-shared/types';
import {
ViewOpenRecordIn,
@@ -16,8 +14,10 @@ import {
ViewVisibility,
} from '~/generated-metadata/graphql';
import { mockedUserData } from '~/testing/mock-data/users';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
import { setTestCoreViewsInMetadataStore } from '~/testing/utils/setTestCoreViewsInMetadataStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
const Wrapper = ({ children }: { children: ReactNode }) =>
createElement(JotaiProvider, { store: jotaiStore }, children);
@@ -40,11 +40,10 @@ const renderHooks = ({
const setCurrentUserWorkspace = useSetAtomState(
currentUserWorkspaceState,
);
const setCoreViews = useSetAtomState(coreViewsState);
useEffect(() => {
if (withExistingView) {
setCoreViews([
setTestCoreViewsInMetadataStore(jotaiStore, [
{
id: 'viewId',
name: 'Test View',
@@ -69,14 +68,14 @@ const renderHooks = ({
},
]);
} else {
setCoreViews([]);
setTestCoreViewsInMetadataStore(jotaiStore, []);
}
if (withCurrentUser) {
setCurrentUser(mockedUserData);
setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
}
}, [setCurrentUser, setCurrentUserWorkspace, setCoreViews]);
}, [setCurrentUser, setCurrentUserWorkspace]);
return useDefaultHomePagePath();
},
@@ -5,7 +5,7 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import isEmpty from 'lodash.isempty';
import { useCallback, useMemo } from 'react';
@@ -43,7 +43,7 @@ export const useDefaultHomePagePath = () => {
[readableAlphaSortedActiveNonSystemObjectMetadataItems],
);
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const getFirstView = useCallback(
(objectMetadataItemId: string | undefined | null) => {
@@ -1,6 +1,6 @@
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -16,7 +16,7 @@ export const useSetLastVisitedViewForObjectMetadataNamePlural = () => {
objectNamePlural: string;
viewId: string;
}) => {
const views = store.get(coreViewsState.atom);
const views = store.get(coreViewsSelector.atom);
const view = views.find(
(view: CoreViewWithRelations) => view.id === viewId,
@@ -4,7 +4,7 @@ import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/com
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
type NavigationDrawerSectionForWorkspaceItemObjectContentProps =
@@ -17,7 +17,7 @@ export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({
onActiveObjectMetadataItemClick,
}: NavigationDrawerSectionForWorkspaceItemObjectContentProps) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
item as ProcessedNavigationMenuItem,
@@ -24,7 +24,7 @@ import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/compo
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -64,7 +64,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
);
const { toggleNavigationSection, isNavigationSectionOpen } =
useNavigationSection('Workspace');
const coreViews = useAtomStateValue(coreViewsState);
const coreViews = useAtomStateValue(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -244,7 +244,6 @@ describe('useFieldMetadataItem', () => {
await act(async () => {
const res = await result.current.deleteMetadataField({
idToDelete: fieldMetadataItem.id,
objectMetadataId,
});
jestExpectSuccessfulMetadataRequestResult(res);
@@ -264,7 +263,6 @@ describe('useFieldMetadataItem', () => {
await act(async () => {
const res = await result.current.deleteMetadataField({
idToDelete: fieldRelationMetadataItem.id,
objectMetadataId,
});
jestExpectSuccessfulMetadataRequestResult(res);
@@ -1,48 +0,0 @@
import { MockedProvider } from '@apollo/client/testing/react';
import { renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import {
query,
responseData,
} from '@/object-metadata/hooks/__mocks__/useFindManyObjectMetadataItems';
const mocks = [
{
request: {
query,
},
result: jest.fn(() => ({
data: {
objects: responseData,
},
})),
},
];
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks}>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
{children}
</SnackBarComponentInstanceContext.Provider>
</MockedProvider>
);
describe('useFindManyObjectMetadataItems', () => {
it('should activateMetadataField', async () => {
const { result } = renderHook(() => useFindManyObjectMetadataItems(), {
wrapper: Wrapper,
});
const { loading, error, objectMetadataItems } = result.current;
expect(loading).toBe(true);
expect(error).toBeUndefined();
expect(objectMetadataItems).toEqual([]);
});
});
@@ -5,25 +5,17 @@ import {
} from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
export const useCreateOneFieldMetadataItem = () => {
const { refreshObjectMetadataItems } =
useRefreshObjectMetadataItems('network-only');
const [createOneFieldMetadataItemMutation] = useMutation(
CreateOneFieldMetadataItemDocument,
);
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
@@ -43,10 +35,6 @@ export const useCreateOneFieldMetadataItem = () => {
},
});
await refreshObjectMetadataItems();
await refreshCoreViewsByObjectMetadataId(input.objectMetadataId);
return {
status: 'successful',
response,
@@ -5,21 +5,12 @@ import {
} from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const useCreateOneObjectMetadataItem = () => {
const { refreshObjectMetadataItems } =
useRefreshObjectMetadataItems('network-only');
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const [createOneObjectMetadataItemMutation] = useMutation(
CreateOneObjectMetadataItemDocument,
);
@@ -41,14 +32,6 @@ export const useCreateOneObjectMetadataItem = () => {
},
});
await refreshObjectMetadataItems();
if (isDefined(createdObjectMetadata.data?.createOneObject?.id)) {
await refreshCoreViewsByObjectMetadataId(
createdObjectMetadata.data.createOneObject.id,
);
}
return {
status: 'successful',
response: createdObjectMetadata,
@@ -3,7 +3,6 @@ import { DeleteOneFieldMetadataItemDocument } from '~/generated-metadata/graphql
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState';
import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState';
@@ -11,7 +10,6 @@ import { AggregateOperations } from '@/object-record/record-table/constants/Aggr
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
@@ -21,11 +19,6 @@ export const useDeleteOneFieldMetadataItem = () => {
DeleteOneFieldMetadataItemDocument,
);
const { refreshObjectMetadataItems } =
useRefreshObjectMetadataItems('network-only');
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
@@ -56,10 +49,8 @@ export const useDeleteOneFieldMetadataItem = () => {
const deleteOneFieldMetadataItem = async ({
idToDelete,
objectMetadataId,
}: {
idToDelete: string;
objectMetadataId: string;
}): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof deleteOneFieldMetadataItemMutation>>
@@ -75,9 +66,6 @@ export const useDeleteOneFieldMetadataItem = () => {
// TODO: see if we can remove this lin altogether
await resetRecordIndexKanbanAggregateOperation(idToDelete);
await refreshObjectMetadataItems();
await refreshCoreViewsByObjectMetadataId(objectMetadataId);
return {
status: 'successful',
response,
@@ -2,10 +2,8 @@ import { useMutation } from '@apollo/client/react';
import { DeleteOneObjectMetadataItemDocument } from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
@@ -15,11 +13,6 @@ export const useDeleteOneObjectMetadataItem = () => {
DeleteOneObjectMetadataItemDocument,
);
const { refreshObjectMetadataItems } =
useRefreshObjectMetadataItems('network-only');
const { refreshAllCoreViews } = useRefreshAllCoreViews();
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
@@ -37,9 +30,6 @@ export const useDeleteOneObjectMetadataItem = () => {
},
});
await refreshObjectMetadataItems();
await refreshAllCoreViews();
return {
status: 'successful',
response,
@@ -1,48 +0,0 @@
import { useQuery } from '@apollo/client/react';
import { useMemo } from 'react';
import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError';
import {
type ObjectMetadataItemsQuery,
type ObjectMetadataItemsQueryVariables,
} from '~/generated-metadata/graphql';
import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
export const useFindManyObjectMetadataItems = ({
skip,
}: {
skip?: boolean;
} = {}) => {
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { data, loading, error, refetch } = useQuery<
ObjectMetadataItemsQuery,
ObjectMetadataItemsQueryVariables
>(FIND_MANY_OBJECT_METADATA_ITEMS, {
skip,
});
useSnackBarOnQueryError(error);
const objectMetadataItems = useMemo(() => {
const objectMetadataItemsArray =
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: data,
});
return enrichObjectMetadataItemsWithPermissions({
objectMetadataItems: objectMetadataItemsArray,
objectPermissionsByObjectMetadataId,
});
}, [data, objectPermissionsByObjectMetadataId]);
return {
objectMetadataItems,
loading,
error,
refetch,
};
};
@@ -1,11 +1,8 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useLoadMockedObjectMetadataItems = () => {
const store = useStore();
const { updateDraft, applyChanges } = useMetadataStore();
const loadMockedObjectMetadataItems = useCallback(async () => {
@@ -20,11 +17,7 @@ export const useLoadMockedObjectMetadataItems = () => {
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
applyChanges();
if (store.get(isAppEffectRedirectEnabledState.atom) === false) {
store.set(isAppEffectRedirectEnabledState.atom, true);
}
}, [store, updateDraft, applyChanges]);
}, [updateDraft, applyChanges]);
return {
loadMockedObjectMetadataItems,
@@ -1,48 +0,0 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { type FetchPolicy } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { type ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
export const useRefreshObjectMetadataItems = (
fetchPolicy: FetchPolicy = 'network-only',
) => {
const store = useStore();
const client = useApolloClient();
const { updateDraft, applyChanges } = useMetadataStore();
const refreshObjectMetadataItems = useCallback(async () => {
const objectMetadataItemsResult =
await client.query<ObjectMetadataItemsQuery>({
query: FIND_MANY_OBJECT_METADATA_ITEMS,
variables: {},
fetchPolicy,
});
const compositeObjects =
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: objectMetadataItemsResult.data,
});
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(compositeObjects);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
applyChanges();
if (store.get(isAppEffectRedirectEnabledState.atom) === false) {
store.set(isAppEffectRedirectEnabledState.atom, true);
}
}, [client, fetchPolicy, store, updateDraft, applyChanges]);
return {
refreshObjectMetadataItems,
};
};
@@ -5,11 +5,9 @@ import {
} from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { lastFieldMetadataItemUpdateState } from '@/object-metadata/states/lastFieldMetadataItemUpdateState';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
@@ -17,12 +15,6 @@ import { CrudOperationType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
export const useUpdateOneFieldMetadataItem = () => {
const { refreshObjectMetadataItems } =
useRefreshObjectMetadataItems('network-only');
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const [updateOneFieldMetadataItemMutation] = useMutation(
UpdateOneFieldMetadataItemDocument,
);
@@ -66,9 +58,6 @@ export const useUpdateOneFieldMetadataItem = () => {
},
});
await refreshObjectMetadataItems();
await refreshCoreViewsByObjectMetadataId(objectMetadataId);
setLastFieldMetadataItemUpdate({
fieldMetadataItemId: fieldMetadataIdToUpdate,
objectMetadataId,
@@ -5,10 +5,8 @@ import {
} from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
@@ -19,12 +17,6 @@ export const useUpdateOneObjectMetadataItem = () => {
UpdateOneObjectMetadataItemDocument,
);
const { refreshObjectMetadataItems } =
useRefreshObjectMetadataItems('network-only');
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
@@ -47,9 +39,6 @@ export const useUpdateOneObjectMetadataItem = () => {
},
});
await refreshObjectMetadataItems();
await refreshCoreViewsByObjectMetadataId(idToUpdate);
return {
status: 'successful',
response,
@@ -1,10 +1,10 @@
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { flatObjectMetadataItemsSelector } from '@/object-metadata/states/flatObjectMetadataItemsSelector';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const activeObjectNameSingularsSelector = createAtomSelector<string[]>({
key: 'activeObjectNameSingularsSelector',
get: ({ get }) => {
const flatObjects = get(objectMetadataItemsSelector);
const flatObjects = get(flatObjectMetadataItemsSelector);
return flatObjects
.filter((flatObject) => flatObject.isActive)
@@ -2,10 +2,10 @@ import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const objectMetadataItemsSelector = createAtomSelector<
export const flatObjectMetadataItemsSelector = createAtomSelector<
FlatObjectMetadataItem[]
>({
key: 'objectMetadataItemsSelector',
key: 'flatObjectMetadataItemsSelector',
get: ({ get }) => {
const storeItem = get(metadataStoreState, 'objectMetadataItems');
@@ -1,4 +1,4 @@
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { flatObjectMetadataItemsSelector } from '@/object-metadata/states/flatObjectMetadataItemsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
export const isSystemObjectByNameSingularFamilySelector =
@@ -7,7 +7,7 @@ export const isSystemObjectByNameSingularFamilySelector =
get:
(nameSingular: string) =>
({ get }) => {
const flatObjects = get(objectMetadataItemsSelector);
const flatObjects = get(flatObjectMetadataItemsSelector);
return (
flatObjects.find(
@@ -1,7 +1,7 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { fieldMetadataItemsSelector } from '@/metadata-store/states/fieldMetadataItemsSelector';
import { indexMetadataItemsSelector } from '@/metadata-store/states/indexMetadataItemsSelector';
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { flatObjectMetadataItemsSelector } from '@/object-metadata/states/flatObjectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getNonReadableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions';
import { getNonUpdatableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonUpdatableFieldMetadataIdsFromObjectPermissions';
@@ -15,7 +15,7 @@ export const objectMetadataItemsWithFieldsSelector = createAtomSelector<
>({
key: 'objectMetadataItemsWithFieldsSelector',
get: ({ get }) => {
const flatObjects = get(objectMetadataItemsSelector);
const flatObjects = get(flatObjectMetadataItemsSelector);
const allFlatFields = get(fieldMetadataItemsSelector);
const allFlatIndexes = get(indexMetadataItemsSelector);
const currentUserWorkspace = get(currentUserWorkspaceState);
@@ -5,11 +5,10 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { convertViewTypeToCore } from '@/views/utils/convertViewTypeToCore';
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
@@ -39,7 +38,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
}),
);
const existingCoreViews = store.get(coreViewsState.atom);
const existingCoreViews = store.get(coreViewsSelector.atom);
if (!isDefined(currentViewId)) {
throw new Error('No view id found');
@@ -74,16 +73,6 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
}
setRecordIndexViewType(viewType);
store.set(coreViewsState.atom, [
...existingCoreViews.filter(
(coreView) => coreView.id !== currentView.id,
),
{
...currentCoreView,
type: convertViewTypeToCore(viewType),
mainGroupByFieldMetadataId,
},
]);
await updateCurrentView(updateCurrentViewParams);
return;
}
@@ -95,16 +84,6 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
updateCurrentViewParams.mainGroupByFieldMetadataId = null;
await updateCurrentView(updateCurrentViewParams);
setRecordIndexViewType(viewType);
store.set(coreViewsState.atom, [
...existingCoreViews.filter(
(coreView) => coreView.id !== currentView.id,
),
{
...currentCoreView,
mainGroupByFieldMetadataId: null,
type: convertViewTypeToCore(viewType),
},
]);
return;
}
case ViewType.Calendar: {
@@ -115,18 +94,6 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
const calendarFieldMetadataId = availableFieldsForCalendar[0].id;
setRecordIndexViewType(viewType);
store.set(coreViewsState.atom, [
...existingCoreViews.filter(
(coreView) => coreView.id !== currentView.id,
),
{
...currentCoreView,
mainGroupByFieldMetadataId: null,
type: convertViewTypeToCore(viewType),
calendarLayout: ViewCalendarLayout.MONTH,
calendarFieldMetadataId,
},
]);
loadRecordIndexStates(
{
@@ -4,12 +4,12 @@ import { useEffect, useMemo, useState } from 'react';
import { type TaskGroups } from '@/activities/tasks/components/TaskGroups';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
@@ -18,9 +18,8 @@ import { RecordCalendarMonth } from '@/object-record/record-calendar/month/compo
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { ViewBarFilterDropdownIds } from '@/views/constants/ViewBarFilterDropdownIds';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { coreViewsState } from '@/views/states/coreViewState';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
@@ -28,6 +27,7 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { mockedCoreViews } from '~/testing/mock-data/generated/metadata/views/mock-views-data';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { setTestCoreViewsInMetadataStore } from '~/testing/utils/setTestCoreViewsInMetadataStore';
const meta: Meta<typeof RecordCalendarMonth> = {
title: 'Modules/ObjectRecord/RecordCalendar/Month',
@@ -44,8 +44,6 @@ const meta: Meta<typeof RecordCalendarMonth> = {
instanceId,
);
const setCoreViews = useSetAtomState(coreViewsState);
const mockCoreView = mockedCoreViews.find(
(v) => v.name === 'All Companies',
)!;
@@ -73,12 +71,11 @@ const meta: Meta<typeof RecordCalendarMonth> = {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setCoreViews([mockCoreView]);
setTestCoreViewsInMetadataStore(jotaiStore, [mockCoreView]);
setContextStoreCurrentViewId(mockCoreView.id);
setCurrentRecordFields(columns);
setIsLoaded(true);
}, [
setCoreViews,
setContextStoreCurrentViewId,
setCurrentRecordFields,
mockCoreView,
@@ -6,7 +6,6 @@ import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/use
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { usePerformViewAPIUpdate } from '@/views/hooks/internal/usePerformViewAPIUpdate';
import { useGetViewFromState } from '@/views/hooks/useGetViewFromState';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { useStore } from 'jotai';
@@ -29,8 +28,6 @@ export const useHandleRecordGroupField = () => {
const { performViewAPIUpdate } = usePerformViewAPIUpdate();
const { loadRecordIndexStates } = useLoadRecordIndexStates();
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const store = useStore();
@@ -121,8 +118,6 @@ export const useHandleRecordGroupField = () => {
viewGroups: newViewGroupsList,
objectMetadataItem,
});
await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id);
},
[
currentViewIdCallbackState,
@@ -130,7 +125,6 @@ export const useHandleRecordGroupField = () => {
performViewAPIUpdate,
setRecordGroupsFromViewGroups,
objectMetadataItem,
refreshCoreViewsByObjectMetadataId,
loadRecordIndexStates,
store,
],
@@ -1,4 +1,3 @@
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
@@ -6,7 +5,6 @@ import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -20,10 +18,6 @@ export const useViewFieldAggregateOperation = () => {
(viewField) => viewField.fieldMetadataId === fieldMetadataId,
);
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
const updateViewFieldAggregateOperation = async (
aggregateOperation: ExtendedAggregateOperations | null,
@@ -48,8 +42,6 @@ export const useViewFieldAggregateOperation = () => {
},
},
]);
refreshCoreViewsByObjectMetadataId(objectMetadataItem.id);
};
const viewFieldAggregateOperation = useAtomFamilyStateValue(
@@ -1,10 +1,10 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { currentUserState } from '@/auth/states/currentUserState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type OnboardingStatus } from '~/generated-metadata/graphql';
export const useOnboardingStatus = (): OnboardingStatus | null | undefined => {
const currentUser = useAtomStateValue(currentUserState);
const isLoggedIn = useIsLogged();
return isLoggedIn ? currentUser?.onboardingStatus : undefined;
const hasAccessTokenPair = useHasAccessTokenPair();
return hasAccessTokenPair ? currentUser?.onboardingStatus : undefined;
};
@@ -1,51 +0,0 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitPageLayoutWithRelated } from '@/metadata-store/utils/splitPageLayoutWithRelated';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { recordPageLayoutsState } from '@/page-layout/states/recordPageLayoutsState';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { type PageLayout as PageLayoutGenerated } from '~/generated-metadata/graphql';
export const useApplyPageLayouts = () => {
const store = useStore();
const { updateDraft, applyChanges } = useMetadataStore();
const applyPageLayouts = useCallback(
(pageLayouts: PageLayoutGenerated[]) => {
const transformedPageLayouts = pageLayouts.map(transformPageLayout);
for (const pageLayout of transformedPageLayouts) {
store.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: pageLayout.id,
}),
pageLayout,
);
store.set(
pageLayoutCurrentLayoutsComponentState.atomFamily({
instanceId: pageLayout.id,
}),
convertPageLayoutToTabLayouts(pageLayout),
);
}
store.set(recordPageLayoutsState.atom, transformedPageLayouts);
const { flatPageLayouts, flatPageLayoutTabs, flatPageLayoutWidgets } =
splitPageLayoutWithRelated(transformedPageLayouts);
updateDraft('pageLayouts', flatPageLayouts);
updateDraft('pageLayoutTabs', flatPageLayoutTabs);
updateDraft('pageLayoutWidgets', flatPageLayoutWidgets);
applyChanges();
},
[store, updateDraft, applyChanges],
);
return {
applyPageLayouts,
};
};
@@ -1,28 +0,0 @@
import { useApplyPageLayouts } from '@/page-layout/hooks/useApplyPageLayouts';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useApolloClient } from '@apollo/client/react';
import { FindAllPageLayoutsDocument } from '~/generated-metadata/graphql';
export const useRefreshPageLayouts = () => {
const client = useApolloClient();
const { applyPageLayouts } = useApplyPageLayouts();
const refreshPageLayouts = useCallback(async () => {
const result = await client.query({
query: FindAllPageLayoutsDocument,
fetchPolicy: 'network-only',
});
if (!isDefined(result.data?.getPageLayouts)) {
return;
}
applyPageLayouts(result.data.getPageLayouts);
}, [client, applyPageLayouts]);
return {
refreshPageLayouts,
};
};
@@ -6,7 +6,6 @@ import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews';
import { useMutation } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback } from 'react';
@@ -82,8 +81,6 @@ export const useSaveFieldsWidgetGroups = ({
{ input: UpsertFieldsWidgetInput }
>(UPSERT_FIELDS_WIDGET);
const { refreshAllCoreViews } = useRefreshAllCoreViews();
const store = useStore();
const saveFieldsWidgetGroups = useCallback(async () => {
@@ -165,8 +162,6 @@ export const useSaveFieldsWidgetGroups = ({
);
store.set(fieldsWidgetEditorModePersistedState, allEditorModes);
await refreshAllCoreViews();
return { status: 'successful' as const };
}, [
fieldsWidgetGroupsDraftState,
@@ -176,7 +171,6 @@ export const useSaveFieldsWidgetGroups = ({
fieldsWidgetEditorModeDraftState,
fieldsWidgetEditorModePersistedState,
upsertFieldsWidgetMutation,
refreshAllCoreViews,
store,
]);
@@ -1,4 +1,4 @@
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
@@ -37,7 +37,7 @@ const meta: Meta<typeof DashboardWidgetPlaceholder> = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -8,7 +8,7 @@ import { CatalogDecorator, type CatalogStory } from 'twenty-ui/testing';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { CoreObjectNameSingular } from 'twenty-shared/types';
@@ -281,7 +281,7 @@ export const WithNumberChart: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -368,7 +368,7 @@ export const WithGaugeChart: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -459,7 +459,7 @@ export const WithBarChart: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -553,7 +553,7 @@ export const SmallWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -651,7 +651,7 @@ export const MediumWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -749,7 +749,7 @@ export const LargeWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -843,7 +843,7 @@ export const WideWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -941,7 +941,7 @@ export const TallWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -1035,7 +1035,7 @@ export const WithManyToOneRelationFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1154,7 +1154,7 @@ export const WithOneToManyRelationFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1265,7 +1265,7 @@ export const OneToManyRelationFieldWidgetWithSeeAllButton: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1403,7 +1403,7 @@ export const OnMobile: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1501,7 +1501,7 @@ export const InSidePanel: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1660,7 +1660,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
if (state === 'hover') {
jotaiStore.set(
@@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router-dom';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@@ -360,7 +360,7 @@ export const TextFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -454,7 +454,7 @@ export const AddressFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -551,7 +551,7 @@ export const NumberFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -645,7 +645,7 @@ export const LinkFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -739,7 +739,7 @@ export const ManyToOneRelationFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -843,7 +843,7 @@ export const OneToManyRelationFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -939,7 +939,7 @@ export const BooleanFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1032,7 +1032,7 @@ export const CurrencyFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1125,7 +1125,7 @@ export const EmailsFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
personObjectMetadataItem.id,
@@ -1219,7 +1219,7 @@ export const PhonesFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
personObjectMetadataItem.id,
@@ -1313,7 +1313,7 @@ export const SelectFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
opportunityObjectMetadataItem.id,
@@ -1408,7 +1408,7 @@ export const MultiSelectFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1506,7 +1506,7 @@ export const TimelineActivityRelationFieldWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
timelineActivityObjectMetadataItem.id,
@@ -1605,7 +1605,7 @@ export const ManyToOneRelationCardWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1717,7 +1717,7 @@ export const OneToManyRelationCardWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1812,7 +1812,7 @@ export const TimelineActivityRelationCardWidget: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
timelineActivityObjectMetadataItem.id,
@@ -1973,7 +1973,7 @@ export const OneToManyRelationCardWidgetWithProgressiveLoading: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -4,10 +4,8 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
import { MemoryRouter } from 'react-router-dom';
import { expect, waitFor, within } from 'storybook/test';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
@@ -23,8 +21,8 @@ import { FieldsWidget } from '@/page-layout/widgets/fields/components/FieldsWidg
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { ComponentDecorator } from 'twenty-ui/testing';
import {
ViewOpenRecordIn as CoreViewOpenRecordIn,
@@ -41,6 +39,8 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
import { setTestCoreViewsInMetadataStore } from '~/testing/utils/setTestCoreViewsInMetadataStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
const companyObjectMetadataItem = getMockObjectMetadataItemOrThrow(
CoreObjectNameSingular.Company,
@@ -206,6 +206,39 @@ const createCoreView = (
...overrides,
});
const createViewField = (
id: string,
fieldMetadataId: string,
position: number,
viewFieldGroupId?: string,
) => ({
id,
fieldMetadataId,
position,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
...(viewFieldGroupId !== undefined && { viewFieldGroupId }),
});
const createViewFieldGroup = (
id: string,
name: string,
position: number,
viewFields: ReturnType<typeof createViewField>[],
isVisible = true,
) => ({
id,
name,
position,
isVisible,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
viewFields,
});
const meta: Meta<typeof FieldsWidget> = {
title: 'Modules/PageLayout/Widgets/FieldsWidget',
component: FieldsWidget,
@@ -229,88 +262,38 @@ type Story = StoryObj<typeof FieldsWidget>;
export const WithViewFieldGroups: Story = {
render: () => {
const contactInfoFields = [
createViewField('vf-name', nameField.id, 0, 'group-contact-info'),
createViewField('vf-address', addressField.id, 1, 'group-contact-info'),
createViewField('vf-linkedin', linkedinField.id, 2, 'group-contact-info'),
];
const businessFields = [
createViewField('vf-employees', employeesField.id, 0, 'group-business'),
createViewField(
'vf-arr',
annualRecurringRevenueField.id,
1,
'group-business',
),
createViewField(
'vf-icp',
idealCustomerProfileField.id,
2,
'group-business',
),
];
const coreView = createCoreView({
viewFields: [...contactInfoFields, ...businessFields],
viewFieldGroups: [
{
id: 'group-contact-info',
name: 'Contact Info',
position: 0,
isVisible: true,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
viewFields: [
{
id: 'vf-name',
fieldMetadataId: nameField.id,
position: 0,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-address',
fieldMetadataId: addressField.id,
position: 1,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-linkedin',
fieldMetadataId: linkedinField.id,
position: 2,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
},
],
},
{
id: 'group-business',
name: 'Business',
position: 1,
isVisible: true,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
viewFields: [
{
id: 'vf-employees',
fieldMetadataId: employeesField.id,
position: 0,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-arr',
fieldMetadataId: annualRecurringRevenueField.id,
position: 1,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-icp',
fieldMetadataId: idealCustomerProfileField.id,
position: 2,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
},
],
},
createViewFieldGroup(
'group-contact-info',
'Contact Info',
0,
contactInfoFields,
),
createViewFieldGroup('group-business', 'Business', 1, businessFields),
],
});
@@ -325,8 +308,8 @@ export const WithViewFieldGroups: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(coreViewsState.atom, [coreView]);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
setTestCoreViewsInMetadataStore(jotaiStore, [coreView]);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -394,36 +377,9 @@ export const WithInlineViewFields: Story = {
render: () => {
const coreView = createCoreView({
viewFields: [
{
id: 'vf-name',
fieldMetadataId: nameField.id,
position: 0,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-employees',
fieldMetadataId: employeesField.id,
position: 1,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-address',
fieldMetadataId: addressField.id,
position: 2,
isVisible: true,
size: 200,
aggregateOperation: null,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
},
createViewField('vf-name', nameField.id, 0),
createViewField('vf-employees', employeesField.id, 1),
createViewField('vf-address', addressField.id, 2),
],
});
@@ -438,8 +394,8 @@ export const WithInlineViewFields: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(coreViewsState.atom, [coreView]);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
setTestCoreViewsInMetadataStore(jotaiStore, [coreView]);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -507,15 +463,7 @@ export const Empty: Story = {
render: () => {
const coreView = createCoreView({
viewFieldGroups: [
{
id: 'group-empty',
name: 'Empty Group',
position: 0,
isVisible: false,
isOverridden: false,
viewId: FIELDS_VIEW_ID,
viewFields: [],
},
createViewFieldGroup('group-empty', 'Empty Group', 0, [], false),
],
});
@@ -530,8 +478,8 @@ export const Empty: Story = {
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(coreViewsState.atom, [coreView]);
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
setTestCoreViewsInMetadataStore(jotaiStore, [coreView]);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -1,4 +1,4 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { type ReactNode } from 'react';
@@ -21,13 +21,13 @@ export const SettingsProtectedRouteWrapper = ({
settingsPermission,
requiredFeatureFlag,
}: SettingsProtectedRouteWrapperProps) => {
const isLoggedIn = useIsLogged();
const hasAccessTokenPair = useHasAccessTokenPair();
const hasPermission = useHasPermissionFlag(settingsPermission);
const requiredFeatureFlagEnabled = useIsFeatureEnabled(
requiredFeatureFlag || null,
);
if (!isLoggedIn) {
if (!hasAccessTokenPair) {
return null;
}
@@ -262,7 +262,6 @@ export const SettingsObjectFieldItemTableRow = ({
onDelete={() =>
deleteOneFieldMetadataItem({
idToDelete: fieldMetadataItem.id,
objectMetadataId: objectMetadataItem.id,
})
}
/>
@@ -242,7 +242,6 @@ export const SettingsObjectRelationItemTableRow = ({
onDelete={() =>
deleteOneFieldMetadataItem({
idToDelete: fieldMetadataItem.id,
objectMetadataId: objectMetadataItem.id,
})
}
/>
@@ -1,4 +1,3 @@
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsRoleAssignment } from '@/settings/roles/role-assignment/components/SettingsRoleAssignment';
@@ -35,7 +34,6 @@ type SettingsRoleProps = {
};
export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID + '-' + roleId,
@@ -126,7 +124,6 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
try {
await saveDraftRoleToDB();
await loadCurrentUser();
await refreshObjectMetadataItems();
} finally {
setIsSaving(false);
}
@@ -93,6 +93,7 @@ export const SignInBackgroundMockContainer = () => {
<ViewBar
viewBarId={viewBarId}
optionsDropdownButton={<></>}
isReadOnly
/>
<RecordTableWithWrappers
@@ -1,4 +1,4 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useHandleSseClientConnectionRetry } from '@/sse-db-event/hooks/useHandleSseClientConnectionRetry';
import { activeQueryListenersState } from '@/sse-db-event/states/activeQueryListenersState';
@@ -14,7 +14,7 @@ import { useStore } from 'jotai';
export const SSEClientEffect = () => {
const store = useStore();
const isLoggedIn = useIsLogged();
const hasAccessTokenPair = useHasAccessTokenPair();
const [sseClient, setSseClient] = useAtomState(sseClientState);
const tokenPair = useAtomStateValue(tokenPairState);
@@ -32,7 +32,7 @@ export const SSEClientEffect = () => {
useHandleSseClientConnectionRetry();
useEffect(() => {
if (isLoggedIn && !isDefined(sseClient) && isDefined(tokenPair)) {
if (hasAccessTokenPair && !isDefined(sseClient) && isDefined(tokenPair)) {
const token = tokenPair?.accessOrWorkspaceAgnosticToken?.token;
const newSseClient = createClient({
@@ -52,7 +52,7 @@ export const SSEClientEffect = () => {
}
}, [
handleSSEClientConnected,
isLoggedIn,
hasAccessTokenPair,
setSseClient,
sseClient,
tokenPair,
@@ -1,4 +1,4 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { currentUserState } from '@/auth/states/currentUserState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useTriggerEventStreamCreation } from '@/sse-db-event/hooks/useTriggerEventStreamCreation';
@@ -27,7 +27,7 @@ export const SSEEventStreamEffect = () => {
isDestroyingEventStreamState,
);
const isLoggedIn = useIsLogged();
const hasAccessTokenPair = useHasAccessTokenPair();
const currentUser = useAtomStateValue(currentUserState);
const { triggerEventStreamCreation } = useTriggerEventStreamCreation();
@@ -39,7 +39,7 @@ export const SSEEventStreamEffect = () => {
const willCreateEventStream =
isSseClientAvailable &&
isLoggedIn &&
hasAccessTokenPair &&
isDefined(currentUser) &&
currentUser.onboardingStatus === OnboardingStatus.COMPLETED &&
!shouldDestroyEventStream &&
@@ -59,7 +59,7 @@ export const SSEEventStreamEffect = () => {
}, [
isCreatingSseEventStream,
triggerEventStreamCreation,
isLoggedIn,
hasAccessTokenPair,
currentUser,
isDestroyingEventStream,
triggerEventStreamDestroy,
@@ -1,14 +1,4 @@
import { FieldMetadataSSEEffect } from '@/metadata-store/effect-components/FieldMetadataSSEEffect';
import { NavigationMenuItemSSEEffect } from '@/metadata-store/effect-components/NavigationMenuItemSSEEffect';
import { ObjectMetadataItemSSEEffect } from '@/metadata-store/effect-components/ObjectMetadataItemSSEEffect';
import { PageLayoutSSEEffect } from '@/metadata-store/effect-components/PageLayoutSSEEffect';
import { PageLayoutTabSSEEffect } from '@/metadata-store/effect-components/PageLayoutTabSSEEffect';
import { PageLayoutWidgetSSEEffect } from '@/metadata-store/effect-components/PageLayoutWidgetSSEEffect';
import { ViewFieldSSEEffect } from '@/metadata-store/effect-components/ViewFieldSSEEffect';
import { ViewFilterGroupSSEEffect } from '@/metadata-store/effect-components/ViewFilterGroupSSEEffect';
import { ViewFilterSSEEffect } from '@/metadata-store/effect-components/ViewFilterSSEEffect';
import { ViewSortSSEEffect } from '@/metadata-store/effect-components/ViewSortSSEEffect';
import { ViewSSEEffect } from '@/metadata-store/effect-components/ViewSSEEffect';
import { MetadataStoreSSEEffect } from '@/metadata-store/effect-components/MetadataStoreSSEEffect';
import { SSEClientEffect } from '@/sse-db-event/components/SSEClientEffect';
import { SSEEventStreamEffect } from '@/sse-db-event/components/SSEEventStreamEffect';
import { SSEQuerySubscribeEffect } from '@/sse-db-event/components/SSEQuerySubscribeEffect';
@@ -24,17 +14,7 @@ export const SSEProvider = ({ children }: SSEProviderProps) => {
<SSEClientEffect />
<SSEEventStreamEffect />
<SSEQuerySubscribeEffect />
<NavigationMenuItemSSEEffect />
<FieldMetadataSSEEffect />
<ObjectMetadataItemSSEEffect />
<PageLayoutSSEEffect />
<PageLayoutTabSSEEffect />
<PageLayoutWidgetSSEEffect />
<ViewSSEEffect />
<ViewFieldSSEEffect />
<ViewFilterSSEEffect />
<ViewSortSSEEffect />
<ViewFilterGroupSSEEffect />
<MetadataStoreSSEEffect />
{children}
</>
);

Some files were not shown because too many files have changed in this diff Show More