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:
+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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user