Introduce standalone page (#19675)
Add support for standalone pages: a new `PageLayout` type (`STANDALONE_PAGE`) that can be rendered independently at `/page/:pageLayoutId`, not tied to any record or object context. - New `STANDALONE_PAGE` page layout type - New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId` foreign key to `NavigationMenuItemEntity`, allowing sidebar items to link directly to standalone pages - New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates object-context-dependent commands (Create Record, Import, Export, See Deleted, Create View, Hide Deleted) from truly global ones, so standalone pages only show relevant commands - Frontend routing & rendering: adds a `/page/:pageLayoutId` route with its own page component, header, and command menu - Widget rendering refactor - Instance commands: two fast 1.22 migrations: `pageLayoutId` column + `STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type enum - Workspace command: backfills existing command menu items from `GLOBAL` to `GLOBAL_OBJECT_CONTEXT` where appropriate - Dev seeds: adds a sample "Star History" standalone page with an iframe widget for local development
This commit is contained in:
@@ -1257,6 +1257,7 @@ enum PageLayoutType {
|
||||
RECORD_INDEX
|
||||
RECORD_PAGE
|
||||
DASHBOARD
|
||||
STANDALONE_PAGE
|
||||
}
|
||||
|
||||
type Analytics {
|
||||
@@ -1485,6 +1486,7 @@ type NavigationMenuItem {
|
||||
icon: String
|
||||
color: String
|
||||
folderId: UUID
|
||||
pageLayoutId: UUID
|
||||
position: Float!
|
||||
applicationId: UUID
|
||||
createdAt: DateTime!
|
||||
@@ -1498,6 +1500,7 @@ enum NavigationMenuItemType {
|
||||
LINK
|
||||
OBJECT
|
||||
RECORD
|
||||
PAGE_LAYOUT
|
||||
}
|
||||
|
||||
type ObjectRecordEventProperties {
|
||||
@@ -2717,6 +2720,7 @@ enum EngineComponentKey {
|
||||
|
||||
enum CommandMenuItemAvailabilityType {
|
||||
GLOBAL
|
||||
GLOBAL_OBJECT_CONTEXT
|
||||
RECORD_SELECTION
|
||||
FALLBACK
|
||||
}
|
||||
@@ -3740,6 +3744,7 @@ input CreateNavigationMenuItemInput {
|
||||
icon: String
|
||||
color: String
|
||||
folderId: UUID
|
||||
pageLayoutId: UUID
|
||||
position: Float
|
||||
}
|
||||
|
||||
@@ -3758,6 +3763,7 @@ input UpdateNavigationMenuItemInput {
|
||||
link: String
|
||||
icon: String
|
||||
color: String
|
||||
pageLayoutId: UUID
|
||||
}
|
||||
|
||||
"""The `Upload` scalar type represents a file upload."""
|
||||
|
||||
@@ -981,7 +981,7 @@ export interface PageLayout {
|
||||
__typename: 'PageLayout'
|
||||
}
|
||||
|
||||
export type PageLayoutType = 'RECORD_INDEX' | 'RECORD_PAGE' | 'DASHBOARD'
|
||||
export type PageLayoutType = 'RECORD_INDEX' | 'RECORD_PAGE' | 'DASHBOARD' | 'STANDALONE_PAGE'
|
||||
|
||||
export interface Analytics {
|
||||
/** Boolean that confirms query was dispatched */
|
||||
@@ -1210,6 +1210,7 @@ export interface NavigationMenuItem {
|
||||
icon?: Scalars['String']
|
||||
color?: Scalars['String']
|
||||
folderId?: Scalars['UUID']
|
||||
pageLayoutId?: Scalars['UUID']
|
||||
position: Scalars['Float']
|
||||
applicationId?: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
@@ -1218,7 +1219,7 @@ export interface NavigationMenuItem {
|
||||
__typename: 'NavigationMenuItem'
|
||||
}
|
||||
|
||||
export type NavigationMenuItemType = 'VIEW' | 'FOLDER' | 'LINK' | 'OBJECT' | 'RECORD'
|
||||
export type NavigationMenuItemType = 'VIEW' | 'FOLDER' | 'LINK' | 'OBJECT' | 'RECORD' | 'PAGE_LAYOUT'
|
||||
|
||||
export interface ObjectRecordEventProperties {
|
||||
updatedFields?: Scalars['String'][]
|
||||
@@ -2368,7 +2369,7 @@ export interface CommandMenuItem {
|
||||
|
||||
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
|
||||
|
||||
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'RECORD_SELECTION' | 'FALLBACK'
|
||||
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'GLOBAL_OBJECT_CONTEXT' | 'RECORD_SELECTION' | 'FALLBACK'
|
||||
|
||||
export type CommandMenuItemPayload = (PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload) & { __isUnion?: true }
|
||||
|
||||
@@ -4459,6 +4460,7 @@ export interface NavigationMenuItemGenqlSelection{
|
||||
icon?: boolean | number
|
||||
color?: boolean | number
|
||||
folderId?: boolean | number
|
||||
pageLayoutId?: boolean | number
|
||||
position?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
@@ -6567,7 +6569,7 @@ export interface AddQuerySubscriptionInput {eventStreamId: Scalars['String'],que
|
||||
|
||||
export interface RemoveQueryFromEventStreamInput {eventStreamId: Scalars['String'],queryId: Scalars['String']}
|
||||
|
||||
export interface CreateNavigationMenuItemInput {id?: (Scalars['UUID'] | null),userWorkspaceId?: (Scalars['UUID'] | null),targetRecordId?: (Scalars['UUID'] | null),targetObjectMetadataId?: (Scalars['UUID'] | null),viewId?: (Scalars['UUID'] | null),type: NavigationMenuItemType,name?: (Scalars['String'] | null),link?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),color?: (Scalars['String'] | null),folderId?: (Scalars['UUID'] | null),position?: (Scalars['Float'] | null)}
|
||||
export interface CreateNavigationMenuItemInput {id?: (Scalars['UUID'] | null),userWorkspaceId?: (Scalars['UUID'] | null),targetRecordId?: (Scalars['UUID'] | null),targetObjectMetadataId?: (Scalars['UUID'] | null),viewId?: (Scalars['UUID'] | null),type: NavigationMenuItemType,name?: (Scalars['String'] | null),link?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),color?: (Scalars['String'] | null),folderId?: (Scalars['UUID'] | null),pageLayoutId?: (Scalars['UUID'] | null),position?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpdateOneNavigationMenuItemInput {
|
||||
/** The id of the record to update */
|
||||
@@ -6575,7 +6577,7 @@ id: Scalars['UUID'],
|
||||
/** The record to update */
|
||||
update: UpdateNavigationMenuItemInput}
|
||||
|
||||
export interface UpdateNavigationMenuItemInput {folderId?: (Scalars['UUID'] | null),position?: (Scalars['Float'] | null),name?: (Scalars['String'] | null),link?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),color?: (Scalars['String'] | null)}
|
||||
export interface UpdateNavigationMenuItemInput {folderId?: (Scalars['UUID'] | null),position?: (Scalars['Float'] | null),name?: (Scalars['String'] | null),link?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),color?: (Scalars['String'] | null),pageLayoutId?: (Scalars['UUID'] | null)}
|
||||
|
||||
export interface CreateViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null),viewId: Scalars['UUID']}
|
||||
|
||||
@@ -9405,7 +9407,8 @@ export const enumFieldDisplayMode = {
|
||||
export const enumPageLayoutType = {
|
||||
RECORD_INDEX: 'RECORD_INDEX' as const,
|
||||
RECORD_PAGE: 'RECORD_PAGE' as const,
|
||||
DASHBOARD: 'DASHBOARD' as const
|
||||
DASHBOARD: 'DASHBOARD' as const,
|
||||
STANDALONE_PAGE: 'STANDALONE_PAGE' as const
|
||||
}
|
||||
|
||||
export const enumBillingPlanKey = {
|
||||
@@ -9444,7 +9447,8 @@ export const enumNavigationMenuItemType = {
|
||||
FOLDER: 'FOLDER' as const,
|
||||
LINK: 'LINK' as const,
|
||||
OBJECT: 'OBJECT' as const,
|
||||
RECORD: 'RECORD' as const
|
||||
RECORD: 'RECORD' as const,
|
||||
PAGE_LAYOUT: 'PAGE_LAYOUT' as const
|
||||
}
|
||||
|
||||
export const enumMetadataEventAction = {
|
||||
@@ -9682,6 +9686,7 @@ export const enumEngineComponentKey = {
|
||||
|
||||
export const enumCommandMenuItemAvailabilityType = {
|
||||
GLOBAL: 'GLOBAL' as const,
|
||||
GLOBAL_OBJECT_CONTEXT: 'GLOBAL_OBJECT_CONTEXT' as const,
|
||||
RECORD_SELECTION: 'RECORD_SELECTION' as const,
|
||||
FALLBACK: 'FALLBACK' as const
|
||||
}
|
||||
|
||||
@@ -3056,6 +3056,9 @@ export default {
|
||||
"folderId": [
|
||||
3
|
||||
],
|
||||
"pageLayoutId": [
|
||||
3
|
||||
],
|
||||
"position": [
|
||||
11
|
||||
],
|
||||
@@ -9692,6 +9695,9 @@ export default {
|
||||
"folderId": [
|
||||
3
|
||||
],
|
||||
"pageLayoutId": [
|
||||
3
|
||||
],
|
||||
"position": [
|
||||
11
|
||||
],
|
||||
@@ -9729,6 +9735,9 @@ export default {
|
||||
"color": [
|
||||
1
|
||||
],
|
||||
"pageLayoutId": [
|
||||
3
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
+44
-6
@@ -2,12 +2,13 @@ import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
|
||||
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
|
||||
import { OnboardingStatus } from '~/generated-metadata/graphql';
|
||||
import { OnboardingStatus, PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||
@@ -58,11 +59,23 @@ const setupMockIsOnAWorkspace = (isOnAWorkspace: boolean) => {
|
||||
});
|
||||
};
|
||||
|
||||
jest.mock('@apollo/client/react');
|
||||
const setupMockUseQuery = (result?: { data?: unknown; loading?: boolean }) => {
|
||||
jest.mocked(useQuery).mockReturnValueOnce({
|
||||
data: result?.data ?? undefined,
|
||||
loading: result?.loading ?? false,
|
||||
} as ReturnType<typeof useQuery>);
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom');
|
||||
const setupMockUseParams = (objectNamePlural?: string) => {
|
||||
jest
|
||||
.mocked(useParams)
|
||||
.mockReturnValueOnce({ objectNamePlural: objectNamePlural ?? '' });
|
||||
const setupMockUseParams = (
|
||||
objectNamePlural?: string,
|
||||
pageLayoutId?: string,
|
||||
) => {
|
||||
jest.mocked(useParams).mockReturnValueOnce({
|
||||
objectNamePlural: objectNamePlural ?? '',
|
||||
pageLayoutId,
|
||||
});
|
||||
};
|
||||
|
||||
jest.mock('@/ui/utilities/state/jotai/hooks/useAtomStateValue');
|
||||
@@ -92,6 +105,8 @@ const testCases: {
|
||||
objectNamePluralFromMetadata?: string;
|
||||
verifyEmailRedirectPath?: string;
|
||||
returnToPath?: string;
|
||||
pageLayoutId?: string;
|
||||
useQueryResult?: { data?: unknown; loading?: boolean };
|
||||
}[] = [
|
||||
{ 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) },
|
||||
@@ -277,6 +292,20 @@ const testCases: {
|
||||
{ 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.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined, pageLayoutId: 'valid-id', useQueryResult: { loading: true } },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: AppPath.NotFound, pageLayoutId: 'non-existent-id', useQueryResult: { data: { getPageLayout: null }, loading: false } },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: AppPath.NotFound, pageLayoutId: 'wrong-type-id', useQueryResult: { data: { getPageLayout: { type: PageLayoutType.RECORD_PAGE } }, loading: false } },
|
||||
{ loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined, pageLayoutId: 'valid-standalone-id', useQueryResult: { data: { getPageLayout: { type: PageLayoutType.STANDALONE_PAGE } }, loading: false } },
|
||||
|
||||
{ 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 },
|
||||
@@ -350,6 +379,8 @@ describe('usePageChangeEffectNavigateLocation', () => {
|
||||
objectNamePluralFromMetadata,
|
||||
verifyEmailRedirectPath,
|
||||
returnToPath,
|
||||
pageLayoutId,
|
||||
useQueryResult,
|
||||
res,
|
||||
}) => {
|
||||
setupMockIsMatchingLocation(loc);
|
||||
@@ -357,7 +388,8 @@ describe('usePageChangeEffectNavigateLocation', () => {
|
||||
setupMockIsWorkspaceActivationStatusEqualsTo(isWorkspaceSuspended);
|
||||
setupMockHasAccessTokenPair(hasAccessTokenPair);
|
||||
setupMockIsOnAWorkspace(isOnAWorkspace ?? true);
|
||||
setupMockUseParams(objectNamePluralFromParams);
|
||||
setupMockUseQuery(useQueryResult);
|
||||
setupMockUseParams(objectNamePluralFromParams, pageLayoutId);
|
||||
setupMockState(
|
||||
objectNamePluralFromMetadata,
|
||||
verifyEmailRedirectPath,
|
||||
@@ -379,6 +411,12 @@ describe('usePageChangeEffectNavigateLocation', () => {
|
||||
['nonExistingObjectInParam', 'existingObjectInParam:false'].length +
|
||||
['caseWithRedirectionToVerifyEmailRedirectPath', 'caseWithout']
|
||||
.length +
|
||||
[
|
||||
'pageLayout:loading',
|
||||
'pageLayout:missing',
|
||||
'pageLayout:wrongType',
|
||||
'pageLayout:validStandalone',
|
||||
].length +
|
||||
['returnToPath:verify', 'returnToPath:signInUp', 'returnToPath:index']
|
||||
.length +
|
||||
['notOnWorkspace:verify', 'notOnWorkspace:signInUp'].length,
|
||||
|
||||
@@ -11,12 +11,17 @@ import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { isValidReturnToPath } from '@/auth/utils/isValidReturnToPath';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { OnboardingStatus } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
FindOnePageLayoutTypeDocument,
|
||||
OnboardingStatus,
|
||||
PageLayoutType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
const readReturnToPathFromUrlSearchParams = (): string | null => {
|
||||
@@ -39,11 +44,27 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
const someMatchingLocationOf = (appPaths: AppPath[]): boolean =>
|
||||
appPaths.some((appPath) => isMatchingLocation(location, appPath));
|
||||
|
||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
||||
const params = useParams();
|
||||
|
||||
const objectNamePlural = params.objectNamePlural ?? '';
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const objectMetadataItem = objectMetadataItems?.find(
|
||||
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
|
||||
);
|
||||
|
||||
const pageLayoutId = params.pageLayoutId;
|
||||
const isOnPageLayoutPage = isMatchingLocation(
|
||||
location,
|
||||
AppPath.PageLayoutPage,
|
||||
);
|
||||
|
||||
const { data: pageLayoutData, loading: isPageLayoutLoading } = useQuery(
|
||||
FindOnePageLayoutTypeDocument,
|
||||
{
|
||||
variables: { id: pageLayoutId ?? '' },
|
||||
skip: !isOnPageLayoutPage || !isDefined(pageLayoutId),
|
||||
},
|
||||
);
|
||||
const verifyEmailRedirectPath = useAtomStateValue(
|
||||
verifyEmailRedirectPathState,
|
||||
);
|
||||
@@ -157,5 +178,15 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
return AppPath.NotFound;
|
||||
}
|
||||
|
||||
if (
|
||||
isOnPageLayoutPage &&
|
||||
isDefined(pageLayoutId) &&
|
||||
!isPageLayoutLoading &&
|
||||
(!isDefined(pageLayoutData?.getPageLayout) ||
|
||||
pageLayoutData.getPageLayout.type !== PageLayoutType.STANDALONE_PAGE)
|
||||
) {
|
||||
return AppPath.NotFound;
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -6,9 +6,11 @@ import { type BrowsingContext } from '@/ai/types/BrowsingContext';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentPageTypeComponentState } from '@/context-store/states/contextStoreCurrentPageTypeComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStorePageType } from 'twenty-shared/types';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
@@ -23,6 +25,12 @@ export const useGetBrowsingContext = () => {
|
||||
const getBrowsingContext = useCallback((): BrowsingContext | null => {
|
||||
const instanceId = MAIN_CONTEXT_STORE_INSTANCE_ID;
|
||||
|
||||
const pageType = store.get(
|
||||
contextStoreCurrentPageTypeComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const viewType = store.get(
|
||||
contextStoreCurrentViewTypeComponentState.atomFamily({
|
||||
instanceId,
|
||||
@@ -45,7 +53,7 @@ export const useGetBrowsingContext = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (viewType === ContextStoreViewType.ShowPage) {
|
||||
if (pageType === ContextStorePageType.Record) {
|
||||
const targetedRecordsRule = store.get(
|
||||
contextStoreTargetedRecordsRuleComponentState.atomFamily({
|
||||
instanceId,
|
||||
|
||||
@@ -94,6 +94,12 @@ const BookCall = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const StandalonePageLayoutPage = lazy(() =>
|
||||
import('~/pages/page-layout/StandalonePageLayoutPage').then((module) => ({
|
||||
default: module.StandalonePageLayoutPage,
|
||||
})),
|
||||
);
|
||||
|
||||
const NotFound = lazy(() =>
|
||||
import('~/pages/not-found/NotFound').then((module) => ({
|
||||
default: module.NotFound,
|
||||
@@ -220,6 +226,14 @@ export const useCreateAppRouter = (
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.PageLayoutPage}
|
||||
element={
|
||||
<LazyRoute>
|
||||
<StandalonePageLayoutPage />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.SettingsCatchAll}
|
||||
element={
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { objectPermissionsFamilySelector } from '@/auth/states/objectPermissionsFamilySelector';
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { PinnedCommandMenuItemButtons } from '@/command-menu-item/display/components/PinnedCommandMenuItemButtons';
|
||||
import { CommandMenuItemEditButton } from '@/command-menu-item/edit/components/CommandMenuItemEditButton';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
ContextStorePageType,
|
||||
type CommandMenuContextApi,
|
||||
} from 'twenty-shared/types';
|
||||
import { evaluateConditionalAvailabilityExpression } from 'twenty-shared/utils';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const StandalonePageCommandMenu = () => {
|
||||
const store = useStore();
|
||||
const isMobile = useIsMobile();
|
||||
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const commandMenuContextApi = useMemo<CommandMenuContextApi>(() => {
|
||||
const featureFlags: Record<string, boolean> = {};
|
||||
|
||||
for (const flag of currentWorkspace?.featureFlags ?? []) {
|
||||
featureFlags[flag.key] = flag.value === true;
|
||||
}
|
||||
|
||||
const targetObjectReadPermissions: Record<string, boolean> = {};
|
||||
const targetObjectWritePermissions: Record<string, boolean> = {};
|
||||
|
||||
for (const metadataItem of objectMetadataItems) {
|
||||
const permissions = store.get(
|
||||
objectPermissionsFamilySelector.selectorFamily({
|
||||
objectNameSingular: metadataItem.nameSingular,
|
||||
}),
|
||||
);
|
||||
targetObjectReadPermissions[metadataItem.nameSingular] =
|
||||
permissions.canRead;
|
||||
targetObjectWritePermissions[metadataItem.nameSingular] =
|
||||
permissions.canUpdate;
|
||||
}
|
||||
|
||||
return {
|
||||
pageType: ContextStorePageType.Standalone,
|
||||
isInSidePanel: false,
|
||||
isPageInEditMode: false,
|
||||
favoriteRecordIds: [],
|
||||
isSelectAll: false,
|
||||
hasAnySoftDeleteFilterOnView: false,
|
||||
numberOfSelectedRecords: 0,
|
||||
objectPermissions: {
|
||||
canReadObjectRecords: false,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
objectMetadataId: '',
|
||||
rowLevelPermissionPredicates: [],
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
},
|
||||
selectedRecords: [],
|
||||
featureFlags,
|
||||
targetObjectReadPermissions,
|
||||
targetObjectWritePermissions,
|
||||
objectMetadataItem: {},
|
||||
objectMetadataLabel: '',
|
||||
};
|
||||
}, [currentWorkspace?.featureFlags, objectMetadataItems, store]);
|
||||
|
||||
const filteredCommandMenuItems = useMemo(() => {
|
||||
return commandMenuItems
|
||||
.filter(doesCommandMenuItemMatchObjectMetadataId(undefined))
|
||||
.filter(
|
||||
(item) =>
|
||||
item.availabilityType !==
|
||||
CommandMenuItemAvailabilityType.RECORD_SELECTION &&
|
||||
item.availabilityType !==
|
||||
CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT,
|
||||
)
|
||||
.filter((item) =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
commandMenuContextApi,
|
||||
),
|
||||
)
|
||||
.sort(
|
||||
(firstItem, secondItem) => firstItem.position - secondItem.position,
|
||||
);
|
||||
}, [commandMenuItems, commandMenuContextApi]);
|
||||
|
||||
return (
|
||||
<CommandMenuContext.Provider
|
||||
value={{
|
||||
displayType: 'button',
|
||||
containerType: 'standalone-page-header',
|
||||
commandMenuItems: filteredCommandMenuItems,
|
||||
commandMenuContextApi,
|
||||
}}
|
||||
>
|
||||
{!isMobile && <PinnedCommandMenuItemButtons />}
|
||||
<CommandMenuItemEditButton />
|
||||
</CommandMenuContext.Provider>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
CommandMenuContextApiPageType,
|
||||
ContextStorePageType,
|
||||
type CommandMenuContextApi,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const EMPTY_COMMAND_MENU_CONTEXT_API: CommandMenuContextApi = {
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
pageType: ContextStorePageType.Index,
|
||||
isInSidePanel: false,
|
||||
isPageInEditMode: false,
|
||||
favoriteRecordIds: [],
|
||||
|
||||
+2
@@ -4,6 +4,7 @@ import {
|
||||
} from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { doesCommandMenuItemMatchPageType } from '@/command-menu-item/utils/doesCommandMenuItemMatchPageType';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMemo } from 'react';
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
@@ -32,6 +33,7 @@ export const CommandMenuContextProviderContent = ({
|
||||
.filter(
|
||||
doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
|
||||
)
|
||||
.filter(doesCommandMenuItemMatchPageType(commandMenuContextApi.pageType))
|
||||
.filter((item) =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ export const SidePanelCommandMenuItemDisplayPage = () => {
|
||||
const sidePanelSearch = useAtomStateValue(sidePanelSearchState);
|
||||
const { commandMenuItems, commandMenuContextApi } =
|
||||
useContext(CommandMenuContext);
|
||||
|
||||
const commandMenuPinnedInlineLayout = useAtomStateValue(
|
||||
commandMenuPinnedInlineLayoutState,
|
||||
);
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ export const PinnedCommandMenuItemButtonsEditMode = () => {
|
||||
() =>
|
||||
new Set<CommandMenuItemAvailabilityType>([
|
||||
CommandMenuItemAvailabilityType.GLOBAL,
|
||||
CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT,
|
||||
mainContextStoreHasSelectedRecords
|
||||
? CommandMenuItemAvailabilityType.RECORD_SELECTION
|
||||
: CommandMenuItemAvailabilityType.FALLBACK,
|
||||
|
||||
+17
-8
@@ -21,7 +21,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CommandMenuContextApiPageType } from 'twenty-shared/types';
|
||||
import { ContextStorePageType } from 'twenty-shared/types';
|
||||
import {
|
||||
interpolateCommandMenuItemTemplate,
|
||||
isDefined,
|
||||
@@ -71,9 +71,13 @@ export const SidePanelCommandMenuItemEditPage = () => {
|
||||
const currentObjectMetadataItemId =
|
||||
commandMenuContextApi.objectMetadataItem.id;
|
||||
|
||||
const hasObjectContext = isDefined(currentObjectMetadataItemId);
|
||||
|
||||
const isRecordPage =
|
||||
commandMenuContextApi.pageType ===
|
||||
CommandMenuContextApiPageType.RECORD_PAGE;
|
||||
commandMenuContextApi.pageType === ContextStorePageType.Record;
|
||||
|
||||
const isIndexPage =
|
||||
commandMenuContextApi.pageType === ContextStorePageType.Index;
|
||||
|
||||
const mainContextStoreHasSelectedRecords = useAtomStateValue(
|
||||
mainContextStoreHasSelectedRecordsSelector,
|
||||
@@ -93,6 +97,9 @@ export const SidePanelCommandMenuItemEditPage = () => {
|
||||
|
||||
const allowedAvailabilityTypes = new Set<CommandMenuItemAvailabilityType>([
|
||||
CommandMenuItemAvailabilityType.GLOBAL,
|
||||
...(isIndexPage || isRecordPage
|
||||
? [CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT]
|
||||
: []),
|
||||
mainContextStoreHasSelectedRecords
|
||||
? CommandMenuItemAvailabilityType.RECORD_SELECTION
|
||||
: CommandMenuItemAvailabilityType.FALLBACK,
|
||||
@@ -231,11 +238,13 @@ export const SidePanelCommandMenuItemEditPage = () => {
|
||||
|
||||
return (
|
||||
<StyledContainer data-click-outside-id={COMMAND_MENU_CLICK_OUTSIDE_ID}>
|
||||
<StyledViewbar>
|
||||
<CommandMenuItemEditRecordSelectionDropdown
|
||||
isRecordPage={isRecordPage}
|
||||
/>
|
||||
</StyledViewbar>
|
||||
{hasObjectContext && (
|
||||
<StyledViewbar>
|
||||
<CommandMenuItemEditRecordSelectionDropdown
|
||||
isRecordPage={isRecordPage}
|
||||
/>
|
||||
</StyledViewbar>
|
||||
)}
|
||||
<StyledContent>
|
||||
<SidePanelList selectableItemIds={selectableItemIds}>
|
||||
<SidePanelGroup heading={t`Pinned`}>
|
||||
|
||||
+1
@@ -74,6 +74,7 @@ export const buildTriggerWorkflowVersionPayloads = ({
|
||||
return payloads;
|
||||
}
|
||||
case CommandMenuItemAvailabilityTypeEnum.GLOBAL:
|
||||
case CommandMenuItemAvailabilityTypeEnum.GLOBAL_OBJECT_CONTEXT:
|
||||
case CommandMenuItemAvailabilityTypeEnum.FALLBACK: {
|
||||
return payloads;
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMe
|
||||
import { type CommandMenuItemContainerType } from '@/command-menu-item/types/CommandMenuItemContainerType';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { CommandMenuContextApiPageType } from 'twenty-shared/types';
|
||||
import { ContextStorePageType } from 'twenty-shared/types';
|
||||
|
||||
const TEST_COMMAND_MENU_ID = 'test-cmd-menu-1';
|
||||
|
||||
@@ -44,7 +44,7 @@ const getWrapper =
|
||||
displayType: 'button',
|
||||
commandMenuItems: [],
|
||||
commandMenuContextApi: {
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
pageType: ContextStorePageType.Index,
|
||||
isInSidePanel,
|
||||
isPageInEditMode: false,
|
||||
favoriteRecordIds: [],
|
||||
|
||||
+17
-11
@@ -2,17 +2,17 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { objectPermissionsFamilySelector } from '@/auth/states/objectPermissionsFamilySelector';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentPageTypeComponentState } from '@/context-store/states/contextStoreCurrentPageTypeComponentState';
|
||||
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { recordStoreRecordsSelector } from '@/object-record/record-store/states/selectors/recordStoreRecordsSelector';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
@@ -22,7 +22,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useStore } from 'jotai';
|
||||
import {
|
||||
CommandMenuContextApiPageType,
|
||||
ContextStorePageType,
|
||||
type CommandMenuContextApi,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined, resolveObjectMetadataLabel } from 'twenty-shared/utils';
|
||||
@@ -93,25 +93,31 @@ export const useCommandMenuContextApi = (): CommandMenuContextApi => {
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
};
|
||||
|
||||
const { recordIndexId } = useRecordIndexIdFromCurrentContextStore();
|
||||
const contextStoreCurrentViewId = useAtomComponentStateValue(
|
||||
contextStoreCurrentViewIdComponentState,
|
||||
);
|
||||
|
||||
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem?.namePlural ?? '',
|
||||
contextStoreCurrentViewId ?? '',
|
||||
);
|
||||
|
||||
const hasAnySoftDeleteFilterOnView = useAtomComponentSelectorValue(
|
||||
hasAnySoftDeleteFilterOnViewComponentSelector,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const contextStoreCurrentViewType = useAtomComponentStateValue(
|
||||
contextStoreCurrentViewTypeComponentState,
|
||||
const contextStoreCurrentPageType = useAtomComponentStateValue(
|
||||
contextStoreCurrentPageTypeComponentState,
|
||||
);
|
||||
|
||||
const contextStoreIsPageInEditMode = useAtomComponentStateValue(
|
||||
contextStoreIsPageInEditModeComponentState,
|
||||
);
|
||||
|
||||
const pageType =
|
||||
contextStoreCurrentViewType === ContextStoreViewType.ShowPage
|
||||
? CommandMenuContextApiPageType.RECORD_PAGE
|
||||
: CommandMenuContextApiPageType.INDEX_PAGE;
|
||||
const pageType = isDefined(contextStoreCurrentPageType)
|
||||
? contextStoreCurrentPageType
|
||||
: ContextStorePageType.Index;
|
||||
|
||||
const isSelectAll = contextStoreTargetedRecordsRule.mode === 'exclusion';
|
||||
|
||||
|
||||
+1
@@ -3,4 +3,5 @@ export type CommandMenuItemContainerType =
|
||||
| 'index-page-header'
|
||||
| 'index-page-dropdown'
|
||||
| 'show-page-header'
|
||||
| 'standalone-page-header'
|
||||
| 'command-menu-show-page-dropdown';
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { ContextStorePageType } from 'twenty-shared/types';
|
||||
import {
|
||||
CommandMenuItemAvailabilityType,
|
||||
type CommandMenuItemFieldsFragment,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const PAGE_TYPES_WITH_RECORD_CONTEXT = new Set([
|
||||
ContextStorePageType.Index,
|
||||
ContextStorePageType.Record,
|
||||
]);
|
||||
|
||||
const AVAILABILITY_TYPES_REQUIRING_RECORD_CONTEXT = new Set([
|
||||
CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT,
|
||||
CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
]);
|
||||
|
||||
export const doesCommandMenuItemMatchPageType =
|
||||
(pageType: ContextStorePageType) => (item: CommandMenuItemFieldsFragment) =>
|
||||
!AVAILABILITY_TYPES_REQUIRING_RECORD_CONTEXT.has(item.availabilityType) ||
|
||||
PAGE_TYPES_WITH_RECORD_CONTEXT.has(pageType);
|
||||
+11
-3
@@ -26,15 +26,17 @@ import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceSta
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentPageTypeComponentState } from '@/context-store/states/contextStoreCurrentPageTypeComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { ContextStorePageType } from 'twenty-shared/types';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
|
||||
import { SidePanelRouter } from '@/side-panel/components/SidePanelRouter';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { type SidePanelRootPage } from '@/side-panel/pages/root/components/SidePanelRootPage';
|
||||
import { type SidePanelCommandMenuItemDisplayPage } from '@/command-menu-item/display/components/SidePanelCommandMenuItemDisplayPage';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
@@ -75,7 +77,7 @@ const ContextStoreDecorator: Decorator = (Story) => {
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof SidePanelRootPage> = {
|
||||
const meta: Meta<typeof SidePanelCommandMenuItemDisplayPage> = {
|
||||
title: 'Modules/CommandMenu/CommandMenu',
|
||||
component: SidePanelRouter,
|
||||
decorators: [
|
||||
@@ -131,6 +133,12 @@ const meta: Meta<typeof SidePanelRootPage> = {
|
||||
}),
|
||||
ContextStoreViewType.Table,
|
||||
);
|
||||
jotaiStore.set(
|
||||
contextStoreCurrentPageTypeComponentState.atomFamily({
|
||||
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
}),
|
||||
ContextStorePageType.Index,
|
||||
);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
@@ -145,7 +153,7 @@ const meta: Meta<typeof SidePanelRootPage> = {
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SidePanelRootPage>;
|
||||
type Story = StoryObj<typeof SidePanelCommandMenuItemDisplayPage>;
|
||||
|
||||
export const DefaultWithoutSearch: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
|
||||
+3
@@ -47,6 +47,7 @@ export const MainContextStoreProvider = () => {
|
||||
AppPath.RecordIndexPage,
|
||||
);
|
||||
const isRecordShowPage = isMatchingLocation(location, AppPath.RecordShowPage);
|
||||
const isStandalonePage = isMatchingLocation(location, AppPath.PageLayoutPage);
|
||||
const isSettingsPage = useIsSettingsPage();
|
||||
const showAuthModal = useShowAuthModal();
|
||||
|
||||
@@ -118,6 +119,7 @@ export const MainContextStoreProvider = () => {
|
||||
const shouldComputeContextStore =
|
||||
(isRecordIndexPage ||
|
||||
isRecordShowPage ||
|
||||
isStandalonePage ||
|
||||
isSettingsPage ||
|
||||
showAuthModal) &&
|
||||
metadataStore.status === 'up-to-date';
|
||||
@@ -132,6 +134,7 @@ export const MainContextStoreProvider = () => {
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
isRecordIndexPage={isRecordIndexPage}
|
||||
isRecordShowPage={isRecordShowPage}
|
||||
isStandalonePage={isStandalonePage}
|
||||
isSettingsPage={isSettingsPage}
|
||||
/>
|
||||
);
|
||||
|
||||
+28
-2
@@ -1,7 +1,9 @@
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentPageTypeComponentState } from '@/context-store/states/contextStoreCurrentPageTypeComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { getPageType } from '@/context-store/utils/getPageType';
|
||||
import { getViewType } from '@/context-store/utils/getViewType';
|
||||
import { useSetLastVisitedObjectMetadataId } from '@/navigation/hooks/useSetLastVisitedObjectMetadataId';
|
||||
import { useSetLastVisitedViewForObjectMetadataNamePlural } from '@/navigation/hooks/useSetLastVisitedViewForObjectMetadataNamePlural';
|
||||
@@ -16,6 +18,7 @@ type MainContextStoreProviderEffectProps = {
|
||||
objectMetadataItem?: EnrichedObjectMetadataItem;
|
||||
isRecordIndexPage: boolean;
|
||||
isRecordShowPage: boolean;
|
||||
isStandalonePage: boolean;
|
||||
isSettingsPage: boolean;
|
||||
};
|
||||
|
||||
@@ -24,6 +27,7 @@ export const MainContextStoreProviderEffect = ({
|
||||
objectMetadataItem,
|
||||
isRecordIndexPage,
|
||||
isRecordShowPage,
|
||||
isStandalonePage,
|
||||
isSettingsPage,
|
||||
}: MainContextStoreProviderEffectProps) => {
|
||||
const { setLastVisitedViewForObjectMetadataNamePlural } =
|
||||
@@ -44,6 +48,12 @@ export const MainContextStoreProviderEffect = ({
|
||||
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const [contextStoreCurrentPageType, setContextStoreCurrentPageType] =
|
||||
useAtomComponentState(
|
||||
contextStoreCurrentPageTypeComponentState,
|
||||
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const [
|
||||
contextStoreCurrentObjectMetadataItemId,
|
||||
setContextStoreCurrentObjectMetadataItemId,
|
||||
@@ -100,8 +110,6 @@ export const MainContextStoreProviderEffect = ({
|
||||
|
||||
useEffect(() => {
|
||||
const viewType = getViewType({
|
||||
isSettingsPage,
|
||||
isRecordShowPage,
|
||||
isRecordIndexPage,
|
||||
view,
|
||||
});
|
||||
@@ -113,9 +121,27 @@ export const MainContextStoreProviderEffect = ({
|
||||
contextStoreCurrentViewType,
|
||||
setContextStoreCurrentViewType,
|
||||
view,
|
||||
isRecordIndexPage,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const pageType = getPageType({
|
||||
isSettingsPage,
|
||||
isRecordShowPage,
|
||||
isRecordIndexPage,
|
||||
isStandalonePage,
|
||||
});
|
||||
|
||||
if (contextStoreCurrentPageType !== pageType) {
|
||||
setContextStoreCurrentPageType(pageType);
|
||||
}
|
||||
}, [
|
||||
contextStoreCurrentPageType,
|
||||
setContextStoreCurrentPageType,
|
||||
isSettingsPage,
|
||||
isRecordShowPage,
|
||||
isRecordIndexPage,
|
||||
isStandalonePage,
|
||||
]);
|
||||
|
||||
return null;
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { type ContextStorePageType } from 'twenty-shared/types';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
export const contextStoreCurrentPageTypeComponentState =
|
||||
createAtomComponentState<ContextStorePageType | null>({
|
||||
key: 'contextStoreCurrentPageTypeComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: ContextStoreComponentInstanceContext,
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
export enum ContextStoreViewType {
|
||||
Table = 'table',
|
||||
Kanban = 'kanban',
|
||||
ShowPage = 'show-page',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ContextStorePageType } from 'twenty-shared/types';
|
||||
|
||||
export const getPageType = ({
|
||||
isSettingsPage,
|
||||
isRecordShowPage,
|
||||
isRecordIndexPage,
|
||||
isStandalonePage,
|
||||
}: {
|
||||
isSettingsPage: boolean;
|
||||
isRecordShowPage: boolean;
|
||||
isRecordIndexPage: boolean;
|
||||
isStandalonePage: boolean;
|
||||
}): ContextStorePageType | null => {
|
||||
if (isSettingsPage) {
|
||||
return ContextStorePageType.Settings;
|
||||
}
|
||||
|
||||
if (isRecordIndexPage) {
|
||||
return ContextStorePageType.Index;
|
||||
}
|
||||
|
||||
if (isRecordShowPage) {
|
||||
return ContextStorePageType.Record;
|
||||
}
|
||||
|
||||
if (isStandalonePage) {
|
||||
return ContextStorePageType.Standalone;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -3,29 +3,17 @@ import { type View } from '@/views/types/View';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
|
||||
export const getViewType = ({
|
||||
isSettingsPage,
|
||||
isRecordShowPage,
|
||||
isRecordIndexPage,
|
||||
view,
|
||||
}: {
|
||||
isSettingsPage: boolean;
|
||||
isRecordShowPage: boolean;
|
||||
isRecordIndexPage: boolean;
|
||||
view?: View;
|
||||
}) => {
|
||||
if (isSettingsPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isRecordIndexPage) {
|
||||
return view?.type === ViewType.KANBAN
|
||||
? ContextStoreViewType.Kanban
|
||||
: ContextStoreViewType.Table;
|
||||
}
|
||||
|
||||
if (isRecordShowPage) {
|
||||
return ContextStoreViewType.ShowPage;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_ONE_PAGE_LAYOUT_TYPE = gql`
|
||||
query FindOnePageLayoutType($id: String!) {
|
||||
getPageLayout(id: $id) {
|
||||
id
|
||||
type
|
||||
}
|
||||
}
|
||||
`;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const DEFAULT_NAVIGATION_MENU_ITEM_COLOR_PAGE_LAYOUT = 'blue';
|
||||
+1
@@ -13,6 +13,7 @@ export const NAVIGATION_MENU_ITEM_FRAGMENT = gql`
|
||||
link
|
||||
icon
|
||||
color
|
||||
pageLayoutId
|
||||
position
|
||||
applicationId
|
||||
createdAt
|
||||
|
||||
+3
@@ -21,6 +21,9 @@ export const filterAndSortNavigationMenuItems = (
|
||||
if (item.type === NavigationMenuItemType.LINK) {
|
||||
return true;
|
||||
}
|
||||
if (item.type === NavigationMenuItemType.PAGE_LAYOUT) {
|
||||
return isDefined(item.pageLayoutId);
|
||||
}
|
||||
if (item.type === NavigationMenuItemType.OBJECT) {
|
||||
return (
|
||||
isDefined(item.targetObjectMetadataId) &&
|
||||
|
||||
+7
@@ -2,6 +2,7 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_PAGE_LAYOUT } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorPageLayout';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
@@ -27,6 +28,12 @@ export const getNavigationMenuItemColor = (
|
||||
return DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK;
|
||||
}
|
||||
|
||||
if (navigationMenuItem.type === NavigationMenuItemType.PAGE_LAYOUT) {
|
||||
return isNonEmptyString(navigationMenuItem.color)
|
||||
? (navigationMenuItem.color as ThemeColor)
|
||||
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_PAGE_LAYOUT;
|
||||
}
|
||||
|
||||
if (
|
||||
navigationMenuItem.type === NavigationMenuItemType.OBJECT ||
|
||||
navigationMenuItem.type === NavigationMenuItemType.VIEW
|
||||
|
||||
+16
@@ -2,6 +2,7 @@ import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { NavigationMenuItemFolder } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolder';
|
||||
import { NavigationMenuItemLinkDisplay } from '@/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay';
|
||||
import { NavigationMenuItemObjectDisplay } from '@/navigation-menu-item/display/object/components/NavigationMenuItemObjectDisplay';
|
||||
import { NavigationMenuItemPageLayoutDisplay } from '@/navigation-menu-item/display/page-layout/components/NavigationMenuItemPageLayoutDisplay';
|
||||
import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps';
|
||||
|
||||
type NavigationMenuItemDisplayProps = NavigationMenuItemSectionContentProps;
|
||||
@@ -49,6 +50,21 @@ export const NavigationMenuItemDisplay = ({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
case NavigationMenuItemType.PAGE_LAYOUT:
|
||||
return (
|
||||
<NavigationMenuItemPageLayoutDisplay
|
||||
item={item}
|
||||
isEditInPlace={isEditInPlace}
|
||||
editModeProps={editModeProps}
|
||||
isDragging={isDragging}
|
||||
folderChildrenById={folderChildrenById}
|
||||
folderCount={folderCount}
|
||||
rightOptions={rightOptions}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<NavigationMenuItemObjectDisplay
|
||||
|
||||
+23
@@ -74,6 +74,29 @@ export const NavigationMenuItemIcon = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (navigationMenuItem.type === NavigationMenuItemType.PAGE_LAYOUT) {
|
||||
const pageLayoutIcon = isDefined(navigationMenuItem.icon)
|
||||
? getIcon(navigationMenuItem.icon)
|
||||
: undefined;
|
||||
const pageLayoutColor = getNavigationMenuItemColor(navigationMenuItem);
|
||||
const pageLayoutIconStyle = getIconTileColorShades(pageLayoutColor);
|
||||
|
||||
return (
|
||||
<StyledTintedIconTileContainer
|
||||
$backgroundColor={pageLayoutIconStyle.backgroundColor}
|
||||
$borderColor={pageLayoutIconStyle.borderColor}
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
type="icon"
|
||||
Icon={pageLayoutIcon}
|
||||
iconColor={pageLayoutIconStyle.iconColor}
|
||||
placeholder={navigationMenuItem.name ?? ''}
|
||||
/>
|
||||
</StyledTintedIconTileContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
navigationMenuItem,
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
|
||||
import { getPageLayoutNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/page-layout/utils/getPageLayoutNavigationMenuItemComputedLink';
|
||||
import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
type NavigationMenuItemPageLayoutDisplayProps =
|
||||
NavigationMenuItemSectionContentProps;
|
||||
|
||||
export const NavigationMenuItemPageLayoutDisplay = ({
|
||||
item,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
rightOptions,
|
||||
}: NavigationMenuItemPageLayoutDisplayProps) => {
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
|
||||
const label = item.name ?? '';
|
||||
const computedLink = getPageLayoutNavigationMenuItemComputedLink(item);
|
||||
|
||||
return (
|
||||
<NavigationDrawerItem
|
||||
label={label}
|
||||
to={
|
||||
isLayoutCustomizationModeEnabled || isDragging
|
||||
? undefined
|
||||
: computedLink
|
||||
}
|
||||
onClick={
|
||||
isLayoutCustomizationModeEnabled
|
||||
? editModeProps?.onEditModeClick
|
||||
: undefined
|
||||
}
|
||||
Icon={() => <NavigationMenuItemIcon navigationMenuItem={item} />}
|
||||
active={false}
|
||||
isSelectedInEditMode={editModeProps?.isSelectedInEditMode}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
rightOptions={rightOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const getPageLayoutNavigationMenuItemComputedLink = (
|
||||
item: Pick<NavigationMenuItem, 'pageLayoutId'>,
|
||||
): string => {
|
||||
if (!isDefined(item.pageLayoutId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getAppPath(AppPath.PageLayoutPage, {
|
||||
pageLayoutId: item.pageLayoutId,
|
||||
});
|
||||
};
|
||||
+2
-1
@@ -86,7 +86,8 @@ export const WorkspaceSectionContainer = ({
|
||||
const itemType = item.type;
|
||||
if (
|
||||
itemType === NavigationMenuItemType.FOLDER ||
|
||||
itemType === NavigationMenuItemType.LINK
|
||||
itemType === NavigationMenuItemType.LINK ||
|
||||
itemType === NavigationMenuItemType.PAGE_LAYOUT
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+3
@@ -1,5 +1,6 @@
|
||||
import { getLinkNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/link/utils/getLinkNavigationMenuItemComputedLink';
|
||||
import { getObjectNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemComputedLink';
|
||||
import { getPageLayoutNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/page-layout/utils/getPageLayoutNavigationMenuItemComputedLink';
|
||||
import { getRecordNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/record/utils/getRecordNavigationMenuItemComputedLink';
|
||||
import { getViewNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/view/utils/getViewNavigationMenuItemComputedLink';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
@@ -29,6 +30,8 @@ export const getNavigationMenuItemComputedLink = (
|
||||
return getLinkNavigationMenuItemComputedLink(item);
|
||||
case NavigationMenuItemType.RECORD:
|
||||
return getRecordNavigationMenuItemComputedLink(item, objectMetadataItems);
|
||||
case NavigationMenuItemType.PAGE_LAYOUT:
|
||||
return getPageLayoutNavigationMenuItemComputedLink(item);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
+4
-1
@@ -49,7 +49,10 @@ export const getWorkspaceSidebarOrphanItemsInDisplayOrder = ({
|
||||
}
|
||||
const rowSource = isDefined(validItem) ? validItem : item;
|
||||
|
||||
if (rowSource.type === NavigationMenuItemType.LINK) {
|
||||
if (
|
||||
rowSource.type === NavigationMenuItemType.LINK ||
|
||||
rowSource.type === NavigationMenuItemType.PAGE_LAYOUT
|
||||
) {
|
||||
acc.push(rowSource);
|
||||
return acc;
|
||||
}
|
||||
|
||||
@@ -7,14 +7,21 @@ import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageL
|
||||
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
|
||||
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
|
||||
import { useReorderPageLayoutWidgets } from '@/page-layout/hooks/useReorderPageLayoutWidgets';
|
||||
import { StandaloneWidgetPlaceholder } from '@/page-layout/widgets/components/StandaloneWidgetPlaceholder';
|
||||
import { RecordPageAddWidgetSection } from '@/page-layout/widgets/components/RecordPageAddWidgetSection';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledEmptyStandalonePageContainer = styled.div`
|
||||
display: grid;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const PageLayoutContent = () => {
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
|
||||
@@ -38,6 +45,18 @@ export const PageLayoutContent = () => {
|
||||
const isCanvasLayout = layoutMode === PageLayoutTabLayoutMode.CANVAS;
|
||||
const isVerticalList = layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST;
|
||||
|
||||
const isEmptyStandalonePage =
|
||||
currentPageLayout.type === PageLayoutType.STANDALONE_PAGE &&
|
||||
activeTab.widgets.length === 0;
|
||||
|
||||
if (isEmptyStandalonePage) {
|
||||
return (
|
||||
<StyledEmptyStandalonePageContainer>
|
||||
<StandaloneWidgetPlaceholder />
|
||||
</StyledEmptyStandalonePageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCanvasLayout) {
|
||||
return <PageLayoutCanvasViewer widgets={activeTab.widgets} />;
|
||||
}
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
import { DashboardPageLayoutEditModeProvider } from '@/page-layout/components/DashboardPageLayoutEditModeProvider';
|
||||
import { RecordPageLayoutEditModeProvider } from '@/page-layout/components/RecordPageLayoutEditModeProvider';
|
||||
import { PageLayoutEditModeProviderContext } from '@/page-layout/contexts/PageLayoutEditModeContext';
|
||||
import { type ReactNode } from 'react';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -14,6 +15,14 @@ export const PageLayoutEditModeProvider = ({
|
||||
pageLayoutId,
|
||||
children,
|
||||
}: PageLayoutEditModeProviderProps) => {
|
||||
if (layoutType === PageLayoutType.STANDALONE_PAGE) {
|
||||
return (
|
||||
<PageLayoutEditModeProviderContext value={{ isInEditMode: false }}>
|
||||
{children}
|
||||
</PageLayoutEditModeProviderContext>
|
||||
);
|
||||
}
|
||||
|
||||
if (layoutType === PageLayoutType.RECORD_PAGE) {
|
||||
return (
|
||||
<RecordPageLayoutEditModeProvider>
|
||||
|
||||
+60
-17
@@ -8,6 +8,7 @@ import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayo
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import { isPageLayoutEmpty } from '@/page-layout/utils/isPageLayoutEmpty';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
@@ -21,24 +22,16 @@ type PageLayoutInitializationQueryEffectProps = {
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
export const PageLayoutInitializationQueryEffect = ({
|
||||
const PageLayoutInitializationEffect = ({
|
||||
pageLayoutId,
|
||||
}: PageLayoutInitializationQueryEffectProps) => {
|
||||
pageLayout,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
pageLayout: PageLayout | undefined;
|
||||
}) => {
|
||||
const [pageLayoutIsInitialized, setPageLayoutIsInitialized] =
|
||||
useAtomComponentState(pageLayoutIsInitializedComponentState);
|
||||
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
|
||||
|
||||
const basePageLayout = useBasePageLayout(pageLayoutId);
|
||||
const pageLayoutWithRelationWidgets =
|
||||
usePageLayoutWithRelationWidgets(basePageLayout);
|
||||
|
||||
const pageLayout = isRecordPageLayoutEditingEnabled
|
||||
? basePageLayout
|
||||
: pageLayoutWithRelationWidgets;
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
@@ -55,8 +48,6 @@ export const PageLayoutInitializationQueryEffect = ({
|
||||
|
||||
const initializePageLayout = useCallback(
|
||||
(layout: PageLayout) => {
|
||||
const isRecordPageLayout = layout.type === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
const currentPersisted = store.get(
|
||||
pageLayoutPersistedComponentCallbackState,
|
||||
);
|
||||
@@ -78,7 +69,9 @@ export const PageLayoutInitializationQueryEffect = ({
|
||||
const tabLayouts = convertPageLayoutToTabLayouts(layout);
|
||||
store.set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts);
|
||||
|
||||
if (!isRecordPageLayout) {
|
||||
const isDashboardLayout = layout.type === PageLayoutType.DASHBOARD;
|
||||
|
||||
if (isDashboardLayout) {
|
||||
const shouldEnterDashboardEditMode = isPageLayoutEmpty(layout);
|
||||
setIsPageLayoutInEditMode(shouldEnterDashboardEditMode);
|
||||
}
|
||||
@@ -106,3 +99,53 @@ export const PageLayoutInitializationQueryEffect = ({
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const PageLayoutInitializationWithRelationWidgets = ({
|
||||
pageLayoutId,
|
||||
basePageLayout,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
basePageLayout: PageLayout | undefined;
|
||||
}) => {
|
||||
const pageLayout = usePageLayoutWithRelationWidgets(basePageLayout);
|
||||
|
||||
return (
|
||||
<PageLayoutInitializationEffect
|
||||
pageLayoutId={pageLayoutId}
|
||||
pageLayout={pageLayout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// oxlint-disable-next-line twenty/effect-components
|
||||
export const PageLayoutInitializationQueryEffect = ({
|
||||
pageLayoutId,
|
||||
}: PageLayoutInitializationQueryEffectProps) => {
|
||||
const { layoutType } = useLayoutRenderingContext();
|
||||
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
|
||||
|
||||
const basePageLayout = useBasePageLayout(pageLayoutId);
|
||||
|
||||
const needsRelationWidgets =
|
||||
layoutType === PageLayoutType.RECORD_PAGE &&
|
||||
!isRecordPageLayoutEditingEnabled;
|
||||
|
||||
if (needsRelationWidgets) {
|
||||
return (
|
||||
<PageLayoutInitializationWithRelationWidgets
|
||||
pageLayoutId={pageLayoutId}
|
||||
basePageLayout={basePageLayout}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayoutInitializationEffect
|
||||
pageLayoutId={pageLayoutId}
|
||||
pageLayout={basePageLayout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/con
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
type PageLayoutRendererProps = {
|
||||
pageLayoutId: string;
|
||||
@@ -48,9 +48,12 @@ export const PageLayoutRenderer = ({
|
||||
>
|
||||
<PageLayoutInitializationQueryEffect pageLayoutId={pageLayoutId} />
|
||||
<PageLayoutRecordPageCustomizationSessionRegistrationEffect />
|
||||
{!isRecordPageLayoutEditingEnabled && (
|
||||
<PageLayoutRelationWidgetsSyncEffect pageLayoutId={pageLayoutId} />
|
||||
)}
|
||||
{!isRecordPageLayoutEditingEnabled &&
|
||||
layoutType === PageLayoutType.RECORD_PAGE && (
|
||||
<PageLayoutRelationWidgetsSyncEffect
|
||||
pageLayoutId={pageLayoutId}
|
||||
/>
|
||||
)}
|
||||
<PageLayoutRendererContent />
|
||||
</PageLayoutEditModeProvider>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
|
||||
+12
-1
@@ -7,6 +7,13 @@ describe('shouldEnableTabEditingFeatures', () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for STANDALONE_PAGE layout type', () => {
|
||||
const result = shouldEnableTabEditingFeatures(
|
||||
PageLayoutType.STANDALONE_PAGE,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for RECORD_PAGE layout type without flag', () => {
|
||||
const result = shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE);
|
||||
expect(result).toBe(false);
|
||||
@@ -42,11 +49,15 @@ describe('shouldEnableTabEditingFeatures', () => {
|
||||
});
|
||||
|
||||
describe('behavior validation', () => {
|
||||
it('should enable tab editing features only for dashboards and record pages with flag', () => {
|
||||
it('should enable tab editing features only for dashboards, standalone pages, and record pages with flag', () => {
|
||||
expect(shouldEnableTabEditingFeatures(PageLayoutType.DASHBOARD)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
expect(
|
||||
shouldEnableTabEditingFeatures(PageLayoutType.STANDALONE_PAGE),
|
||||
).toBe(true);
|
||||
|
||||
expect(shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE)).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
+4
-1
@@ -4,7 +4,10 @@ export const shouldEnableTabEditingFeatures = (
|
||||
pageLayoutType: PageLayoutType,
|
||||
isRecordPageGlobalEditionEnabled?: boolean,
|
||||
): boolean => {
|
||||
if (pageLayoutType === PageLayoutType.DASHBOARD) {
|
||||
if (
|
||||
pageLayoutType === PageLayoutType.DASHBOARD ||
|
||||
pageLayoutType === PageLayoutType.STANDALONE_PAGE
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { WidgetCardShell } from '@/page-layout/widgets/components/WidgetCardShell';
|
||||
import { useWidgetRendererState } from '@/page-layout/widgets/hooks/useWidgetRendererState';
|
||||
|
||||
type NonRecordPageWidgetRendererProps = {
|
||||
widget: PageLayoutWidget;
|
||||
};
|
||||
|
||||
export const NonRecordPageWidgetRenderer = ({
|
||||
widget,
|
||||
}: NonRecordPageWidgetRendererProps) => {
|
||||
const state = useWidgetRendererState(widget);
|
||||
|
||||
const isCanvasVariant = state.variant === 'canvas';
|
||||
|
||||
return (
|
||||
<WidgetCardShell
|
||||
widget={widget}
|
||||
variant={state.variant}
|
||||
isEditable={state.isPageLayoutInEditMode}
|
||||
isEditing={state.isEditing}
|
||||
isDragging={state.isDragging}
|
||||
isResizing={state.isResizing}
|
||||
isLastWidget={state.isLastWidget}
|
||||
showHeader={state.showHeader}
|
||||
hasAccess={state.hasAccess}
|
||||
restriction={state.restriction}
|
||||
actions={[]}
|
||||
isInVerticalListTab={state.isInVerticalListTab}
|
||||
isMobile={state.isMobile}
|
||||
isReorderEnabled={true}
|
||||
isDeletingWidgetEnabled={true}
|
||||
onClick={state.isPageLayoutInEditMode ? state.handleClick : undefined}
|
||||
onRemove={state.handleRemove}
|
||||
onMouseEnter={isCanvasVariant ? undefined : state.handleMouseEnter}
|
||||
onMouseLeave={isCanvasVariant ? undefined : state.handleMouseLeave}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { WidgetCardShell } from '@/page-layout/widgets/components/WidgetCardShell';
|
||||
import { useWidgetActions } from '@/page-layout/widgets/hooks/useWidgetActions';
|
||||
import { useWidgetRendererState } from '@/page-layout/widgets/hooks/useWidgetRendererState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
PageLayoutType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledEditingWidgetWrapper = styled.div`
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
type RecordPageWidgetRendererProps = {
|
||||
widget: PageLayoutWidget;
|
||||
};
|
||||
|
||||
export const RecordPageWidgetRenderer = ({
|
||||
widget,
|
||||
}: RecordPageWidgetRendererProps) => {
|
||||
const state = useWidgetRendererState(widget);
|
||||
|
||||
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
);
|
||||
|
||||
const isRecordPageLayout =
|
||||
state.currentPageLayout.type === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
const isReorderEnabled =
|
||||
!isRecordPageLayout ||
|
||||
(isRecordPageLayout && isRecordPageGlobalEditionEnabled);
|
||||
|
||||
const isDeletingWidgetEnabled =
|
||||
!isRecordPageLayout ||
|
||||
(isRecordPageLayout && isRecordPageGlobalEditionEnabled);
|
||||
|
||||
const isWidgetEditable =
|
||||
state.isPageLayoutInEditMode &&
|
||||
(!isRecordPageLayout ||
|
||||
(isRecordPageLayout && isRecordPageGlobalEditionEnabled) ||
|
||||
widget.type === WidgetType.FIELDS ||
|
||||
widget.type === WidgetType.FIELD);
|
||||
|
||||
const actions = useWidgetActions({ widget });
|
||||
|
||||
// TODO: remove once all record page layouts widgets use the editable contain in edit mode
|
||||
const shouldWrapWithEditingWrapper =
|
||||
isWidgetEditable &&
|
||||
state.variant === 'side-column' &&
|
||||
!isRecordPageGlobalEditionEnabled;
|
||||
|
||||
const isCanvasVariant = state.variant === 'canvas';
|
||||
|
||||
const shell = (
|
||||
<WidgetCardShell
|
||||
widget={widget}
|
||||
variant={state.variant}
|
||||
isEditable={isWidgetEditable}
|
||||
isEditing={state.isEditing}
|
||||
isDragging={state.isDragging}
|
||||
isResizing={state.isResizing}
|
||||
isLastWidget={state.isLastWidget}
|
||||
showHeader={state.showHeader}
|
||||
hasAccess={state.hasAccess}
|
||||
restriction={state.restriction}
|
||||
actions={actions}
|
||||
isInVerticalListTab={state.isInVerticalListTab}
|
||||
isMobile={state.isMobile}
|
||||
isReorderEnabled={isReorderEnabled}
|
||||
isDeletingWidgetEnabled={isDeletingWidgetEnabled}
|
||||
onClick={isWidgetEditable ? state.handleClick : undefined}
|
||||
onRemove={state.handleRemove}
|
||||
onMouseEnter={isCanvasVariant ? undefined : state.handleMouseEnter}
|
||||
onMouseLeave={isCanvasVariant ? undefined : state.handleMouseLeave}
|
||||
/>
|
||||
);
|
||||
|
||||
if (shouldWrapWithEditingWrapper) {
|
||||
return <StyledEditingWidgetWrapper>{shell}</StyledEditingWidgetWrapper>;
|
||||
}
|
||||
|
||||
return shell;
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
AnimatedPlaceholder,
|
||||
AnimatedPlaceholderEmptyContainer,
|
||||
AnimatedPlaceholderEmptySubTitle,
|
||||
AnimatedPlaceholderEmptyTextContainer,
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
||||
} from 'twenty-ui/layout';
|
||||
|
||||
const StyledPlaceholderContainer = styled.div`
|
||||
background: ${themeCssVariables.background.secondary};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const StandaloneWidgetPlaceholder = () => {
|
||||
return (
|
||||
<StyledPlaceholderContainer className="widget">
|
||||
<AnimatedPlaceholderEmptyContainer
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
|
||||
>
|
||||
<AnimatedPlaceholder type="noWidgets" />
|
||||
<AnimatedPlaceholderEmptyTextContainer>
|
||||
<AnimatedPlaceholderEmptyTitle>
|
||||
<Trans>Nothing to see</Trans>
|
||||
</AnimatedPlaceholderEmptyTitle>
|
||||
<AnimatedPlaceholderEmptySubTitle>
|
||||
<Trans>This page has no content</Trans>
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
</StyledPlaceholderContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { PageLayoutWidgetForbiddenDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetForbiddenDisplay';
|
||||
import { PageLayoutWidgetInvalidConfigDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetInvalidConfigDisplay';
|
||||
import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer';
|
||||
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
|
||||
import { type WidgetAccessDenialInfo } from '@/page-layout/widgets/types/WidgetAccessDenialInfo';
|
||||
import { type WidgetAction } from '@/page-layout/widgets/types/WidgetAction';
|
||||
import { type WidgetCardVariant } from '@/page-layout/widgets/types/WidgetCardVariant';
|
||||
import { WidgetCard } from '@/page-layout/widgets/widget-card/components/WidgetCard';
|
||||
import { WidgetCardContent } from '@/page-layout/widgets/widget-card/components/WidgetCardContent';
|
||||
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type MouseEvent, useContext } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { IconLock } from 'twenty-ui/display';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledNoAccessContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
type WidgetCardShellProps = {
|
||||
widget: PageLayoutWidget;
|
||||
variant: WidgetCardVariant;
|
||||
isEditable: boolean;
|
||||
isEditing: boolean;
|
||||
isDragging: boolean;
|
||||
isResizing: boolean;
|
||||
isLastWidget: boolean;
|
||||
showHeader: boolean;
|
||||
hasAccess: boolean;
|
||||
restriction: WidgetAccessDenialInfo;
|
||||
actions: WidgetAction[];
|
||||
isInVerticalListTab: boolean;
|
||||
isMobile: boolean;
|
||||
isReorderEnabled: boolean;
|
||||
isDeletingWidgetEnabled: boolean;
|
||||
onClick?: () => void;
|
||||
onRemove: (e?: MouseEvent) => void;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
};
|
||||
|
||||
export const WidgetCardShell = ({
|
||||
widget,
|
||||
variant,
|
||||
isEditable,
|
||||
isEditing,
|
||||
isDragging,
|
||||
isResizing,
|
||||
isLastWidget,
|
||||
showHeader,
|
||||
hasAccess,
|
||||
restriction,
|
||||
actions,
|
||||
isInVerticalListTab,
|
||||
isMobile,
|
||||
isReorderEnabled,
|
||||
isDeletingWidgetEnabled,
|
||||
onClick,
|
||||
onRemove,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: WidgetCardShellProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<WidgetComponentInstanceContext.Provider value={{ instanceId: widget.id }}>
|
||||
<WidgetCard
|
||||
headerLess={!showHeader}
|
||||
variant={variant}
|
||||
isEditable={isEditable}
|
||||
onClick={onClick}
|
||||
isEditing={isEditing}
|
||||
isDragging={isDragging}
|
||||
isResizing={isResizing}
|
||||
isLastWidget={isLastWidget}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
data-widget-id={widget.id}
|
||||
data-testid={widget.id}
|
||||
className="widget"
|
||||
>
|
||||
{showHeader && (
|
||||
<WidgetCardHeader
|
||||
widgetId={widget.id}
|
||||
variant={variant}
|
||||
isInEditMode={isEditable}
|
||||
isResizing={isResizing}
|
||||
isReorderEnabled={isReorderEnabled}
|
||||
isDeletingWidgetEnabled={isDeletingWidgetEnabled}
|
||||
title={widget.title}
|
||||
onRemove={onRemove}
|
||||
actions={actions}
|
||||
forbiddenDisplay={
|
||||
!hasAccess && (
|
||||
<PageLayoutWidgetForbiddenDisplay
|
||||
widgetId={widget.id}
|
||||
restriction={restriction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WidgetCardContent
|
||||
variant={variant}
|
||||
hasHeader={showHeader}
|
||||
isEditable={isEditable}
|
||||
isInVerticalListTab={isInVerticalListTab}
|
||||
isMobile={isMobile}
|
||||
hasInteractiveContent={widget.type === WidgetType.RECORD_TABLE}
|
||||
>
|
||||
{hasAccess ? (
|
||||
<ErrorBoundary
|
||||
FallbackComponent={PageLayoutWidgetInvalidConfigDisplay}
|
||||
resetKeys={[
|
||||
widget.id,
|
||||
widget.configuration,
|
||||
widget.objectMetadataId,
|
||||
]}
|
||||
>
|
||||
<WidgetContentRenderer widget={widget} />
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<StyledNoAccessContainer>
|
||||
<IconLock
|
||||
color={theme.font.color.tertiary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledNoAccessContainer>
|
||||
)}
|
||||
</WidgetCardContent>
|
||||
</WidgetCard>
|
||||
</WidgetComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
|
||||
import { DashboardWidgetPlaceholder } from '@/page-layout/widgets/components/DashboardWidgetPlaceholder';
|
||||
import { StandaloneWidgetPlaceholder } from '@/page-layout/widgets/components/StandaloneWidgetPlaceholder';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const WidgetPlaceholder = () => {
|
||||
@@ -9,6 +10,10 @@ export const WidgetPlaceholder = () => {
|
||||
return <DashboardWidgetPlaceholder />;
|
||||
}
|
||||
|
||||
if (currentPageLayout.type === PageLayoutType.STANDALONE_PAGE) {
|
||||
return <StandaloneWidgetPlaceholder />;
|
||||
}
|
||||
|
||||
// TODO: Implement RecordPageWidgetPlaceholder when needed
|
||||
return <DashboardWidgetPlaceholder />;
|
||||
};
|
||||
|
||||
+8
-235
@@ -1,246 +1,19 @@
|
||||
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
|
||||
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
|
||||
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/pageLayoutResizingWidgetIdComponentState';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { PageLayoutWidgetForbiddenDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetForbiddenDisplay';
|
||||
import { PageLayoutWidgetInvalidConfigDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetInvalidConfigDisplay';
|
||||
import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer';
|
||||
import { useIsCurrentWidgetLastOfTab } from '@/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab';
|
||||
import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab';
|
||||
import { useWidgetActions } from '@/page-layout/widgets/hooks/useWidgetActions';
|
||||
import { useWidgetPermissions } from '@/page-layout/widgets/hooks/useWidgetPermissions';
|
||||
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
|
||||
import { widgetCardHoveredComponentFamilyState } from '@/page-layout/widgets/states/widgetCardHoveredComponentFamilyState';
|
||||
import { getWidgetCardVariant } from '@/page-layout/widgets/utils/getWidgetCardVariant';
|
||||
import { WidgetCard } from '@/page-layout/widgets/widget-card/components/WidgetCard';
|
||||
import { WidgetCardContent } from '@/page-layout/widgets/widget-card/components/WidgetCardContent';
|
||||
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
|
||||
import { useOpenWidgetSettingsInSidePanel } from '@/side-panel/hooks/useOpenWidgetSettingsInSidePanel';
|
||||
import { NonRecordPageWidgetRenderer } from '@/page-layout/widgets/components/NonRecordPageWidgetRenderer';
|
||||
import { RecordPageWidgetRenderer } from '@/page-layout/widgets/components/RecordPageWidgetRenderer';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useSetAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentFamilyState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type MouseEvent, useContext } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { IconLock } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledEditingWidgetWrapper = styled.div`
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledNoAccessContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
type WidgetRendererProps = {
|
||||
widget: PageLayoutWidget;
|
||||
};
|
||||
|
||||
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
|
||||
const { openWidgetSettingsInSidePanel } = useOpenWidgetSettingsInSidePanel();
|
||||
const { layoutType } = useLayoutRenderingContext();
|
||||
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
if (layoutType === PageLayoutType.RECORD_PAGE) {
|
||||
return <RecordPageWidgetRenderer widget={widget} />;
|
||||
}
|
||||
|
||||
const pageLayoutDraggingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutDraggingWidgetIdComponentState,
|
||||
);
|
||||
|
||||
const pageLayoutResizingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutResizingWidgetIdComponentState,
|
||||
);
|
||||
|
||||
const pageLayoutEditingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
);
|
||||
|
||||
const isEditing = pageLayoutEditingWidgetId === widget.id;
|
||||
|
||||
const isDragging = pageLayoutDraggingWidgetId === widget.id;
|
||||
|
||||
const isResizing = pageLayoutResizingWidgetId === widget.id;
|
||||
|
||||
const { hasAccess, restriction } = useWidgetPermissions(widget);
|
||||
|
||||
const { layoutMode } = usePageLayoutContentContext();
|
||||
const { isInPinnedTab } = useIsInPinnedTab();
|
||||
const { isInSidePanel } = useLayoutRenderingContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
|
||||
|
||||
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
);
|
||||
|
||||
const isRecordPageLayout =
|
||||
currentPageLayout.type === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
const isLastWidget = useIsCurrentWidgetLastOfTab(widget.id);
|
||||
|
||||
const isReorderEnabled =
|
||||
!isRecordPageLayout ||
|
||||
(isRecordPageLayout && isRecordPageGlobalEditionEnabled);
|
||||
|
||||
const isDeletingWidgetEnabled =
|
||||
!isRecordPageLayout ||
|
||||
(isRecordPageLayout && isRecordPageGlobalEditionEnabled);
|
||||
|
||||
const isWidgetEditable =
|
||||
isPageLayoutInEditMode &&
|
||||
(!isRecordPageLayout ||
|
||||
(isRecordPageLayout && isRecordPageGlobalEditionEnabled) ||
|
||||
widget.type === WidgetType.FIELDS ||
|
||||
widget.type === WidgetType.FIELD);
|
||||
|
||||
// TODO: when we have more widgets without headers, we should use a more generic approach to hide the header
|
||||
// each widget type could have metadata (e.g., hasHeader: boolean or headerMode: 'always' | 'editOnly' | 'never')
|
||||
const isHeaderHiddenInViewMode =
|
||||
widget.type === WidgetType.STANDALONE_RICH_TEXT ||
|
||||
widget.type === WidgetType.EMAIL_THREAD;
|
||||
const hideHeaderInViewMode =
|
||||
isHeaderHiddenInViewMode && !isPageLayoutInEditMode;
|
||||
|
||||
const showHeader =
|
||||
layoutMode !== PageLayoutTabLayoutMode.CANVAS && !hideHeaderInViewMode;
|
||||
|
||||
const handleClick = () => {
|
||||
openWidgetSettingsInSidePanel({
|
||||
widgetId: widget.id,
|
||||
widgetType: widget.type,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemove = (e?: MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
deletePageLayoutWidget(widget.id);
|
||||
};
|
||||
|
||||
const setWidgetCardHovered = useSetAtomComponentFamilyState(
|
||||
widgetCardHoveredComponentFamilyState,
|
||||
widget.id,
|
||||
);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setWidgetCardHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setWidgetCardHovered(false);
|
||||
};
|
||||
|
||||
const variant = getWidgetCardVariant({
|
||||
layoutMode,
|
||||
isInPinnedTab,
|
||||
pageLayoutType: currentPageLayout.type,
|
||||
isMobile,
|
||||
isInSidePanel,
|
||||
});
|
||||
|
||||
const actions = useWidgetActions({ widget });
|
||||
|
||||
// TODO: remove once all record page layouts widgets use the editable contain in edit mode
|
||||
const shouldWrapWithEditingWrapper =
|
||||
isWidgetEditable &&
|
||||
variant === 'side-column' &&
|
||||
!isRecordPageGlobalEditionEnabled;
|
||||
|
||||
const isCanvasVariant = variant === 'canvas';
|
||||
|
||||
const widgetCard = (
|
||||
<WidgetCard
|
||||
headerLess={!showHeader}
|
||||
variant={variant}
|
||||
isEditable={isWidgetEditable}
|
||||
onClick={isWidgetEditable ? handleClick : undefined}
|
||||
isEditing={isEditing}
|
||||
isDragging={isDragging}
|
||||
isResizing={isResizing}
|
||||
isLastWidget={isLastWidget}
|
||||
onMouseEnter={isCanvasVariant ? undefined : handleMouseEnter}
|
||||
onMouseLeave={isCanvasVariant ? undefined : handleMouseLeave}
|
||||
data-widget-id={widget.id}
|
||||
data-testid={widget.id}
|
||||
className="widget"
|
||||
>
|
||||
{showHeader && (
|
||||
<WidgetCardHeader
|
||||
widgetId={widget.id}
|
||||
variant={variant}
|
||||
isInEditMode={isWidgetEditable}
|
||||
isResizing={isResizing}
|
||||
isReorderEnabled={isReorderEnabled}
|
||||
isDeletingWidgetEnabled={isDeletingWidgetEnabled}
|
||||
title={widget.title}
|
||||
onRemove={handleRemove}
|
||||
actions={actions}
|
||||
forbiddenDisplay={
|
||||
!hasAccess && (
|
||||
<PageLayoutWidgetForbiddenDisplay
|
||||
widgetId={widget.id}
|
||||
restriction={restriction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WidgetCardContent
|
||||
variant={variant}
|
||||
hasHeader={showHeader}
|
||||
isEditable={isWidgetEditable}
|
||||
isInVerticalListTab={
|
||||
layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
|
||||
}
|
||||
isMobile={isMobile}
|
||||
hasInteractiveContent={widget.type === WidgetType.RECORD_TABLE}
|
||||
>
|
||||
{hasAccess ? (
|
||||
<ErrorBoundary
|
||||
FallbackComponent={PageLayoutWidgetInvalidConfigDisplay}
|
||||
resetKeys={[
|
||||
widget.id,
|
||||
widget.configuration,
|
||||
widget.objectMetadataId,
|
||||
]}
|
||||
>
|
||||
<WidgetContentRenderer widget={widget} />
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<StyledNoAccessContainer>
|
||||
<IconLock
|
||||
color={theme.font.color.tertiary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledNoAccessContainer>
|
||||
)}
|
||||
</WidgetCardContent>
|
||||
</WidgetCard>
|
||||
);
|
||||
|
||||
return (
|
||||
<WidgetComponentInstanceContext.Provider value={{ instanceId: widget.id }}>
|
||||
{shouldWrapWithEditingWrapper ? (
|
||||
<StyledEditingWidgetWrapper>{widgetCard}</StyledEditingWidgetWrapper>
|
||||
) : (
|
||||
widgetCard
|
||||
)}
|
||||
</WidgetComponentInstanceContext.Provider>
|
||||
);
|
||||
return <NonRecordPageWidgetRenderer widget={widget} />;
|
||||
};
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
|
||||
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
|
||||
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/pageLayoutResizingWidgetIdComponentState';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { useIsCurrentWidgetLastOfTab } from '@/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab';
|
||||
import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab';
|
||||
import { useWidgetPermissions } from '@/page-layout/widgets/hooks/useWidgetPermissions';
|
||||
import { widgetCardHoveredComponentFamilyState } from '@/page-layout/widgets/states/widgetCardHoveredComponentFamilyState';
|
||||
import { getWidgetCardVariant } from '@/page-layout/widgets/utils/getWidgetCardVariant';
|
||||
import { useOpenWidgetSettingsInSidePanel } from '@/side-panel/hooks/useOpenWidgetSettingsInSidePanel';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useSetAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentFamilyState';
|
||||
import { type MouseEvent } from 'react';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useWidgetRendererState = (widget: PageLayoutWidget) => {
|
||||
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
|
||||
const { openWidgetSettingsInSidePanel } = useOpenWidgetSettingsInSidePanel();
|
||||
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
|
||||
const pageLayoutDraggingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutDraggingWidgetIdComponentState,
|
||||
);
|
||||
|
||||
const pageLayoutResizingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutResizingWidgetIdComponentState,
|
||||
);
|
||||
|
||||
const pageLayoutEditingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
);
|
||||
|
||||
const isEditing = pageLayoutEditingWidgetId === widget.id;
|
||||
const isDragging = pageLayoutDraggingWidgetId === widget.id;
|
||||
const isResizing = pageLayoutResizingWidgetId === widget.id;
|
||||
|
||||
const { hasAccess, restriction } = useWidgetPermissions(widget);
|
||||
|
||||
const { layoutMode } = usePageLayoutContentContext();
|
||||
const { isInPinnedTab } = useIsInPinnedTab();
|
||||
const { isInSidePanel } = useLayoutRenderingContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
|
||||
|
||||
const isLastWidget = useIsCurrentWidgetLastOfTab(widget.id);
|
||||
|
||||
const isHeaderHiddenInViewMode =
|
||||
widget.type === WidgetType.STANDALONE_RICH_TEXT ||
|
||||
widget.type === WidgetType.EMAIL_THREAD;
|
||||
const hideHeaderInViewMode =
|
||||
isHeaderHiddenInViewMode && !isPageLayoutInEditMode;
|
||||
|
||||
const showHeader =
|
||||
layoutMode !== PageLayoutTabLayoutMode.CANVAS && !hideHeaderInViewMode;
|
||||
|
||||
const handleClick = () => {
|
||||
openWidgetSettingsInSidePanel({
|
||||
widgetId: widget.id,
|
||||
widgetType: widget.type,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemove = (e?: MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
deletePageLayoutWidget(widget.id);
|
||||
};
|
||||
|
||||
const setWidgetCardHovered = useSetAtomComponentFamilyState(
|
||||
widgetCardHoveredComponentFamilyState,
|
||||
widget.id,
|
||||
);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setWidgetCardHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setWidgetCardHovered(false);
|
||||
};
|
||||
|
||||
const variant = getWidgetCardVariant({
|
||||
layoutMode,
|
||||
isInPinnedTab,
|
||||
pageLayoutType: currentPageLayout.type,
|
||||
isMobile,
|
||||
isInSidePanel,
|
||||
});
|
||||
|
||||
const isInVerticalListTab =
|
||||
layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST;
|
||||
|
||||
return {
|
||||
isPageLayoutInEditMode,
|
||||
isEditing,
|
||||
isDragging,
|
||||
isResizing,
|
||||
hasAccess,
|
||||
restriction,
|
||||
currentPageLayout,
|
||||
isLastWidget,
|
||||
showHeader,
|
||||
variant,
|
||||
isMobile,
|
||||
isInVerticalListTab,
|
||||
handleClick,
|
||||
handleRemove,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
};
|
||||
};
|
||||
@@ -2,4 +2,5 @@ export type WidgetCardVariant =
|
||||
| 'canvas'
|
||||
| 'side-column'
|
||||
| 'dashboard'
|
||||
| 'standalone'
|
||||
| 'record-page';
|
||||
|
||||
+24
@@ -17,6 +17,30 @@ describe('getWidgetCardVariant', () => {
|
||||
).toBe('dashboard');
|
||||
});
|
||||
|
||||
it('should return standalone for STANDALONE_PAGE page layout type', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
isInPinnedTab: false,
|
||||
pageLayoutType: PageLayoutType.STANDALONE_PAGE,
|
||||
isMobile: false,
|
||||
isInSidePanel: false,
|
||||
}),
|
||||
).toBe('standalone');
|
||||
});
|
||||
|
||||
it('should prioritize standalone over canvas', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
isInPinnedTab: false,
|
||||
pageLayoutType: PageLayoutType.STANDALONE_PAGE,
|
||||
isMobile: false,
|
||||
isInSidePanel: false,
|
||||
}),
|
||||
).toBe('standalone');
|
||||
});
|
||||
|
||||
it('should return canvas for CANVAS layout mode', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
|
||||
@@ -23,6 +23,10 @@ export const getWidgetCardVariant = ({
|
||||
return 'dashboard';
|
||||
}
|
||||
|
||||
if (pageLayoutType === PageLayoutType.STANDALONE_PAGE) {
|
||||
return 'standalone';
|
||||
}
|
||||
|
||||
if (layoutMode === PageLayoutTabLayoutMode.CANVAS) {
|
||||
return 'canvas';
|
||||
}
|
||||
|
||||
+14
-4
@@ -23,7 +23,7 @@ const computeBorderColor = (
|
||||
if (props.isEditable && (props.isEditing || props.isDragging)) {
|
||||
return themeCssVariables.color.blue;
|
||||
}
|
||||
if (props.variant === 'dashboard') {
|
||||
if (props.variant === 'dashboard' || props.variant === 'standalone') {
|
||||
return themeCssVariables.border.color.light;
|
||||
}
|
||||
return 'transparent';
|
||||
@@ -37,6 +37,7 @@ const StyledWidgetCard = styled.div<WidgetCardStyledProps>`
|
||||
}
|
||||
if (
|
||||
props.variant === 'dashboard' ||
|
||||
props.variant === 'standalone' ||
|
||||
(props.variant === 'side-column' && props.isEditable)
|
||||
) {
|
||||
return themeCssVariables.background.secondary;
|
||||
@@ -49,6 +50,7 @@ const StyledWidgetCard = styled.div<WidgetCardStyledProps>`
|
||||
|
||||
border: ${(props) =>
|
||||
props.variant === 'dashboard' ||
|
||||
props.variant === 'standalone' ||
|
||||
props.variant === 'record-page' ||
|
||||
props.isEditable
|
||||
? `1px solid ${computeBorderColor(props)}`
|
||||
@@ -66,7 +68,10 @@ const StyledWidgetCard = styled.div<WidgetCardStyledProps>`
|
||||
return `1px solid ${computeBorderColor(props)}`;
|
||||
}};
|
||||
border-radius: ${({ variant, isEditable }) =>
|
||||
variant === 'dashboard' || variant === 'record-page' || isEditable
|
||||
variant === 'dashboard' ||
|
||||
variant === 'standalone' ||
|
||||
variant === 'record-page' ||
|
||||
isEditable
|
||||
? themeCssVariables.border.radius.md
|
||||
: '0'};
|
||||
|
||||
@@ -90,8 +95,13 @@ const StyledWidgetCard = styled.div<WidgetCardStyledProps>`
|
||||
height: 100%;
|
||||
|
||||
padding: ${({ variant, isEditable, headerLess }) => {
|
||||
if (variant === 'dashboard' && headerLess === true) return '0';
|
||||
if (variant === 'dashboard') return themeCssVariables.spacing[2];
|
||||
if (
|
||||
(variant === 'dashboard' || variant === 'standalone') &&
|
||||
headerLess === true
|
||||
)
|
||||
return '0';
|
||||
if (variant === 'dashboard' || variant === 'standalone')
|
||||
return themeCssVariables.spacing[2];
|
||||
if (variant === 'side-column' && !isEditable)
|
||||
return themeCssVariables.spacing[3];
|
||||
if (variant === 'record-page' || isEditable)
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@ const StyledWidgetCardContent = styled.div<WidgetCardContentStyledProps>`
|
||||
overflow: hidden;
|
||||
|
||||
padding: ${({ variant, isEditable }) => {
|
||||
if (variant === 'dashboard') return themeCssVariables.spacing[2];
|
||||
if (variant === 'dashboard' || variant === 'standalone')
|
||||
return themeCssVariables.spacing[2];
|
||||
if (
|
||||
variant === 'record-page' ||
|
||||
(variant === 'side-column' && isEditable)
|
||||
|
||||
+13
-13
@@ -1,20 +1,20 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentPageTypeComponentState } from '@/context-store/states/contextStoreCurrentPageTypeComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
|
||||
import { viewableRecordIdComponentState } from '@/side-panel/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { viewableRecordNameSingularComponentState } from '@/side-panel/pages/record-page/states/viewableRecordNameSingularComponentState';
|
||||
import { sidePanelNavigationMorphItemsByPageState } from '@/side-panel/states/sidePanelNavigationMorphItemsByPageState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { ContextStorePageType, SidePanelPages } from 'twenty-shared/types';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndCommandMenuWrapper';
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
@@ -85,8 +85,8 @@ const renderHooks = () => {
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const contextStoreCurrentViewType = useAtomComponentStateValue(
|
||||
contextStoreCurrentViewTypeComponentState,
|
||||
const contextStoreCurrentPageType = useAtomComponentStateValue(
|
||||
contextStoreCurrentPageTypeComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const { getIcon } = useIcons();
|
||||
@@ -98,7 +98,7 @@ const renderHooks = () => {
|
||||
contextStoreCurrentObjectMetadataItemId,
|
||||
contextStoreTargetedRecordsRule,
|
||||
contextStoreNumberOfSelectedRecords,
|
||||
contextStoreCurrentViewType,
|
||||
contextStoreCurrentPageType,
|
||||
getIcon,
|
||||
};
|
||||
},
|
||||
@@ -137,8 +137,8 @@ describe('useOpenRecordInSidePanel', () => {
|
||||
selectedRecordIds: [recordId],
|
||||
});
|
||||
expect(result.current.contextStoreNumberOfSelectedRecords).toBe(1);
|
||||
expect(result.current.contextStoreCurrentViewType).toBe(
|
||||
ContextStoreViewType.ShowPage,
|
||||
expect(result.current.contextStoreCurrentPageType).toBe(
|
||||
ContextStorePageType.Record,
|
||||
);
|
||||
|
||||
const sidePanelNavigationMorphItemsByPage = jotaiStore.get(
|
||||
|
||||
@@ -6,17 +6,20 @@ import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavi
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreCurrentPageTypeComponentState } from '@/context-store/states/contextStoreCurrentPageTypeComponentState';
|
||||
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { viewableRecordIdState } from '@/object-record/record-side-panel/states/viewableRecordIdState';
|
||||
import { useOpenNewRecordTitleCell } from '@/object-record/record-title-cell/hooks/useOpenNewRecordTitleCell';
|
||||
import { CoreObjectNameSingular, SidePanelPages } from 'twenty-shared/types';
|
||||
import {
|
||||
ContextStorePageType,
|
||||
CoreObjectNameSingular,
|
||||
SidePanelPages,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { useRunWorkflowRunOpeningInSidePanelEffects } from '@/workflow/hooks/useRunWorkflowRunOpeningInSidePanelEffects';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -117,10 +120,10 @@ export const useOpenRecordInSidePanel = () => {
|
||||
);
|
||||
|
||||
store.set(
|
||||
contextStoreCurrentViewTypeComponentState.atomFamily({
|
||||
contextStoreCurrentPageTypeComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
ContextStoreViewType.ShowPage,
|
||||
ContextStorePageType.Record,
|
||||
);
|
||||
|
||||
store.set(
|
||||
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { CommandMenuItemRenderer } from '@/command-menu-item/display/components/CommandMenuItemRenderer';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { useFilterCommandMenuItemsWithSidePanelSearch } from '@/side-panel/pages/root/hooks/useFilterCommandMenuItemsWithSidePanelSearch';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const SidePanelRootPage = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const sidePanelSearch = useAtomStateValue(sidePanelSearchState);
|
||||
const { commandMenuItems, commandMenuContextApi } =
|
||||
useContext(CommandMenuContext);
|
||||
|
||||
const { filterCommandMenuItemsWithSidePanelSearch } =
|
||||
useFilterCommandMenuItemsWithSidePanelSearch({
|
||||
sidePanelSearch,
|
||||
commandMenuContextApi,
|
||||
});
|
||||
|
||||
const recordSelectionItems = useMemo(
|
||||
() =>
|
||||
commandMenuItems.filter(
|
||||
(item) =>
|
||||
item.availabilityType ===
|
||||
CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
),
|
||||
[commandMenuItems],
|
||||
);
|
||||
|
||||
const globalItems = useMemo(
|
||||
() =>
|
||||
commandMenuItems.filter(
|
||||
(item) =>
|
||||
item.availabilityType === CommandMenuItemAvailabilityType.GLOBAL,
|
||||
),
|
||||
[commandMenuItems],
|
||||
);
|
||||
|
||||
const fallbackItems = useMemo(
|
||||
() =>
|
||||
commandMenuItems.filter(
|
||||
(item) =>
|
||||
item.availabilityType === CommandMenuItemAvailabilityType.FALLBACK,
|
||||
),
|
||||
[commandMenuItems],
|
||||
);
|
||||
|
||||
const matchingRecordSelectionItems =
|
||||
filterCommandMenuItemsWithSidePanelSearch(recordSelectionItems);
|
||||
const matchingGlobalItems =
|
||||
filterCommandMenuItemsWithSidePanelSearch(globalItems);
|
||||
|
||||
const noResults =
|
||||
!matchingRecordSelectionItems.length && !matchingGlobalItems.length;
|
||||
|
||||
const selectableItemIds = [
|
||||
...matchingRecordSelectionItems,
|
||||
...matchingGlobalItems,
|
||||
...(noResults ? fallbackItems : []),
|
||||
].map((item) => item.id);
|
||||
|
||||
return (
|
||||
<SidePanelList selectableItemIds={selectableItemIds} noResults={noResults}>
|
||||
{matchingRecordSelectionItems.length > 0 && (
|
||||
<SidePanelGroup heading={t`Record Selection`}>
|
||||
{matchingRecordSelectionItems.map((item) => (
|
||||
<CommandMenuItemRenderer item={item} key={item.id} />
|
||||
))}
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
{matchingGlobalItems.length > 0 && (
|
||||
<SidePanelGroup heading={t`Global`}>
|
||||
{matchingGlobalItems.map((item) => (
|
||||
<CommandMenuItemRenderer item={item} key={item.id} />
|
||||
))}
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
{noResults && fallbackItems.length > 0 && (
|
||||
<SidePanelGroup heading={t`Search ''${sidePanelSearch}'' with...`}>
|
||||
{fallbackItems.map((item) => (
|
||||
<CommandMenuItemRenderer item={item} key={item.id} />
|
||||
))}
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
</SidePanelList>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { StandalonePageCommandMenu } from '@/command-menu-item/components/StandalonePageCommandMenu';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
|
||||
import { SidePanelToggleButton } from '@/side-panel/components/SidePanelToggleButton';
|
||||
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
type StandalonePageHeaderProps = {
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
export const StandalonePageHeader = ({
|
||||
pageLayoutId,
|
||||
}: StandalonePageHeaderProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
|
||||
const navigationMenuItem = navigationMenuItems.find(
|
||||
(item) => item.pageLayoutId === pageLayoutId,
|
||||
);
|
||||
|
||||
const title = navigationMenuItem?.name ?? '';
|
||||
const Icon = isDefined(navigationMenuItem?.icon)
|
||||
? getIcon(navigationMenuItem.icon)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<PageHeader title={title} Icon={Icon}>
|
||||
<StandalonePageCommandMenu />
|
||||
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
|
||||
</PageHeader>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
|
||||
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
import { StandalonePageHeader } from '~/pages/page-layout/StandalonePageHeader';
|
||||
|
||||
export const StandalonePageLayoutPage = () => {
|
||||
const { pageLayoutId } = useParams<{ pageLayoutId: string }>();
|
||||
|
||||
if (!isDefined(pageLayoutId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
|
||||
>
|
||||
<CommandMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: pageLayoutId }}
|
||||
>
|
||||
<StandalonePageHeader pageLayoutId={pageLayoutId} />
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier: undefined,
|
||||
layoutType: PageLayoutType.STANDALONE_PAGE,
|
||||
isInSidePanel: false,
|
||||
}}
|
||||
>
|
||||
<MainContainerLayoutWithSidePanel>
|
||||
<PageLayoutRenderer pageLayoutId={pageLayoutId} />
|
||||
</MainContainerLayoutWithSidePanel>
|
||||
</LayoutRenderingProvider>
|
||||
</CommandMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type PropsWithChildren, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentPageTypeComponentState } from '@/context-store/states/contextStoreCurrentPageTypeComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
type ContextStoreTargetedRecordsRule,
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
} from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { type ContextStorePageType } from 'twenty-shared/types';
|
||||
import { type ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
|
||||
@@ -26,6 +28,7 @@ export type JestContextStoreSetterMocks = {
|
||||
contextStoreCurrentObjectMetadataNameSingular?: string;
|
||||
contextStoreCurrentViewId?: string;
|
||||
contextStoreCurrentViewType?: ContextStoreViewType;
|
||||
contextStoreCurrentPageType?: ContextStorePageType;
|
||||
};
|
||||
|
||||
type JestContextStoreSetterProps =
|
||||
@@ -41,6 +44,7 @@ export const JestContextStoreSetter = ({
|
||||
contextStoreFilters = [],
|
||||
contextStoreFilterGroups = [],
|
||||
contextStoreCurrentViewType,
|
||||
contextStoreCurrentPageType,
|
||||
children,
|
||||
}: JestContextStoreSetterProps) => {
|
||||
const contextStoreInstanceContext = useContext(
|
||||
@@ -84,6 +88,11 @@ export const JestContextStoreSetter = ({
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const setContextStoreCurrentPageType = useSetAtomComponentState(
|
||||
contextStoreCurrentPageTypeComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: contextStoreCurrentObjectMetadataNameSingular,
|
||||
});
|
||||
@@ -99,6 +108,7 @@ export const JestContextStoreSetter = ({
|
||||
setContextStoreFilters(contextStoreFilters);
|
||||
setContextStoreFilterGroups(contextStoreFilterGroups);
|
||||
setContextStoreCurrentViewType(contextStoreCurrentViewType ?? null);
|
||||
setContextStoreCurrentPageType(contextStoreCurrentPageType ?? null);
|
||||
setIsLoaded(true);
|
||||
}, [
|
||||
setContextStoreTargetedRecordsRule,
|
||||
@@ -114,6 +124,8 @@ export const JestContextStoreSetter = ({
|
||||
contextStoreCurrentViewId,
|
||||
setContextStoreCurrentViewType,
|
||||
contextStoreCurrentViewType,
|
||||
setContextStoreCurrentPageType,
|
||||
contextStoreCurrentPageType,
|
||||
setContextStoreFilterGroups,
|
||||
contextStoreFilterGroups,
|
||||
]);
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ export const getJestMetadataAndApolloMocksAndCommandMenuWrapper = ({
|
||||
contextStoreTargetedRecordsRule,
|
||||
contextStoreCurrentViewId,
|
||||
contextStoreCurrentViewType,
|
||||
contextStoreCurrentPageType,
|
||||
contextStoreNumberOfSelectedRecords,
|
||||
contextStoreCurrentObjectMetadataNameSingular,
|
||||
contextStoreFilters,
|
||||
@@ -70,6 +71,7 @@ export const getJestMetadataAndApolloMocksAndCommandMenuWrapper = ({
|
||||
contextStoreCurrentObjectMetadataNameSingular
|
||||
}
|
||||
contextStoreCurrentViewType={contextStoreCurrentViewType}
|
||||
contextStoreCurrentPageType={contextStoreCurrentPageType}
|
||||
>
|
||||
{children}
|
||||
</JestContextStoreSetter>
|
||||
|
||||
+10
-10
@@ -3,7 +3,7 @@ import * as path from 'path';
|
||||
|
||||
import { transformConditionalAvailabilityExpressionsForEsBuildPlugin } from '@/cli/utilities/build/common/conditional-availability/utils/transform-conditional-availability-expressions';
|
||||
import {
|
||||
CommandMenuContextApiPageType,
|
||||
ContextStorePageType,
|
||||
type CommandMenuContextApi,
|
||||
} from 'twenty-shared/types';
|
||||
import { evaluateConditionalAvailabilityExpression } from 'twenty-shared/utils';
|
||||
@@ -16,7 +16,7 @@ const readMock = (filename: string): string =>
|
||||
const buildMockCommandMenuContextApi = (
|
||||
overrides: Partial<CommandMenuContextApi> = {},
|
||||
): CommandMenuContextApi => ({
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
pageType: ContextStorePageType.Index,
|
||||
isInSidePanel: false,
|
||||
isPageInEditMode: false,
|
||||
favoriteRecordIds: [],
|
||||
@@ -190,7 +190,7 @@ describe('transformConditionalAvailabilityExpressionsForEsBuildPlugin', () => {
|
||||
describe('simple-boolean-front-component', () => {
|
||||
it('should evaluate pageType when RECORD_PAGE', () => {
|
||||
const context = buildMockCommandMenuContextApi({
|
||||
pageType: CommandMenuContextApiPageType.RECORD_PAGE,
|
||||
pageType: ContextStorePageType.Record,
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -203,7 +203,7 @@ describe('transformConditionalAvailabilityExpressionsForEsBuildPlugin', () => {
|
||||
|
||||
it('should evaluate pageType when INDEX_PAGE', () => {
|
||||
const context = buildMockCommandMenuContextApi({
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
pageType: ContextStorePageType.Index,
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -348,7 +348,7 @@ describe('transformConditionalAvailabilityExpressionsForEsBuildPlugin', () => {
|
||||
describe('parenthesized-expression-front-component', () => {
|
||||
it('should allow when favorite and not remote', () => {
|
||||
const context = buildMockCommandMenuContextApi({
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
pageType: ContextStorePageType.Index,
|
||||
favoriteRecordIds: ['rec-1'],
|
||||
objectMetadataItem: { isRemote: false },
|
||||
});
|
||||
@@ -363,7 +363,7 @@ describe('transformConditionalAvailabilityExpressionsForEsBuildPlugin', () => {
|
||||
|
||||
it('should deny when remote even if record page', () => {
|
||||
const context = buildMockCommandMenuContextApi({
|
||||
pageType: CommandMenuContextApiPageType.RECORD_PAGE,
|
||||
pageType: ContextStorePageType.Record,
|
||||
favoriteRecordIds: [],
|
||||
objectMetadataItem: { isRemote: true },
|
||||
});
|
||||
@@ -422,7 +422,7 @@ describe('transformConditionalAvailabilityExpressionsForEsBuildPlugin', () => {
|
||||
describe('string-comparison-front-component', () => {
|
||||
it('should match when on record page and company name matches', () => {
|
||||
const context = buildMockCommandMenuContextApi({
|
||||
pageType: CommandMenuContextApiPageType.RECORD_PAGE,
|
||||
pageType: ContextStorePageType.Record,
|
||||
selectedRecords: [
|
||||
{
|
||||
id: 'rec-1',
|
||||
@@ -444,7 +444,7 @@ describe('transformConditionalAvailabilityExpressionsForEsBuildPlugin', () => {
|
||||
|
||||
it('should not match when company name differs', () => {
|
||||
const context = buildMockCommandMenuContextApi({
|
||||
pageType: CommandMenuContextApiPageType.RECORD_PAGE,
|
||||
pageType: ContextStorePageType.Record,
|
||||
selectedRecords: [
|
||||
{
|
||||
id: 'rec-1',
|
||||
@@ -468,7 +468,7 @@ describe('transformConditionalAvailabilityExpressionsForEsBuildPlugin', () => {
|
||||
describe('target-permissions-front-component', () => {
|
||||
it('should allow when on record page with write permission', () => {
|
||||
const context = buildMockCommandMenuContextApi({
|
||||
pageType: CommandMenuContextApiPageType.RECORD_PAGE,
|
||||
pageType: ContextStorePageType.Record,
|
||||
targetObjectWritePermissions: { person: true },
|
||||
});
|
||||
|
||||
@@ -482,7 +482,7 @@ describe('transformConditionalAvailabilityExpressionsForEsBuildPlugin', () => {
|
||||
|
||||
it('should deny when not on record page', () => {
|
||||
const context = buildMockCommandMenuContextApi({
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
pageType: ContextStorePageType.Index,
|
||||
targetObjectWritePermissions: { person: true },
|
||||
});
|
||||
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.23.0', 1775752781995)
|
||||
export class AddStandalonePageFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" ADD "pageLayoutId" uuid',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TYPE "core"."pageLayout_type_enum" RENAME TO "pageLayout_type_enum_old"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
"CREATE TYPE \"core\".\"pageLayout_type_enum\" AS ENUM('RECORD_INDEX', 'RECORD_PAGE', 'DASHBOARD', 'STANDALONE_PAGE')",
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."pageLayout" ALTER COLUMN "type" DROP DEFAULT',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."pageLayout" ALTER COLUMN "type" TYPE "core"."pageLayout_type_enum" USING "type"::"text"::"core"."pageLayout_type_enum"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."pageLayout" ALTER COLUMN "type" SET DEFAULT \'RECORD_PAGE\'',
|
||||
);
|
||||
await queryRunner.query('DROP TYPE "core"."pageLayout_type_enum_old"');
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT IF EXISTS "CHK_navigation_menu_item_type_fields"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TYPE "core"."navigationMenuItem_type_enum" RENAME TO "navigationMenuItem_type_enum_old"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
"CREATE TYPE \"core\".\"navigationMenuItem_type_enum\" AS ENUM('VIEW', 'FOLDER', 'LINK', 'OBJECT', 'RECORD', 'PAGE_LAYOUT')",
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "type" TYPE "core"."navigationMenuItem_type_enum" USING "type"::"text"::"core"."navigationMenuItem_type_enum"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'DROP TYPE "core"."navigationMenuItem_type_enum_old"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "CHK_navigation_menu_item_type_fields" CHECK (("type" = 'FOLDER') OR ("type" = 'OBJECT' AND "targetObjectMetadataId" IS NOT NULL) OR ("type" = 'VIEW' AND "viewId" IS NOT NULL) OR ("type" = 'RECORD' AND "targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL) OR ("type" = 'LINK' AND "link" IS NOT NULL) OR ("type" = 'PAGE_LAYOUT' AND "pageLayoutId" IS NOT NULL))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX "IDX_NAVIGATION_MENU_ITEM_PAGE_LAYOUT_ID_WORKSPACE_ID" ON "core"."navigationMenuItem" ("pageLayoutId", "workspaceId") ',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "FK_4ba3e5e988c4c5f159ec8753ee3" FOREIGN KEY ("pageLayoutId") REFERENCES "core"."pageLayout"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT "FK_4ba3e5e988c4c5f159ec8753ee3"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'DROP INDEX "core"."IDX_NAVIGATION_MENU_ITEM_PAGE_LAYOUT_ID_WORKSPACE_ID"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT IF EXISTS "CHK_navigation_menu_item_type_fields"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'DELETE FROM "core"."navigationMenuItem" WHERE "type" = \'PAGE_LAYOUT\'',
|
||||
);
|
||||
await queryRunner.query(
|
||||
"CREATE TYPE \"core\".\"navigationMenuItem_type_enum_old\" AS ENUM('FOLDER', 'LINK', 'OBJECT', 'RECORD', 'VIEW')",
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "type" TYPE "core"."navigationMenuItem_type_enum_old" USING "type"::"text"::"core"."navigationMenuItem_type_enum_old"',
|
||||
);
|
||||
await queryRunner.query('DROP TYPE "core"."navigationMenuItem_type_enum"');
|
||||
await queryRunner.query(
|
||||
'ALTER TYPE "core"."navigationMenuItem_type_enum_old" RENAME TO "navigationMenuItem_type_enum"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "CHK_navigation_menu_item_type_fields" CHECK (("type" = 'FOLDER') OR ("type" = 'OBJECT' AND "targetObjectMetadataId" IS NOT NULL) OR ("type" = 'VIEW' AND "viewId" IS NOT NULL) OR ("type" = 'RECORD' AND "targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL) OR ("type" = 'LINK' AND "link" IS NOT NULL))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
'DELETE FROM "core"."pageLayout" WHERE "type" = \'STANDALONE_PAGE\'',
|
||||
);
|
||||
await queryRunner.query(
|
||||
"CREATE TYPE \"core\".\"pageLayout_type_enum_old\" AS ENUM('DASHBOARD', 'RECORD_INDEX', 'RECORD_PAGE')",
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."pageLayout" ALTER COLUMN "type" DROP DEFAULT',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."pageLayout" ALTER COLUMN "type" TYPE "core"."pageLayout_type_enum_old" USING "type"::"text"::"core"."pageLayout_type_enum_old"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."pageLayout" ALTER COLUMN "type" SET DEFAULT \'RECORD_PAGE\'',
|
||||
);
|
||||
await queryRunner.query('DROP TYPE "core"."pageLayout_type_enum"');
|
||||
await queryRunner.query(
|
||||
'ALTER TYPE "core"."pageLayout_type_enum_old" RENAME TO "pageLayout_type_enum"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."navigationMenuItem" DROP COLUMN "pageLayoutId"',
|
||||
);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.23.0', 1776090711153)
|
||||
export class AddGlobalObjectContextToCommandMenuItemAvailabilityTypeFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TYPE "core"."commandMenuItem_availabilitytype_enum" RENAME TO "commandMenuItem_availabilitytype_enum_old"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
"CREATE TYPE \"core\".\"commandMenuItem_availabilitytype_enum\" AS ENUM('GLOBAL', 'GLOBAL_OBJECT_CONTEXT', 'RECORD_SELECTION', 'FALLBACK')",
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "availabilityType" DROP DEFAULT',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "availabilityType" TYPE "core"."commandMenuItem_availabilitytype_enum" USING "availabilityType"::"text"::"core"."commandMenuItem_availabilitytype_enum"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "availabilityType" SET DEFAULT \'GLOBAL\'',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'DROP TYPE "core"."commandMenuItem_availabilitytype_enum_old"',
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
"CREATE TYPE \"core\".\"commandMenuItem_availabilitytype_enum_old\" AS ENUM('FALLBACK', 'GLOBAL', 'RECORD_SELECTION')",
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "availabilityType" DROP DEFAULT',
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "core"."commandMenuItem" SET "availabilityType" = 'GLOBAL' WHERE "availabilityType" = 'GLOBAL_OBJECT_CONTEXT'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "availabilityType" TYPE "core"."commandMenuItem_availabilitytype_enum_old" USING "availabilityType"::"text"::"core"."commandMenuItem_availabilitytype_enum_old"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "availabilityType" SET DEFAULT \'GLOBAL\'',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'DROP TYPE "core"."commandMenuItem_availabilitytype_enum"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TYPE "core"."commandMenuItem_availabilitytype_enum_old" RENAME TO "commandMenuItem_availabilitytype_enum"',
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-workspace-command-1780000001000-backfill-page-layouts-and-fields-widget-view-fields.command';
|
||||
import { UpdateGlobalObjectContextCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-workspace-command-1780000005000-update-global-object-context-command-menu-items.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
@@ -15,6 +16,9 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
WorkspaceIteratorModule,
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
providers: [BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand],
|
||||
providers: [
|
||||
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
UpdateGlobalObjectContextCommandMenuItemsCommand,
|
||||
],
|
||||
})
|
||||
export class V1_23_UpgradeVersionCommandModule {}
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_COMMAND_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const UNIVERSAL_IDENTIFIERS_TO_FIX = new Set<string>([
|
||||
STANDARD_COMMAND_MENU_ITEMS.createNewRecord.universalIdentifier,
|
||||
STANDARD_COMMAND_MENU_ITEMS.importRecords.universalIdentifier,
|
||||
STANDARD_COMMAND_MENU_ITEMS.exportView.universalIdentifier,
|
||||
STANDARD_COMMAND_MENU_ITEMS.seeDeletedRecords.universalIdentifier,
|
||||
STANDARD_COMMAND_MENU_ITEMS.createNewView.universalIdentifier,
|
||||
STANDARD_COMMAND_MENU_ITEMS.hideDeletedRecords.universalIdentifier,
|
||||
]);
|
||||
|
||||
@RegisteredWorkspaceCommand('1.23.0', 1780000005000)
|
||||
@Command({
|
||||
name: 'upgrade:1-23:update-global-object-context-command-menu-items',
|
||||
description:
|
||||
'Update command menu items that require object context from GLOBAL to GLOBAL_OBJECT_CONTEXT',
|
||||
})
|
||||
export class UpdateGlobalObjectContextCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting GLOBAL_OBJECT_CONTEXT availability type update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatCommandMenuItemMaps',
|
||||
]);
|
||||
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
shouldIncludeRecordPageLayouts: true,
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const itemsToUpdate = [...UNIVERSAL_IDENTIFIERS_TO_FIX]
|
||||
.map((universalIdentifier) => {
|
||||
const standardItem =
|
||||
standardAllFlatEntityMaps.flatCommandMenuItemMaps
|
||||
.byUniversalIdentifier[universalIdentifier];
|
||||
const existingItem =
|
||||
existingFlatCommandMenuItemMaps.byUniversalIdentifier[
|
||||
universalIdentifier
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(standardItem) ||
|
||||
!isDefined(existingItem) ||
|
||||
existingItem.availabilityType === standardItem.availabilityType
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingItem,
|
||||
availabilityType: standardItem.availabilityType,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (itemsToUpdate.length === 0) {
|
||||
this.logger.log(
|
||||
`Command menu item availability types already up to date for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${itemsToUpdate.length} command menu item(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would update ${itemsToUpdate.length} command menu item availability type(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: itemsToUpdate,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to update command menu item availability types:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to update command menu item availability types for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully updated ${itemsToUpdate.length} command menu item availability type(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -3,6 +3,7 @@
|
||||
import { AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-instance-command-fast-1775129420309-add-view-field-group-id-index-on-view-field';
|
||||
import { MigrateMessagingCalendarToCoreFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-instance-command-fast-1775165049548-migrate-messaging-calendar-to-core';
|
||||
import { AddEmailThreadWidgetTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-instance-command-fast-1775200000000-add-email-thread-widget-type';
|
||||
import { AddStandalonePageFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1775752781995-add-standalone-page';
|
||||
import { AddPermissionFlagRoleIdIndexFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1775749486425-add-permission-flag-role-id-index';
|
||||
import { AddWorkspaceIdToIndirectEntitiesFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1775758621017-add-workspace-id-to-indirect-entities';
|
||||
import { AddWorkspaceIdIndexesAndFksFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1775761294897-add-workspace-id-indexes-and-fks-to-indirect-entities';
|
||||
@@ -10,11 +11,13 @@ import { DropObjectMetadataDataSourceFkFastInstanceCommand } from 'src/database/
|
||||
import { AddCreditBalanceToBillingCustomerFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1776078919203-add-credit-balance-to-billing-customer';
|
||||
import { BackfillWorkspaceIdOnIndirectEntitiesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-slow-1775758621018-backfill-workspace-id-on-indirect-entities';
|
||||
import { DropWorkspaceVersionColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1785000000000-drop-workspace-version-column';
|
||||
import { AddGlobalObjectContextToCommandMenuItemAvailabilityTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1776090711153-add-global-object-context-to-command-menu-item-availability-type';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||
MigrateMessagingCalendarToCoreFastInstanceCommand,
|
||||
AddEmailThreadWidgetTypeFastInstanceCommand,
|
||||
AddStandalonePageFastInstanceCommand,
|
||||
AddPermissionFlagRoleIdIndexFastInstanceCommand,
|
||||
AddWorkspaceIdToIndirectEntitiesFastInstanceCommand,
|
||||
BackfillWorkspaceIdOnIndirectEntitiesSlowInstanceCommand,
|
||||
@@ -22,4 +25,5 @@ export const INSTANCE_COMMANDS = [
|
||||
DropObjectMetadataDataSourceFkFastInstanceCommand,
|
||||
AddCreditBalanceToBillingCustomerFastInstanceCommand,
|
||||
DropWorkspaceVersionColumnFastInstanceCommand,
|
||||
AddGlobalObjectContextToCommandMenuItemAvailabilityTypeFastInstanceCommand,
|
||||
];
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ const AVAILABILITY_TYPE_MAP: Record<
|
||||
CommandMenuItemAvailabilityType
|
||||
> = {
|
||||
GLOBAL: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
GLOBAL_OBJECT_CONTEXT: CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT,
|
||||
RECORD_SELECTION: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
FALLBACK: CommandMenuItemAvailabilityType.FALLBACK,
|
||||
};
|
||||
|
||||
+2
@@ -27,6 +27,8 @@ export const fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem =
|
||||
navigationMenuItemManifest.folderUniversalIdentifier ?? null,
|
||||
targetObjectMetadataUniversalIdentifier:
|
||||
navigationMenuItemManifest.targetObjectUniversalIdentifier ?? null,
|
||||
pageLayoutUniversalIdentifier:
|
||||
navigationMenuItemManifest.pageLayoutUniversalIdentifier ?? null,
|
||||
targetRecordId: null,
|
||||
userWorkspaceId: null,
|
||||
createdAt: now,
|
||||
|
||||
@@ -1100,7 +1100,12 @@ export const computeMetadataSchemaComponents = (
|
||||
name: { type: 'string' },
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['RECORD_INDEX', 'RECORD_PAGE', 'DASHBOARD'],
|
||||
enum: [
|
||||
'RECORD_INDEX',
|
||||
'RECORD_PAGE',
|
||||
'DASHBOARD',
|
||||
'STANDALONE_PAGE',
|
||||
],
|
||||
default: 'RECORD_PAGE',
|
||||
},
|
||||
objectMetadataId: { type: 'string', format: 'uuid' },
|
||||
@@ -1121,7 +1126,12 @@ export const computeMetadataSchemaComponents = (
|
||||
name: { type: 'string' },
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['RECORD_INDEX', 'RECORD_PAGE', 'DASHBOARD'],
|
||||
enum: [
|
||||
'RECORD_INDEX',
|
||||
'RECORD_PAGE',
|
||||
'DASHBOARD',
|
||||
'STANDALONE_PAGE',
|
||||
],
|
||||
},
|
||||
objectMetadataId: { type: 'string', format: 'uuid' },
|
||||
},
|
||||
@@ -1134,7 +1144,12 @@ export const computeMetadataSchemaComponents = (
|
||||
name: { type: 'string' },
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['RECORD_INDEX', 'RECORD_PAGE', 'DASHBOARD'],
|
||||
enum: [
|
||||
'RECORD_INDEX',
|
||||
'RECORD_PAGE',
|
||||
'DASHBOARD',
|
||||
'STANDALONE_PAGE',
|
||||
],
|
||||
},
|
||||
objectMetadataId: { type: 'string', format: 'uuid' },
|
||||
tabs: {
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum CommandMenuItemAvailabilityType {
|
||||
GLOBAL = 'GLOBAL',
|
||||
GLOBAL_OBJECT_CONTEXT = 'GLOBAL_OBJECT_CONTEXT',
|
||||
RECORD_SELECTION = 'RECORD_SELECTION',
|
||||
FALLBACK = 'FALLBACK',
|
||||
}
|
||||
|
||||
+5
@@ -1169,6 +1169,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: false,
|
||||
universalProperty: 'targetObjectMetadataUniversalIdentifier',
|
||||
},
|
||||
pageLayoutId: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: 'pageLayoutUniversalIdentifier',
|
||||
},
|
||||
},
|
||||
permissionFlag: {
|
||||
flag: {
|
||||
|
||||
+3
@@ -64,6 +64,9 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
|
||||
view: {
|
||||
foreignKey: 'viewId',
|
||||
},
|
||||
pageLayout: {
|
||||
foreignKey: 'pageLayoutId',
|
||||
},
|
||||
},
|
||||
fieldMetadata: {
|
||||
object: {
|
||||
|
||||
+7
@@ -112,6 +112,13 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
|
||||
isNullable: true,
|
||||
universalForeignKey: 'viewUniversalIdentifier',
|
||||
},
|
||||
pageLayout: {
|
||||
metadataName: 'pageLayout',
|
||||
foreignKey: 'pageLayoutId',
|
||||
inverseOneToManyProperty: null,
|
||||
isNullable: true,
|
||||
universalForeignKey: 'pageLayoutUniversalIdentifier',
|
||||
},
|
||||
},
|
||||
fieldMetadata: {
|
||||
object: {
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
|
||||
navigationMenuItem: {
|
||||
objectMetadata: true,
|
||||
view: true,
|
||||
pageLayout: true,
|
||||
},
|
||||
permissionFlag: {
|
||||
role: true,
|
||||
|
||||
+1
@@ -44,6 +44,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
"objectMetadata",
|
||||
"navigationMenuItem",
|
||||
"view",
|
||||
"pageLayout",
|
||||
]
|
||||
`;
|
||||
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@
|
||||
exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = `
|
||||
[
|
||||
"rowLevelPermissionPredicate",
|
||||
"fieldPermission",
|
||||
"navigationMenuItem",
|
||||
"fieldPermission",
|
||||
"viewField",
|
||||
"viewFilter",
|
||||
"commandMenuItem",
|
||||
|
||||
+1
@@ -7,4 +7,5 @@ export const FLAT_NAVIGATION_MENU_ITEM_EDITABLE_PROPERTIES = [
|
||||
'link',
|
||||
'icon',
|
||||
'color',
|
||||
'pageLayoutId',
|
||||
] as const satisfies MetadataEntityPropertyName<'navigationMenuItem'>[];
|
||||
|
||||
+2
@@ -6,6 +6,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata
|
||||
import { WorkspaceFlatNavigationMenuItemMapCacheService } from 'src/engine/metadata-modules/flat-navigation-menu-item/services/workspace-flat-navigation-menu-item-map-cache.service';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
|
||||
@Module({
|
||||
@@ -14,6 +15,7 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit
|
||||
NavigationMenuItemEntity,
|
||||
ApplicationEntity,
|
||||
ObjectMetadataEntity,
|
||||
PageLayoutEntity,
|
||||
ViewEntity,
|
||||
]),
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
|
||||
+38
-22
@@ -12,6 +12,7 @@ import { addFlatNavigationMenuItemToMapsAndUpdateIndex } from 'src/engine/metada
|
||||
import { fromNavigationMenuItemEntityToFlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-navigation-menu-item-entity-to-flat-navigation-menu-item.util';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
|
||||
@@ -28,6 +29,8 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
@InjectRepository(PageLayoutEntity)
|
||||
private readonly pageLayoutRepository: Repository<PageLayoutEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -35,28 +38,38 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<FlatNavigationMenuItemMaps> {
|
||||
const [navigationMenuItems, applications, objectMetadatas, views] =
|
||||
await Promise.all([
|
||||
this.navigationMenuItemRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
const [
|
||||
navigationMenuItems,
|
||||
applications,
|
||||
objectMetadatas,
|
||||
views,
|
||||
pageLayouts,
|
||||
] = await Promise.all([
|
||||
this.navigationMenuItemRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.pageLayoutRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const applicationIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(applications);
|
||||
@@ -66,6 +79,8 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac
|
||||
createIdToUniversalIdentifierMap(navigationMenuItems);
|
||||
const viewIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(views);
|
||||
const pageLayoutIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(pageLayouts);
|
||||
|
||||
const flatNavigationMenuItemMaps = {
|
||||
...createEmptyFlatEntityMaps(),
|
||||
@@ -80,6 +95,7 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac
|
||||
objectMetadataIdToUniversalIdentifierMap,
|
||||
navigationMenuItemIdToUniversalIdentifierMap,
|
||||
viewIdToUniversalIdentifierMap,
|
||||
pageLayoutIdToUniversalIdentifierMap,
|
||||
});
|
||||
|
||||
addFlatNavigationMenuItemToMapsAndUpdateIndex({
|
||||
|
||||
+7
-1
@@ -16,6 +16,7 @@ export const fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate =
|
||||
flatNavigationMenuItemMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatPageLayoutMaps,
|
||||
}: {
|
||||
createNavigationMenuItemInput: CreateNavigationMenuItemInput;
|
||||
workspaceId: string;
|
||||
@@ -23,7 +24,7 @@ export const fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate =
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatObjectMetadataMaps' | 'flatViewMaps'
|
||||
'flatObjectMetadataMaps' | 'flatViewMaps' | 'flatPageLayoutMaps'
|
||||
>): FlatNavigationMenuItem => {
|
||||
const id = createNavigationMenuItemInput.id ?? uuidv4();
|
||||
const now = new Date().toISOString();
|
||||
@@ -52,6 +53,7 @@ export const fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate =
|
||||
targetObjectMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
folderUniversalIdentifier,
|
||||
pageLayoutUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'navigationMenuItem',
|
||||
foreignKeyValues: {
|
||||
@@ -59,11 +61,13 @@ export const fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate =
|
||||
createNavigationMenuItemInput.targetObjectMetadataId,
|
||||
viewId: createNavigationMenuItemInput.viewId,
|
||||
folderId: createNavigationMenuItemInput.folderId,
|
||||
pageLayoutId: createNavigationMenuItemInput.pageLayoutId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatNavigationMenuItemMaps,
|
||||
flatPageLayoutMaps,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,6 +84,8 @@ export const fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate =
|
||||
viewUniversalIdentifier,
|
||||
folderId: createNavigationMenuItemInput.folderId ?? null,
|
||||
folderUniversalIdentifier,
|
||||
pageLayoutId: createNavigationMenuItemInput.pageLayoutId ?? null,
|
||||
pageLayoutUniversalIdentifier,
|
||||
name: createNavigationMenuItemInput.name ?? null,
|
||||
link: createNavigationMenuItemInput.link ?? null,
|
||||
icon: createNavigationMenuItemInput.icon ?? null,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ export const fromFlatNavigationMenuItemToNavigationMenuItemDto = (
|
||||
link: flatNavigationMenuItem.link ?? undefined,
|
||||
icon: flatNavigationMenuItem.icon ?? undefined,
|
||||
color: flatNavigationMenuItem.color ?? undefined,
|
||||
pageLayoutId: flatNavigationMenuItem.pageLayoutId ?? undefined,
|
||||
position: flatNavigationMenuItem.position,
|
||||
workspaceId: flatNavigationMenuItem.workspaceId,
|
||||
applicationId: flatNavigationMenuItem.applicationId ?? undefined,
|
||||
|
||||
+19
@@ -13,6 +13,7 @@ export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ({
|
||||
objectMetadataIdToUniversalIdentifierMap,
|
||||
navigationMenuItemIdToUniversalIdentifierMap,
|
||||
viewIdToUniversalIdentifierMap,
|
||||
pageLayoutIdToUniversalIdentifierMap,
|
||||
}: FromEntityToFlatEntityArgs<'navigationMenuItem'>): FlatNavigationMenuItem => {
|
||||
const applicationUniversalIdentifier =
|
||||
applicationIdToUniversalIdentifierMap.get(
|
||||
@@ -73,6 +74,22 @@ export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ({
|
||||
}
|
||||
}
|
||||
|
||||
let pageLayoutUniversalIdentifier: string | null = null;
|
||||
|
||||
if (isDefined(navigationMenuItemEntity.pageLayoutId)) {
|
||||
pageLayoutUniversalIdentifier =
|
||||
pageLayoutIdToUniversalIdentifierMap.get(
|
||||
navigationMenuItemEntity.pageLayoutId,
|
||||
) ?? null;
|
||||
|
||||
if (!isDefined(pageLayoutUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`PageLayout with id ${navigationMenuItemEntity.pageLayoutId} not found for navigationMenuItem ${navigationMenuItemEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: navigationMenuItemEntity.id,
|
||||
type: navigationMenuItemEntity.type,
|
||||
@@ -85,6 +102,7 @@ export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ({
|
||||
link: navigationMenuItemEntity.link,
|
||||
icon: navigationMenuItemEntity.icon,
|
||||
color: navigationMenuItemEntity.color,
|
||||
pageLayoutId: navigationMenuItemEntity.pageLayoutId,
|
||||
position: navigationMenuItemEntity.position,
|
||||
workspaceId: navigationMenuItemEntity.workspaceId,
|
||||
universalIdentifier: navigationMenuItemEntity.universalIdentifier,
|
||||
@@ -95,5 +113,6 @@ export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ({
|
||||
targetObjectMetadataUniversalIdentifier,
|
||||
folderUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
pageLayoutUniversalIdentifier,
|
||||
};
|
||||
};
|
||||
|
||||
+20
-1
@@ -1,5 +1,6 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
import { FLAT_NAVIGATION_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-navigation-menu-item/constants/flat-navigation-menu-item-editable-properties.constant';
|
||||
@@ -15,13 +16,17 @@ import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-
|
||||
export const fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOrThrow =
|
||||
({
|
||||
flatNavigationMenuItemMaps,
|
||||
flatPageLayoutMaps,
|
||||
updateNavigationMenuItemInput,
|
||||
}: {
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
updateNavigationMenuItemInput: UpdateNavigationMenuItemInput & {
|
||||
id: string;
|
||||
};
|
||||
}): FlatNavigationMenuItem => {
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatPageLayoutMaps'
|
||||
>): FlatNavigationMenuItem => {
|
||||
const existingFlatNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: updateNavigationMenuItemInput.id,
|
||||
flatEntityMaps: flatNavigationMenuItemMaps,
|
||||
@@ -59,5 +64,19 @@ export const fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOr
|
||||
folderUniversalIdentifier;
|
||||
}
|
||||
|
||||
if (updates.pageLayoutId !== undefined) {
|
||||
const { pageLayoutUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'navigationMenuItem',
|
||||
foreignKeyValues: {
|
||||
pageLayoutId: flatNavigationMenuItemToUpdate.pageLayoutId,
|
||||
},
|
||||
flatEntityMaps: { flatPageLayoutMaps },
|
||||
});
|
||||
|
||||
flatNavigationMenuItemToUpdate.pageLayoutUniversalIdentifier =
|
||||
pageLayoutUniversalIdentifier;
|
||||
}
|
||||
|
||||
return flatNavigationMenuItemToUpdate;
|
||||
};
|
||||
|
||||
+5
@@ -67,6 +67,11 @@ export class CreateNavigationMenuItemInput {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
folderId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
pageLayoutId?: string | null;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
|
||||
+5
@@ -66,6 +66,11 @@ export class NavigationMenuItemDTO {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
folderId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
pageLayoutId?: string | null;
|
||||
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
|
||||
+5
@@ -43,6 +43,11 @@ export class UpdateNavigationMenuItemInput {
|
||||
@IsString()
|
||||
@Field(() => String, { nullable: true })
|
||||
color?: string | null;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
pageLayoutId?: string | null;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
|
||||
+17
-1
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
@@ -35,13 +36,18 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
'viewId',
|
||||
'workspaceId',
|
||||
])
|
||||
@Index('IDX_NAVIGATION_MENU_ITEM_PAGE_LAYOUT_ID_WORKSPACE_ID', [
|
||||
'pageLayoutId',
|
||||
'workspaceId',
|
||||
])
|
||||
@Check(
|
||||
'CHK_navigation_menu_item_type_fields',
|
||||
`("type" = 'FOLDER')
|
||||
OR ("type" = 'OBJECT' AND "targetObjectMetadataId" IS NOT NULL)
|
||||
OR ("type" = 'VIEW' AND "viewId" IS NOT NULL)
|
||||
OR ("type" = 'RECORD' AND "targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL)
|
||||
OR ("type" = 'LINK' AND "link" IS NOT NULL)`,
|
||||
OR ("type" = 'LINK' AND "link" IS NOT NULL)
|
||||
OR ("type" = 'PAGE_LAYOUT' AND "pageLayoutId" IS NOT NULL)`,
|
||||
)
|
||||
export class NavigationMenuItemEntity
|
||||
extends SyncableEntity
|
||||
@@ -112,6 +118,16 @@ export class NavigationMenuItemEntity
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
folderId: string | null;
|
||||
|
||||
@ManyToOne(() => PageLayoutEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({ name: 'pageLayoutId' })
|
||||
pageLayout: Relation<PageLayoutEntity> | null;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
pageLayoutId: string | null;
|
||||
|
||||
@Column({ nullable: false, type: 'double precision' })
|
||||
position: number;
|
||||
|
||||
|
||||
+9
-2
@@ -186,6 +186,7 @@ export class NavigationMenuItemService {
|
||||
flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatPageLayoutMaps,
|
||||
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -193,6 +194,7 @@ export class NavigationMenuItemService {
|
||||
'flatNavigationMenuItemMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
'flatViewMaps',
|
||||
'flatPageLayoutMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -230,6 +232,7 @@ export class NavigationMenuItemService {
|
||||
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatPageLayoutMaps,
|
||||
});
|
||||
|
||||
addFlatNavigationMenuItemToMapsAndUpdateIndex({
|
||||
@@ -341,11 +344,14 @@ export class NavigationMenuItemService {
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps } =
|
||||
const {
|
||||
flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps,
|
||||
flatPageLayoutMaps,
|
||||
} =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps', 'flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -378,6 +384,7 @@ export class NavigationMenuItemService {
|
||||
fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOrThrow(
|
||||
{
|
||||
flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps,
|
||||
flatPageLayoutMaps,
|
||||
updateNavigationMenuItemInput: updateInput,
|
||||
},
|
||||
),
|
||||
|
||||
+2
@@ -835,6 +835,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
viewUniversalIdentifier: null,
|
||||
folderId: null,
|
||||
folderUniversalIdentifier: null,
|
||||
pageLayoutId: null,
|
||||
pageLayoutUniversalIdentifier: null,
|
||||
name: null,
|
||||
link: null,
|
||||
icon: null,
|
||||
|
||||
+1
@@ -2,4 +2,5 @@ export enum PageLayoutType {
|
||||
RECORD_INDEX = 'RECORD_INDEX',
|
||||
RECORD_PAGE = 'RECORD_PAGE',
|
||||
DASHBOARD = 'DASHBOARD',
|
||||
STANDALONE_PAGE = 'STANDALONE_PAGE',
|
||||
}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const NAVIGATION_MENU_ITEM_SEEDS = {
|
||||
DOCUMENTATION_PAGE: 'DOCUMENTATION_PAGE_NAV_ITEM',
|
||||
};
|
||||
+1
@@ -2,4 +2,5 @@ export const PAGE_LAYOUT_SEEDS = {
|
||||
SALES_DASHBOARD: 'SALES_DASHBOARD',
|
||||
CUSTOMER_DASHBOARD: 'CUSTOMER_DASHBOARD',
|
||||
TEAM_DASHBOARD: 'TEAM_DASHBOARD',
|
||||
DOCUMENTATION_STANDALONE_PAGE: 'DOCUMENTATION_STANDALONE_PAGE',
|
||||
};
|
||||
|
||||
+1
@@ -5,4 +5,5 @@ export const PAGE_LAYOUT_TAB_SEEDS = {
|
||||
CUSTOMER_ANALYTICS: 'CUSTOMER_ANALYTICS_TAB',
|
||||
TEAM_OVERVIEW: 'TEAM_OVERVIEW_TAB',
|
||||
TEAM_METRICS: 'TEAM_METRICS_TAB',
|
||||
DOCUMENTATION: 'DOCUMENTATION',
|
||||
};
|
||||
|
||||
+1
@@ -21,4 +21,5 @@ export const PAGE_LAYOUT_WIDGET_SEEDS = {
|
||||
TEAM_OPEN_TASKS: 'TEAM_OPEN_TASKS_WIDGET',
|
||||
|
||||
FRONT_COMPONENT: 'FRONT_COMPONENT_WIDGET',
|
||||
DOCUMENTATION_IFRAME: 'DOCUMENTATION_IFRAME_WIDGET',
|
||||
};
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
import { NAVIGATION_MENU_ITEM_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/navigation-menu-item-seeds.constant';
|
||||
import { PAGE_LAYOUT_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant';
|
||||
import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util';
|
||||
|
||||
export const getNavigationMenuItemFlatEntitySeeds = ({
|
||||
workspaceId,
|
||||
flatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatApplication: FlatApplication;
|
||||
}): FlatNavigationMenuItem[] => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return [
|
||||
{
|
||||
id: generateSeedId(
|
||||
workspaceId,
|
||||
NAVIGATION_MENU_ITEM_SEEDS.DOCUMENTATION_PAGE,
|
||||
),
|
||||
universalIdentifier: generateSeedId(
|
||||
workspaceId,
|
||||
NAVIGATION_MENU_ITEM_SEEDS.DOCUMENTATION_PAGE,
|
||||
),
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
type: NavigationMenuItemType.PAGE_LAYOUT,
|
||||
name: 'Star History',
|
||||
icon: 'IconStar',
|
||||
color: 'yellow',
|
||||
position: 9999,
|
||||
link: null,
|
||||
userWorkspaceId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
targetObjectMetadataUniversalIdentifier: null,
|
||||
viewId: null,
|
||||
viewUniversalIdentifier: null,
|
||||
folderId: null,
|
||||
folderUniversalIdentifier: null,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
|
||||
),
|
||||
pageLayoutUniversalIdentifier: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
|
||||
),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
];
|
||||
};
|
||||
+99
-42
@@ -1,48 +1,105 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { PAGE_LAYOUT_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant';
|
||||
import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util';
|
||||
|
||||
type PageLayoutSeed = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: PageLayoutType;
|
||||
objectMetadataId: string | null;
|
||||
export const getPageLayoutFlatEntitySeeds = ({
|
||||
workspaceId,
|
||||
flatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
universalIdentifier: string;
|
||||
applicationId: string;
|
||||
};
|
||||
flatApplication: FlatApplication;
|
||||
}): FlatPageLayout[] => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
export const getPageLayoutDataSeeds = (
|
||||
workspaceId: string,
|
||||
applicationId: string,
|
||||
): PageLayoutSeed[] => [
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.SALES_DASHBOARD),
|
||||
name: 'Sales Dashboard Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD),
|
||||
name: 'Customer Dashboard Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
|
||||
name: 'Team Dashboard Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId,
|
||||
},
|
||||
];
|
||||
return [
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.SALES_DASHBOARD),
|
||||
universalIdentifier: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.SALES_DASHBOARD,
|
||||
),
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
name: 'Sales Dashboard Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
objectMetadataUniversalIdentifier: null,
|
||||
tabIds: [],
|
||||
tabUniversalIdentifiers: [],
|
||||
defaultTabToFocusOnMobileAndSidePanelId: null,
|
||||
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD),
|
||||
universalIdentifier: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD,
|
||||
),
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
name: 'Customer Dashboard Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
objectMetadataUniversalIdentifier: null,
|
||||
tabIds: [],
|
||||
tabUniversalIdentifiers: [],
|
||||
defaultTabToFocusOnMobileAndSidePanelId: null,
|
||||
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
|
||||
universalIdentifier: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD,
|
||||
),
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
name: 'Team Dashboard Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
objectMetadataUniversalIdentifier: null,
|
||||
tabIds: [],
|
||||
tabUniversalIdentifiers: [],
|
||||
defaultTabToFocusOnMobileAndSidePanelId: null,
|
||||
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
|
||||
),
|
||||
universalIdentifier: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
|
||||
),
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
name: 'Documentation',
|
||||
type: PageLayoutType.STANDALONE_PAGE,
|
||||
objectMetadataId: null,
|
||||
objectMetadataUniversalIdentifier: null,
|
||||
tabIds: [],
|
||||
tabUniversalIdentifiers: [],
|
||||
defaultTabToFocusOnMobileAndSidePanelId: null,
|
||||
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
+71
-81
@@ -1,98 +1,88 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { PAGE_LAYOUT_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant';
|
||||
import { PAGE_LAYOUT_TAB_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant';
|
||||
import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util';
|
||||
|
||||
type PageLayoutTabSeed = {
|
||||
id: string;
|
||||
title: string;
|
||||
position: number;
|
||||
pageLayoutId: string;
|
||||
workspaceId: string;
|
||||
} & Pick<
|
||||
FlatPageLayoutTab,
|
||||
'applicationId' | 'universalIdentifier' | 'overrides'
|
||||
>;
|
||||
|
||||
export const getPageLayoutTabDataSeeds = ({
|
||||
applicationId,
|
||||
export const getPageLayoutTabFlatEntitySeeds = ({
|
||||
workspaceId,
|
||||
flatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
}): PageLayoutTabSeed[] => [
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW),
|
||||
title: 'Overview',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
flatApplication: FlatApplication;
|
||||
}): FlatPageLayoutTab[] => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const buildTab = (
|
||||
tabSeed: string,
|
||||
title: string,
|
||||
position: number,
|
||||
pageLayoutSeed: string,
|
||||
): FlatPageLayoutTab => ({
|
||||
id: generateSeedId(workspaceId, tabSeed),
|
||||
universalIdentifier: generateSeedId(workspaceId, tabSeed),
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
title,
|
||||
position,
|
||||
pageLayoutId: generateSeedId(workspaceId, pageLayoutSeed),
|
||||
pageLayoutUniversalIdentifier: generateSeedId(workspaceId, pageLayoutSeed),
|
||||
widgetIds: [],
|
||||
widgetUniversalIdentifiers: [],
|
||||
isActive: true,
|
||||
icon: null,
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
overrides: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
return [
|
||||
buildTab(
|
||||
PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW,
|
||||
'Overview',
|
||||
0,
|
||||
PAGE_LAYOUT_SEEDS.SALES_DASHBOARD,
|
||||
),
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId,
|
||||
overrides: null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.SALES_DETAILS),
|
||||
title: 'Details',
|
||||
position: 1,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
buildTab(
|
||||
PAGE_LAYOUT_TAB_SEEDS.SALES_DETAILS,
|
||||
'Details',
|
||||
1,
|
||||
PAGE_LAYOUT_SEEDS.SALES_DASHBOARD,
|
||||
),
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId,
|
||||
overrides: null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW),
|
||||
title: 'Overview',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
buildTab(
|
||||
PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW,
|
||||
'Overview',
|
||||
0,
|
||||
PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD,
|
||||
),
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId,
|
||||
overrides: null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS),
|
||||
title: 'Analytics',
|
||||
position: 1,
|
||||
pageLayoutId: generateSeedId(
|
||||
workspaceId,
|
||||
buildTab(
|
||||
PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS,
|
||||
'Analytics',
|
||||
1,
|
||||
PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD,
|
||||
),
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId,
|
||||
overrides: null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW),
|
||||
title: 'Team & People',
|
||||
position: 0,
|
||||
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId,
|
||||
overrides: null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS),
|
||||
title: 'Tasks & Activity',
|
||||
position: 1,
|
||||
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId,
|
||||
overrides: null,
|
||||
},
|
||||
];
|
||||
buildTab(
|
||||
PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW,
|
||||
'Team & People',
|
||||
0,
|
||||
PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD,
|
||||
),
|
||||
buildTab(
|
||||
PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS,
|
||||
'Tasks & Activity',
|
||||
1,
|
||||
PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD,
|
||||
),
|
||||
buildTab(
|
||||
PAGE_LAYOUT_TAB_SEEDS.DOCUMENTATION,
|
||||
'Documentation',
|
||||
0,
|
||||
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user