Refactor navigation commands to use NAVIGATION engine key with payload (#19303)
- Adds a payload JSON column to `CommandMenuItem` and introduces a unified `NAVIGATION` engine component key that replaces all individual GO_TO_* keys - Navigation commands now use the payload to determine their target (either an objectMetadataItemId or a path), making navigation commands dynamic and eliminating the need for a hardcoded engine key per object - Includes a 1.21 upgrade command (refactor-navigation-commands) that migrates existing GO_TO_* items to NAVIGATION items with the appropriate payload, and applies a CHECK constraint enforcing payload coherence https://github.com/user-attachments/assets/4d305ba2-ae0b-4556-bb0e-e9d899777350 TODO: In a second PR, create the sync between object metadata items and the navigation command menu items - Object metadata item created or enabled -> Create navigation command - Object metadata item deleted or disabled -> Delete associated navigation command In another PR: - Allow `label`, `shortLabel` and `icon` to resolve the `navigateToObjectMetadataItem` dynamically in their interpolation instead of being hardcoded in the command menu item - Make the icon dynamic in the command menu items as the label so that we can resolve ${navigateToObjectMetadataItem.icon} at runTime -> This way we won't need to keep update the command menu item icon when we update the objectMetadataItem icon
This commit is contained in:
@@ -2575,6 +2575,7 @@ type CommandMenuItem {
|
||||
position: Float!
|
||||
isPinned: Boolean!
|
||||
availabilityType: CommandMenuItemAvailabilityType!
|
||||
payload: JSON
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
@@ -2601,19 +2602,11 @@ enum EngineComponentKey {
|
||||
SEE_DELETED_RECORDS
|
||||
CREATE_NEW_VIEW
|
||||
HIDE_DELETED_RECORDS
|
||||
GO_TO_PEOPLE
|
||||
GO_TO_COMPANIES
|
||||
GO_TO_DASHBOARDS
|
||||
GO_TO_OPPORTUNITIES
|
||||
GO_TO_SETTINGS
|
||||
GO_TO_TASKS
|
||||
GO_TO_NOTES
|
||||
EDIT_RECORD_PAGE_LAYOUT
|
||||
EDIT_DASHBOARD_LAYOUT
|
||||
SAVE_DASHBOARD_LAYOUT
|
||||
CANCEL_DASHBOARD_LAYOUT
|
||||
DUPLICATE_DASHBOARD
|
||||
GO_TO_WORKFLOWS
|
||||
ACTIVATE_WORKFLOW
|
||||
DEACTIVATE_WORKFLOW
|
||||
DISCARD_DRAFT_WORKFLOW
|
||||
@@ -2624,7 +2617,6 @@ enum EngineComponentKey {
|
||||
ADD_NODE_WORKFLOW
|
||||
TIDY_UP_WORKFLOW
|
||||
DUPLICATE_WORKFLOW
|
||||
GO_TO_RUNS
|
||||
SEE_VERSION_WORKFLOW_RUN
|
||||
SEE_WORKFLOW_WORKFLOW_RUN
|
||||
STOP_WORKFLOW_RUN
|
||||
@@ -2636,10 +2628,20 @@ enum EngineComponentKey {
|
||||
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
|
||||
@@ -4049,6 +4051,7 @@ input CreateCommandMenuItemInput {
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
payload: JSON
|
||||
}
|
||||
|
||||
input UpdateCommandMenuItemInput {
|
||||
|
||||
@@ -2286,6 +2286,7 @@ export interface CommandMenuItem {
|
||||
position: Scalars['Float']
|
||||
isPinned: Scalars['Boolean']
|
||||
availabilityType: CommandMenuItemAvailabilityType
|
||||
payload?: Scalars['JSON']
|
||||
hotKeys?: Scalars['String'][]
|
||||
conditionalAvailabilityExpression?: Scalars['String']
|
||||
availabilityObjectMetadataId?: Scalars['UUID']
|
||||
@@ -2295,7 +2296,7 @@ export interface CommandMenuItem {
|
||||
__typename: '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' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'GO_TO_WORKFLOWS' | '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' | 'GO_TO_RUNS' | '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' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | '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 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'
|
||||
|
||||
@@ -5485,6 +5486,7 @@ export interface CommandMenuItemGenqlSelection{
|
||||
position?: boolean | number
|
||||
isPinned?: boolean | number
|
||||
availabilityType?: boolean | number
|
||||
payload?: boolean | number
|
||||
hotKeys?: boolean | number
|
||||
conditionalAvailabilityExpression?: boolean | number
|
||||
availabilityObjectMetadataId?: boolean | number
|
||||
@@ -6458,7 +6460,7 @@ update: UpdateLogicFunctionFromSourceInputUpdates}
|
||||
|
||||
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),toolInputSchema?: (Scalars['JSON'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),isTool?: (Scalars['Boolean'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
|
||||
|
||||
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null)}
|
||||
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null)}
|
||||
|
||||
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null)}
|
||||
|
||||
@@ -9263,19 +9265,11 @@ export const enumEngineComponentKey = {
|
||||
SEE_DELETED_RECORDS: 'SEE_DELETED_RECORDS' as const,
|
||||
CREATE_NEW_VIEW: 'CREATE_NEW_VIEW' as const,
|
||||
HIDE_DELETED_RECORDS: 'HIDE_DELETED_RECORDS' as const,
|
||||
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
|
||||
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
|
||||
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
|
||||
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
|
||||
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
|
||||
GO_TO_TASKS: 'GO_TO_TASKS' as const,
|
||||
GO_TO_NOTES: 'GO_TO_NOTES' as const,
|
||||
EDIT_RECORD_PAGE_LAYOUT: 'EDIT_RECORD_PAGE_LAYOUT' as const,
|
||||
EDIT_DASHBOARD_LAYOUT: 'EDIT_DASHBOARD_LAYOUT' as const,
|
||||
SAVE_DASHBOARD_LAYOUT: 'SAVE_DASHBOARD_LAYOUT' as const,
|
||||
CANCEL_DASHBOARD_LAYOUT: 'CANCEL_DASHBOARD_LAYOUT' as const,
|
||||
DUPLICATE_DASHBOARD: 'DUPLICATE_DASHBOARD' as const,
|
||||
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
|
||||
ACTIVATE_WORKFLOW: 'ACTIVATE_WORKFLOW' as const,
|
||||
DEACTIVATE_WORKFLOW: 'DEACTIVATE_WORKFLOW' as const,
|
||||
DISCARD_DRAFT_WORKFLOW: 'DISCARD_DRAFT_WORKFLOW' as const,
|
||||
@@ -9286,7 +9280,6 @@ export const enumEngineComponentKey = {
|
||||
ADD_NODE_WORKFLOW: 'ADD_NODE_WORKFLOW' as const,
|
||||
TIDY_UP_WORKFLOW: 'TIDY_UP_WORKFLOW' as const,
|
||||
DUPLICATE_WORKFLOW: 'DUPLICATE_WORKFLOW' as const,
|
||||
GO_TO_RUNS: 'GO_TO_RUNS' as const,
|
||||
SEE_VERSION_WORKFLOW_RUN: 'SEE_VERSION_WORKFLOW_RUN' as const,
|
||||
SEE_WORKFLOW_WORKFLOW_RUN: 'SEE_WORKFLOW_WORKFLOW_RUN' as const,
|
||||
STOP_WORKFLOW_RUN: 'STOP_WORKFLOW_RUN' as const,
|
||||
@@ -9298,10 +9291,20 @@ export const enumEngineComponentKey = {
|
||||
SEARCH_RECORDS_FALLBACK: 'SEARCH_RECORDS_FALLBACK' as const,
|
||||
ASK_AI: 'ASK_AI' as const,
|
||||
VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const,
|
||||
NAVIGATION: 'NAVIGATION' as const,
|
||||
TRIGGER_WORKFLOW_VERSION: 'TRIGGER_WORKFLOW_VERSION' as const,
|
||||
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const,
|
||||
REPLY_TO_EMAIL_THREAD: 'REPLY_TO_EMAIL_THREAD' as const,
|
||||
COMPOSE_EMAIL: 'COMPOSE_EMAIL' as const,
|
||||
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
|
||||
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
|
||||
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
|
||||
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
|
||||
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
|
||||
GO_TO_TASKS: 'GO_TO_TASKS' as const,
|
||||
GO_TO_NOTES: 'GO_TO_NOTES' as const,
|
||||
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
|
||||
GO_TO_RUNS: 'GO_TO_RUNS' as const,
|
||||
DELETE_SINGLE_RECORD: 'DELETE_SINGLE_RECORD' as const,
|
||||
DELETE_MULTIPLE_RECORDS: 'DELETE_MULTIPLE_RECORDS' as const,
|
||||
RESTORE_SINGLE_RECORD: 'RESTORE_SINGLE_RECORD' as const,
|
||||
|
||||
@@ -5159,6 +5159,9 @@ export default {
|
||||
"availabilityType": [
|
||||
297
|
||||
],
|
||||
"payload": [
|
||||
15
|
||||
],
|
||||
"hotKeys": [
|
||||
1
|
||||
],
|
||||
@@ -10247,6 +10250,9 @@ export default {
|
||||
"availabilityObjectMetadataId": [
|
||||
3
|
||||
],
|
||||
"payload": [
|
||||
15
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
@@ -60,6 +60,7 @@ export const HeadlessCommandMenuItem = ({
|
||||
workflowVersionId: item.workflowVersionId ?? undefined,
|
||||
availabilityType: item.availabilityType,
|
||||
availabilityObjectMetadataId: item.availabilityObjectMetadataId,
|
||||
payload: item.payload ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
|
||||
import { isObjectMetadataNavigationPayload } from '@/command-menu-item/engine-command/utils/isObjectMetadataNavigationPayload';
|
||||
import { isPathNavigationPayload } from '@/command-menu-item/engine-command/utils/isPathNavigationPayload';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const NavigationEngineCommand = () => {
|
||||
const { payload } = useHeadlessCommandContextApi();
|
||||
const navigate = useNavigate();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const onExecute = () => {
|
||||
if (!isDefined(payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const navigationPayload = payload;
|
||||
|
||||
if (isObjectMetadataNavigationPayload(navigationPayload)) {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === navigationPayload.objectMetadataItemId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = getAppPath(AppPath.RecordIndexPage, {
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line twenty/no-navigate-prefer-link
|
||||
navigate(path);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPathNavigationPayload(navigationPayload)) {
|
||||
// eslint-disable-next-line twenty/no-navigate-prefer-link
|
||||
navigate(navigationPayload.path);
|
||||
}
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={onExecute} />;
|
||||
};
|
||||
+12
-8
@@ -1,5 +1,13 @@
|
||||
import { HeadlessFrontComponentRendererEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessFrontComponentRendererEngineCommand';
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { HeadlessOpenSidePanelPageEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessOpenSidePanelPageEngineCommand';
|
||||
import { NavigationEngineCommand } from '@/command-menu-item/engine-command/components/NavigationEngineCommand';
|
||||
import { ComposeEmailCommand } from '@/command-menu-item/engine-command/global/components/ComposeEmailCommand';
|
||||
import { DeleteRecordsCommand } from '@/command-menu-item/engine-command/record/components/DeleteRecordsCommand';
|
||||
import { DestroyRecordsCommand } from '@/command-menu-item/engine-command/record/components/DestroyRecordsCommand';
|
||||
import { ExportRecordsCommand } from '@/command-menu-item/engine-command/record/components/ExportRecordsCommand';
|
||||
import { RestoreRecordsCommand } from '@/command-menu-item/engine-command/record/components/RestoreRecordsCommand';
|
||||
import { TriggerWorkflowVersionEngineCommand } from '@/command-menu-item/engine-command/record/components/TriggerWorkflowVersionEngineCommand';
|
||||
import { MergeMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/MergeMultipleRecordsCommand';
|
||||
import { UpdateMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/UpdateMultipleRecordsCommand';
|
||||
import { CreateNewIndexRecordNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/CreateNewIndexRecordNoSelectionRecordCommand';
|
||||
@@ -7,10 +15,6 @@ import { CreateNewViewNoSelectionRecordCommand } from '@/command-menu-item/engin
|
||||
import { HideDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/HideDeletedRecordsNoSelectionRecordCommand';
|
||||
import { ImportRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/ImportRecordsNoSelectionRecordCommand';
|
||||
import { SeeDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/SeeDeletedRecordsNoSelectionRecordCommand';
|
||||
import { DeleteRecordsCommand } from '@/command-menu-item/engine-command/record/components/DeleteRecordsCommand';
|
||||
import { DestroyRecordsCommand } from '@/command-menu-item/engine-command/record/components/DestroyRecordsCommand';
|
||||
import { ExportRecordsCommand } from '@/command-menu-item/engine-command/record/components/ExportRecordsCommand';
|
||||
import { RestoreRecordsCommand } from '@/command-menu-item/engine-command/record/components/RestoreRecordsCommand';
|
||||
import { AddToFavoritesSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand';
|
||||
import { ExportNoteSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/ExportNoteSingleRecordCommand';
|
||||
import { NavigateToNextRecordSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/NavigateToNextRecordSingleRecordCommand';
|
||||
@@ -20,6 +24,7 @@ import { CancelDashboardSingleRecordCommand } from '@/command-menu-item/engine-c
|
||||
import { DuplicateDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand';
|
||||
import { EditDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/EditDashboardSingleRecordCommand';
|
||||
import { SaveDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/SaveDashboardSingleRecordCommand';
|
||||
import { ReplyToEmailThreadCommand } from '@/command-menu-item/engine-command/record/single-record/message-thread/components/ReplyToEmailThreadCommand';
|
||||
import { EditRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand';
|
||||
import { SeeVersionWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeVersionWorkflowRunSingleRecordCommand';
|
||||
import { SeeWorkflowWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeWorkflowWorkflowRunSingleRecordCommand';
|
||||
@@ -38,10 +43,6 @@ import { SeeRunsWorkflowSingleRecordCommand } from '@/command-menu-item/engine-c
|
||||
import { SeeVersionsWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/SeeVersionsWorkflowSingleRecordCommand';
|
||||
import { TestWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand';
|
||||
import { TidyUpWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand';
|
||||
import { HeadlessFrontComponentRendererEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessFrontComponentRendererEngineCommand';
|
||||
import { TriggerWorkflowVersionEngineCommand } from '@/command-menu-item/engine-command/record/components/TriggerWorkflowVersionEngineCommand';
|
||||
import { ComposeEmailCommand } from '@/command-menu-item/engine-command/global/components/ComposeEmailCommand';
|
||||
import { ReplyToEmailThreadCommand } from '@/command-menu-item/engine-command/record/single-record/message-thread/components/ReplyToEmailThreadCommand';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { AppPath, SettingsPath, SidePanelPages } from 'twenty-shared/types';
|
||||
@@ -122,6 +123,9 @@ export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record<
|
||||
[EngineComponentKey.CANCEL_DASHBOARD_LAYOUT]: (
|
||||
<CancelDashboardSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.NAVIGATION]: <NavigationEngineCommand />,
|
||||
// TODO: Remove these keys once we have ran the migration command `upgrade:1-21:refactor-navigation-commands`
|
||||
// These keys are kept for backward compatibility during migration
|
||||
[EngineComponentKey.GO_TO_PEOPLE]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
|
||||
+1
@@ -49,6 +49,7 @@ const buildBaseContextApi = (
|
||||
targetedRecordsRule: { mode: 'selection', selectedRecordIds: [] },
|
||||
selectedRecords: [],
|
||||
graphqlFilter: null,
|
||||
payload: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ const buildHeadlessContextApi = (
|
||||
targetedRecordsRule: { mode: 'selection', selectedRecordIds: [] },
|
||||
selectedRecords: [],
|
||||
graphqlFilter: null,
|
||||
payload: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ const baseContextApi: HeadlessEngineCommandContextApi = {
|
||||
targetedRecordsRule: { mode: 'selection', selectedRecordIds: [] },
|
||||
selectedRecords: [],
|
||||
graphqlFilter: null,
|
||||
payload: null,
|
||||
};
|
||||
|
||||
jest.mock(
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ const buildHeadlessContextApi = (
|
||||
targetedRecordsRule: { mode: 'selection', selectedRecordIds: [] },
|
||||
selectedRecords: [],
|
||||
graphqlFilter: null,
|
||||
payload: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ type MountCommandParams = {
|
||||
workflowVersionId?: string;
|
||||
availabilityType?: CommandMenuItemAvailabilityType;
|
||||
availabilityObjectMetadataId?: string | null;
|
||||
payload?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export const useMountCommand = () => {
|
||||
@@ -36,11 +37,13 @@ export const useMountCommand = () => {
|
||||
workflowVersionId,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId,
|
||||
payload,
|
||||
}: MountCommandParams) => {
|
||||
const headlessEngineCommandContextApi = buildHeadlessCommandContextApi({
|
||||
store,
|
||||
contextStoreInstanceId,
|
||||
engineComponentKey,
|
||||
payload,
|
||||
});
|
||||
|
||||
const commandState = isDefined(frontComponentId)
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ export type HeadlessEngineCommandContextApi = {
|
||||
targetedRecordsRule: ContextStoreTargetedRecordsRule;
|
||||
selectedRecords: ObjectRecord[];
|
||||
graphqlFilter: Nullable<RecordGqlOperationFilter>;
|
||||
payload: Nullable<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
export type HeadlessFrontComponentCommandContextApi =
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ const baseContextApi: HeadlessCommandContextApi = {
|
||||
targetedRecordsRule: { mode: 'selection', selectedRecordIds: [] },
|
||||
selectedRecords: [],
|
||||
graphqlFilter: null,
|
||||
payload: null,
|
||||
};
|
||||
|
||||
describe('isHeadlessTriggerWorkflowVersionCommandContextApi', () => {
|
||||
|
||||
+3
@@ -20,10 +20,12 @@ export const buildHeadlessCommandContextApi = ({
|
||||
store,
|
||||
contextStoreInstanceId,
|
||||
engineComponentKey,
|
||||
payload,
|
||||
}: {
|
||||
store: Store;
|
||||
contextStoreInstanceId: string;
|
||||
engineComponentKey: EngineComponentKey;
|
||||
payload?: Record<string, unknown> | null;
|
||||
}): HeadlessEngineCommandContextApi => {
|
||||
const objectMetadataItemId = store.get(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
|
||||
@@ -115,5 +117,6 @@ export const buildHeadlessCommandContextApi = ({
|
||||
targetedRecordsRule,
|
||||
selectedRecords,
|
||||
graphqlFilter,
|
||||
payload: payload ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export const isObjectMetadataNavigationPayload = (
|
||||
payload: Record<string, unknown>,
|
||||
): payload is { objectMetadataItemId: string } =>
|
||||
'objectMetadataItemId' in payload &&
|
||||
typeof payload.objectMetadataItemId === 'string';
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export const isPathNavigationPayload = (
|
||||
payload: Record<string, unknown>,
|
||||
): payload is { path: string } =>
|
||||
'path' in payload && typeof payload.path === 'string';
|
||||
+1
@@ -16,6 +16,7 @@ export const COMMAND_MENU_ITEM_FRAGMENT = gql`
|
||||
shortLabel
|
||||
position
|
||||
isPinned
|
||||
payload
|
||||
hotKeys
|
||||
conditionalAvailabilityExpression
|
||||
availabilityType
|
||||
|
||||
+1
@@ -270,6 +270,7 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaceC
|
||||
icon: trigger.settings.icon ?? null,
|
||||
isPinned: trigger.settings.isPinned ?? false,
|
||||
position: 0,
|
||||
payload: null,
|
||||
hotKeys: null,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId: availabilityObjectMetadataId ?? null,
|
||||
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { v4, v5 } from 'uuid';
|
||||
|
||||
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,
|
||||
type WorkspaceCommandOptions,
|
||||
} from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { addPayloadCheckConstraintToCommandMenuItem } from 'src/database/typeorm/core/migrations/utils/1775129635528-add-payload-to-command-menu-item.util';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import {
|
||||
buildNavigationFlatCommandMenuItem,
|
||||
NAVIGATION_COMMAND_UUID_NAMESPACE,
|
||||
} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { seedCompareObjectMetadataForNavigationPosition } from 'src/engine/metadata-modules/flat-command-menu-item/utils/seed-compare-object-metadata-for-navigation-position.util';
|
||||
import { STANDARD_COMMAND_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const GO_TO_ENGINE_KEYS = [
|
||||
'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',
|
||||
];
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-21:refactor-navigation-commands',
|
||||
description:
|
||||
'Replace GO_TO_* command menu items with unified NAVIGATION engine key and payload',
|
||||
})
|
||||
export class RefactorNavigationCommandsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async run(
|
||||
passedParams: string[],
|
||||
options: WorkspaceCommandOptions,
|
||||
): Promise<void> {
|
||||
await super.run(passedParams, options);
|
||||
|
||||
if (options.workspaceId && options.workspaceId.size > 0) {
|
||||
this.logger.log(
|
||||
'Skipping CHECK constraint application: command was not launched for all workspaces',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
'[DRY RUN] Would apply CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
|
||||
try {
|
||||
await addPayloadCheckConstraintToCommandMenuItem(queryRunner);
|
||||
this.logger.log(
|
||||
'Successfully applied CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE',
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Refactoring navigation commands for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatCommandMenuItemMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatCommandMenuItemMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
const allCommandMenuItems = Object.values(
|
||||
flatCommandMenuItemMaps.byUniversalIdentifier,
|
||||
).filter(isDefined);
|
||||
|
||||
const standardAppCommandMenuItems = allCommandMenuItems.filter(
|
||||
(item) => item.applicationId === twentyStandardFlatApplication.id,
|
||||
);
|
||||
|
||||
const goToItemsToDelete = standardAppCommandMenuItems.filter((item) =>
|
||||
GO_TO_ENGINE_KEYS.includes(item.engineComponentKey),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] Would delete' : 'Deleting'} ${goToItemsToDelete.length} old GO_TO_* command(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const existingNavigationUniversalIdentifiers = new Set(
|
||||
allCommandMenuItems
|
||||
.filter(
|
||||
(item) => item.engineComponentKey === EngineComponentKey.NAVIGATION,
|
||||
)
|
||||
.map((item) => item.universalIdentifier),
|
||||
);
|
||||
|
||||
const activeObjects = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((objectMetadata) => objectMetadata.isActive)
|
||||
.sort(seedCompareObjectMetadataForNavigationPosition);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${activeObjects.length} active object(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const nonGoToItems = allCommandMenuItems.filter(
|
||||
(item) => !GO_TO_ENGINE_KEYS.includes(item.engineComponentKey),
|
||||
);
|
||||
|
||||
let nextPosition =
|
||||
nonGoToItems.reduce((max, item) => Math.max(max, item.position), -1) + 1;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = [];
|
||||
|
||||
for (const objectMetadata of activeObjects) {
|
||||
const universalIdentifier = v5(
|
||||
objectMetadata.universalIdentifier,
|
||||
NAVIGATION_COMMAND_UUID_NAMESPACE,
|
||||
);
|
||||
|
||||
if (existingNavigationUniversalIdentifiers.has(universalIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flatCommandMenuItemsToCreate.push(
|
||||
buildNavigationFlatCommandMenuItem({
|
||||
objectMetadata,
|
||||
commandMenuItemId: v4(),
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
workspaceId,
|
||||
position: nextPosition++,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const settingsUniversalIdentifier =
|
||||
STANDARD_COMMAND_MENU_ITEMS.goToSettings.universalIdentifier;
|
||||
|
||||
if (
|
||||
!existingNavigationUniversalIdentifiers.has(settingsUniversalIdentifier)
|
||||
) {
|
||||
flatCommandMenuItemsToCreate.push({
|
||||
id: v4(),
|
||||
universalIdentifier: settingsUniversalIdentifier,
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
workspaceId,
|
||||
label: 'Go to Settings',
|
||||
shortLabel: 'Settings',
|
||||
icon: 'IconSettings',
|
||||
position: nextPosition++,
|
||||
isPinned: false,
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: null,
|
||||
frontComponentId: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.NAVIGATION,
|
||||
payload: { path: '/settings/profile' },
|
||||
hotKeys: ['G', 'S'],
|
||||
workflowVersionId: null,
|
||||
availabilityObjectMetadataId: null,
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
goToItemsToDelete.length === 0 &&
|
||||
flatCommandMenuItemsToCreate.length === 0
|
||||
) {
|
||||
this.logger.log(
|
||||
`All NAVIGATION commands already exist for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] Would create' : 'Creating'} ${flatCommandMenuItemsToCreate.length} NAVIGATION command(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: flatCommandMenuItemsToCreate,
|
||||
flatEntityToDelete: goToItemsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to refactor navigation commands:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to refactor navigation commands for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully refactored navigation commands for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { AddComposeEmailCommandMenuItemCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-add-compose-email-command-menu-item.command';
|
||||
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-migrate-messaging-infrastructure-to-metadata.command';
|
||||
import { RefactorNavigationCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-refactor-navigation-commands.command';
|
||||
import { AddGlobalKeyValuePairUniqueIndexCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-add-global-key-value-pair-unique-index.command';
|
||||
import { BackfillDatasourceToWorkspaceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-datasource-to-workspace.command';
|
||||
import { BackfillMessageThreadSubjectCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command';
|
||||
@@ -59,6 +60,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
FixSelectAllCommandMenuItemsCommand,
|
||||
MigrateAiAgentTextToJsonResponseFormatCommand,
|
||||
UpdateEditLayoutCommandMenuItemLabelCommand,
|
||||
RefactorNavigationCommandsCommand,
|
||||
DropWorkspaceMessagingFksCommand,
|
||||
MigrateMessageFolderParentIdToExternalIdCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
@@ -73,6 +75,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
FixSelectAllCommandMenuItemsCommand,
|
||||
MigrateAiAgentTextToJsonResponseFormatCommand,
|
||||
UpdateEditLayoutCommandMenuItemLabelCommand,
|
||||
RefactorNavigationCommandsCommand,
|
||||
DropWorkspaceMessagingFksCommand,
|
||||
MigrateMessageFolderParentIdToExternalIdCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
|
||||
+4
@@ -24,7 +24,9 @@ import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/co
|
||||
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
|
||||
import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command';
|
||||
|
||||
import { AddComposeEmailCommandMenuItemCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-add-compose-email-command-menu-item.command';
|
||||
import { RefactorNavigationCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-refactor-navigation-commands.command';
|
||||
import { AddGlobalKeyValuePairUniqueIndexCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-add-global-key-value-pair-unique-index.command';
|
||||
import { BackfillDatasourceToWorkspaceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-datasource-to-workspace.command';
|
||||
import { BackfillMessageThreadSubjectCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command';
|
||||
@@ -85,6 +87,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
private readonly fixSelectAllCommandMenuItemsCommand: FixSelectAllCommandMenuItemsCommand,
|
||||
private readonly migrateAiAgentTextToJsonResponseFormatCommand: MigrateAiAgentTextToJsonResponseFormatCommand,
|
||||
private readonly updateEditLayoutCommandMenuItemLabelCommand: UpdateEditLayoutCommandMenuItemLabelCommand,
|
||||
private readonly refactorNavigationCommandsCommand: RefactorNavigationCommandsCommand,
|
||||
private readonly dropWorkspaceMessagingFksCommand: DropWorkspaceMessagingFksCommand,
|
||||
private readonly migrateMessageFolderParentIdToExternalIdCommand: MigrateMessageFolderParentIdToExternalIdCommand,
|
||||
) {
|
||||
@@ -129,6 +132,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.fixSelectAllCommandMenuItemsCommand,
|
||||
this.migrateAiAgentTextToJsonResponseFormatCommand,
|
||||
this.updateEditLayoutCommandMenuItemLabelCommand,
|
||||
this.refactorNavigationCommandsCommand,
|
||||
this.dropWorkspaceMessagingFksCommand,
|
||||
this.migrateMessageFolderParentIdToExternalIdCommand,
|
||||
];
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
import { addPayloadCheckConstraintToCommandMenuItem } from 'src/database/typeorm/core/migrations/utils/1775129635528-add-payload-to-command-menu-item.util';
|
||||
|
||||
export class AddPayloadToCommandMenuItem1775129635528
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddPayloadToCommandMenuItem1775129635528';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."commandMenuItem" ADD "payload" jsonb`,
|
||||
);
|
||||
|
||||
const savepointName =
|
||||
'sp_add_payload_check_constraint_to_command_menu_item';
|
||||
|
||||
try {
|
||||
await queryRunner.query(`SAVEPOINT ${savepointName}`);
|
||||
|
||||
await addPayloadCheckConstraintToCommandMenuItem(queryRunner);
|
||||
|
||||
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
|
||||
} catch (e) {
|
||||
try {
|
||||
await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
||||
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
|
||||
} catch (rollbackError) {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(
|
||||
'Failed to rollback to savepoint in AddPayloadToCommandMenuItem1775129635528',
|
||||
rollbackError,
|
||||
);
|
||||
throw rollbackError;
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(
|
||||
'Swallowing AddPayloadToCommandMenuItem1775129635528 error',
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."commandMenuItem" DROP CONSTRAINT IF EXISTS "CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."commandMenuItem" ADD CONSTRAINT "CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE" CHECK (("engineComponentKey" = 'TRIGGER_WORKFLOW_VERSION' AND "workflowVersionId" IS NOT NULL AND "frontComponentId" IS NULL) OR ("engineComponentKey" = 'FRONT_COMPONENT_RENDERER' AND "frontComponentId" IS NOT NULL AND "workflowVersionId" IS NULL) OR ("engineComponentKey" NOT IN ('TRIGGER_WORKFLOW_VERSION', 'FRONT_COMPONENT_RENDERER') AND "workflowVersionId" IS NULL AND "frontComponentId" IS NULL))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."commandMenuItem" DROP COLUMN "payload"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
export const addPayloadCheckConstraintToCommandMenuItem = async (
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> => {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."commandMenuItem" DROP CONSTRAINT IF EXISTS "CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."commandMenuItem" ADD CONSTRAINT "CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE" CHECK (("engineComponentKey" = 'TRIGGER_WORKFLOW_VERSION' AND "workflowVersionId" IS NOT NULL AND "frontComponentId" IS NULL AND "payload" IS NULL) OR ("engineComponentKey" = 'FRONT_COMPONENT_RENDERER' AND "frontComponentId" IS NOT NULL AND "workflowVersionId" IS NULL AND "payload" IS NULL) OR ("engineComponentKey" = 'NAVIGATION' AND "payload" IS NOT NULL AND "workflowVersionId" IS NULL AND "frontComponentId" IS NULL) OR ("engineComponentKey" NOT IN ('TRIGGER_WORKFLOW_VERSION', 'FRONT_COMPONENT_RENDERER', 'NAVIGATION') AND "workflowVersionId" IS NULL AND "frontComponentId" IS NULL AND "payload" IS NULL))`,
|
||||
);
|
||||
};
|
||||
+1
@@ -40,6 +40,7 @@ export const fromCommandMenuItemManifestToUniversalFlatCommandMenuItem = ({
|
||||
availabilityObjectMetadataUniversalIdentifier:
|
||||
commandMenuItemManifest.availabilityObjectUniversalIdentifier ?? null,
|
||||
engineComponentKey: EngineComponentKey.FRONT_COMPONENT_RENDERER,
|
||||
payload: null,
|
||||
hotKeys: null,
|
||||
workflowVersionId: null,
|
||||
createdAt: now,
|
||||
|
||||
+5
@@ -10,6 +10,7 @@ import {
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
@@ -68,6 +69,10 @@ export class CommandMenuItemDTO {
|
||||
@Field(() => CommandMenuItemAvailabilityType)
|
||||
availabilityType: CommandMenuItemAvailabilityType;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
payload?: Record<string, unknown>;
|
||||
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
@Field(() => [String], { nullable: true })
|
||||
|
||||
+7
@@ -5,10 +5,12 @@ import {
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
@@ -75,4 +77,9 @@ export class CreateCommandMenuItemInput {
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
availabilityObjectMetadataId?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
payload?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
+4
-1
@@ -31,7 +31,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
])
|
||||
@Check(
|
||||
'CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE',
|
||||
`("engineComponentKey" = 'TRIGGER_WORKFLOW_VERSION' AND "workflowVersionId" IS NOT NULL AND "frontComponentId" IS NULL) OR ("engineComponentKey" = 'FRONT_COMPONENT_RENDERER' AND "frontComponentId" IS NOT NULL AND "workflowVersionId" IS NULL) OR ("engineComponentKey" NOT IN ('TRIGGER_WORKFLOW_VERSION', 'FRONT_COMPONENT_RENDERER') AND "workflowVersionId" IS NULL AND "frontComponentId" IS NULL)`,
|
||||
`("engineComponentKey" = 'TRIGGER_WORKFLOW_VERSION' AND "workflowVersionId" IS NOT NULL AND "frontComponentId" IS NULL AND "payload" IS NULL) OR ("engineComponentKey" = 'FRONT_COMPONENT_RENDERER' AND "frontComponentId" IS NOT NULL AND "workflowVersionId" IS NULL AND "payload" IS NULL) OR ("engineComponentKey" = 'NAVIGATION' AND "payload" IS NOT NULL AND "workflowVersionId" IS NULL AND "frontComponentId" IS NULL) OR ("engineComponentKey" NOT IN ('TRIGGER_WORKFLOW_VERSION', 'FRONT_COMPONENT_RENDERER', 'NAVIGATION') AND "workflowVersionId" IS NULL AND "frontComponentId" IS NULL AND "payload" IS NULL)`,
|
||||
)
|
||||
export class CommandMenuItemEntity
|
||||
extends SyncableEntity
|
||||
@@ -79,6 +79,9 @@ export class CommandMenuItemEntity
|
||||
})
|
||||
availabilityType: CommandMenuItemAvailabilityType;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
payload: Record<string, unknown> | null;
|
||||
|
||||
@Column({ type: 'text', array: true, nullable: true })
|
||||
hotKeys: string[] | null;
|
||||
|
||||
|
||||
+13
-9
@@ -18,19 +18,11 @@ export enum EngineComponentKey {
|
||||
SEE_DELETED_RECORDS = 'SEE_DELETED_RECORDS',
|
||||
CREATE_NEW_VIEW = 'CREATE_NEW_VIEW',
|
||||
HIDE_DELETED_RECORDS = 'HIDE_DELETED_RECORDS',
|
||||
GO_TO_PEOPLE = 'GO_TO_PEOPLE',
|
||||
GO_TO_COMPANIES = 'GO_TO_COMPANIES',
|
||||
GO_TO_DASHBOARDS = 'GO_TO_DASHBOARDS',
|
||||
GO_TO_OPPORTUNITIES = 'GO_TO_OPPORTUNITIES',
|
||||
GO_TO_SETTINGS = 'GO_TO_SETTINGS',
|
||||
GO_TO_TASKS = 'GO_TO_TASKS',
|
||||
GO_TO_NOTES = 'GO_TO_NOTES',
|
||||
EDIT_RECORD_PAGE_LAYOUT = 'EDIT_RECORD_PAGE_LAYOUT',
|
||||
EDIT_DASHBOARD_LAYOUT = 'EDIT_DASHBOARD_LAYOUT',
|
||||
SAVE_DASHBOARD_LAYOUT = 'SAVE_DASHBOARD_LAYOUT',
|
||||
CANCEL_DASHBOARD_LAYOUT = 'CANCEL_DASHBOARD_LAYOUT',
|
||||
DUPLICATE_DASHBOARD = 'DUPLICATE_DASHBOARD',
|
||||
GO_TO_WORKFLOWS = 'GO_TO_WORKFLOWS',
|
||||
ACTIVATE_WORKFLOW = 'ACTIVATE_WORKFLOW',
|
||||
DEACTIVATE_WORKFLOW = 'DEACTIVATE_WORKFLOW',
|
||||
DISCARD_DRAFT_WORKFLOW = 'DISCARD_DRAFT_WORKFLOW',
|
||||
@@ -41,7 +33,6 @@ export enum EngineComponentKey {
|
||||
ADD_NODE_WORKFLOW = 'ADD_NODE_WORKFLOW',
|
||||
TIDY_UP_WORKFLOW = 'TIDY_UP_WORKFLOW',
|
||||
DUPLICATE_WORKFLOW = 'DUPLICATE_WORKFLOW',
|
||||
GO_TO_RUNS = 'GO_TO_RUNS',
|
||||
SEE_VERSION_WORKFLOW_RUN = 'SEE_VERSION_WORKFLOW_RUN',
|
||||
SEE_WORKFLOW_WORKFLOW_RUN = 'SEE_WORKFLOW_WORKFLOW_RUN',
|
||||
STOP_WORKFLOW_RUN = 'STOP_WORKFLOW_RUN',
|
||||
@@ -53,11 +44,24 @@ export enum EngineComponentKey {
|
||||
SEARCH_RECORDS_FALLBACK = 'SEARCH_RECORDS_FALLBACK',
|
||||
ASK_AI = 'ASK_AI',
|
||||
VIEW_PREVIOUS_AI_CHATS = 'VIEW_PREVIOUS_AI_CHATS',
|
||||
NAVIGATION = 'NAVIGATION',
|
||||
TRIGGER_WORKFLOW_VERSION = 'TRIGGER_WORKFLOW_VERSION',
|
||||
FRONT_COMPONENT_RENDERER = 'FRONT_COMPONENT_RENDERER',
|
||||
REPLY_TO_EMAIL_THREAD = 'REPLY_TO_EMAIL_THREAD',
|
||||
COMPOSE_EMAIL = 'COMPOSE_EMAIL',
|
||||
|
||||
// TODO: Remove deprecated keys once upgrade:1-21:refactor-navigation-commands has run on all workspaces
|
||||
// Deprecated: replaced by NAVIGATION engine key with payload
|
||||
GO_TO_PEOPLE = 'GO_TO_PEOPLE',
|
||||
GO_TO_COMPANIES = 'GO_TO_COMPANIES',
|
||||
GO_TO_DASHBOARDS = 'GO_TO_DASHBOARDS',
|
||||
GO_TO_OPPORTUNITIES = 'GO_TO_OPPORTUNITIES',
|
||||
GO_TO_SETTINGS = 'GO_TO_SETTINGS',
|
||||
GO_TO_TASKS = 'GO_TO_TASKS',
|
||||
GO_TO_NOTES = 'GO_TO_NOTES',
|
||||
GO_TO_WORKFLOWS = 'GO_TO_WORKFLOWS',
|
||||
GO_TO_RUNS = 'GO_TO_RUNS',
|
||||
|
||||
// Deprecated keys kept for backward compatibility until migration runs
|
||||
DELETE_SINGLE_RECORD = 'DELETE_SINGLE_RECORD',
|
||||
DELETE_MULTIPLE_RECORDS = 'DELETE_MULTIPLE_RECORDS',
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { v5 } from 'uuid';
|
||||
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { buildNavigationFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
const NAVIGATION_COMMAND_UUID_NAMESPACE =
|
||||
'b31830da-2ae0-48eb-a915-12fa4ab96dd3';
|
||||
|
||||
const baseObjectMetadata = {
|
||||
id: 'obj-id-1',
|
||||
universalIdentifier: 'obj-universal-1',
|
||||
labelPlural: 'People',
|
||||
icon: 'IconUser',
|
||||
nameSingular: 'person',
|
||||
shortcut: 'P',
|
||||
};
|
||||
|
||||
const baseArgs = {
|
||||
objectMetadata: baseObjectMetadata,
|
||||
commandMenuItemId: 'cmd-id-1',
|
||||
applicationId: 'app-id-1',
|
||||
workspaceId: 'ws-id-1',
|
||||
position: 5,
|
||||
now: '2026-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
describe('buildNavigationFlatCommandMenuItem', () => {
|
||||
it('should produce a deterministic universalIdentifier via UUID v5', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
const expectedUniversalIdentifier = v5(
|
||||
baseObjectMetadata.universalIdentifier,
|
||||
NAVIGATION_COMMAND_UUID_NAMESPACE,
|
||||
);
|
||||
|
||||
expect(result.universalIdentifier).toBe(expectedUniversalIdentifier);
|
||||
});
|
||||
|
||||
it('should set label and shortLabel from objectMetadata', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.label).toBe('Go to People');
|
||||
expect(result.shortLabel).toBe('People');
|
||||
});
|
||||
|
||||
it('should set icon from objectMetadata', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.icon).toBe('IconUser');
|
||||
});
|
||||
|
||||
it('should set payload with objectMetadataItemId', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.payload).toEqual({ objectMetadataItemId: 'obj-id-1' });
|
||||
});
|
||||
|
||||
it('should include shortcut in hotKeys when shortcut is defined', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.hotKeys).toEqual(['G', 'P']);
|
||||
});
|
||||
|
||||
it('should set hotKeys to null when shortcut is null', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem({
|
||||
...baseArgs,
|
||||
objectMetadata: { ...baseObjectMetadata, shortcut: null },
|
||||
});
|
||||
|
||||
expect(result.hotKeys).toBeNull();
|
||||
});
|
||||
|
||||
it('should use the provided id, applicationId, workspaceId, and position', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.id).toBe('cmd-id-1');
|
||||
expect(result.applicationId).toBe('app-id-1');
|
||||
expect(result.workspaceId).toBe('ws-id-1');
|
||||
expect(result.position).toBe(5);
|
||||
});
|
||||
|
||||
it('should set applicationUniversalIdentifier from TWENTY_STANDARD_APPLICATION', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.applicationUniversalIdentifier).toBe(
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set engineComponentKey to NAVIGATION', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.engineComponentKey).toBe(EngineComponentKey.NAVIGATION);
|
||||
});
|
||||
|
||||
it('should set availabilityType to GLOBAL', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.availabilityType).toBe(
|
||||
CommandMenuItemAvailabilityType.GLOBAL,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set conditionalAvailabilityExpression based on nameSingular', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.conditionalAvailabilityExpression).toBe(
|
||||
'targetObjectReadPermissions.person',
|
||||
);
|
||||
});
|
||||
|
||||
it('should set isPinned to false', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.isPinned).toBe(false);
|
||||
});
|
||||
|
||||
it('should set null fields correctly', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.frontComponentId).toBeNull();
|
||||
expect(result.frontComponentUniversalIdentifier).toBeNull();
|
||||
expect(result.workflowVersionId).toBeNull();
|
||||
expect(result.availabilityObjectMetadataId).toBeNull();
|
||||
expect(result.availabilityObjectMetadataUniversalIdentifier).toBeNull();
|
||||
});
|
||||
|
||||
it('should set createdAt and updatedAt from the now parameter', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.createdAt).toBe('2026-01-01T00:00:00.000Z');
|
||||
expect(result.updatedAt).toBe('2026-01-01T00:00:00.000Z');
|
||||
});
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v5 } from 'uuid';
|
||||
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
export const NAVIGATION_COMMAND_UUID_NAMESPACE =
|
||||
'b31830da-2ae0-48eb-a915-12fa4ab96dd3';
|
||||
|
||||
export const buildNavigationFlatCommandMenuItem = ({
|
||||
objectMetadata,
|
||||
commandMenuItemId,
|
||||
applicationId,
|
||||
workspaceId,
|
||||
position,
|
||||
now,
|
||||
}: {
|
||||
objectMetadata: {
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
labelPlural: string;
|
||||
icon: string | null;
|
||||
nameSingular: string;
|
||||
shortcut: string | null;
|
||||
};
|
||||
commandMenuItemId: string;
|
||||
applicationId: string;
|
||||
workspaceId: string;
|
||||
position: number;
|
||||
now: string;
|
||||
}): FlatCommandMenuItem => {
|
||||
const universalIdentifier = v5(
|
||||
objectMetadata.universalIdentifier,
|
||||
NAVIGATION_COMMAND_UUID_NAMESPACE,
|
||||
);
|
||||
|
||||
return {
|
||||
id: commandMenuItemId,
|
||||
universalIdentifier,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
workspaceId,
|
||||
label: `Go to ${objectMetadata.labelPlural}`,
|
||||
shortLabel: objectMetadata.labelPlural,
|
||||
icon: objectMetadata.icon,
|
||||
position,
|
||||
isPinned: false,
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: `targetObjectReadPermissions.${objectMetadata.nameSingular}`,
|
||||
frontComponentId: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.NAVIGATION,
|
||||
payload: { objectMetadataItemId: objectMetadata.id },
|
||||
hotKeys: isDefined(objectMetadata.shortcut)
|
||||
? ['G', objectMetadata.shortcut]
|
||||
: null,
|
||||
workflowVersionId: null,
|
||||
availabilityObjectMetadataId: null,
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
};
|
||||
+1
@@ -67,6 +67,7 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
|
||||
shortLabel: commandMenuItemEntity.shortLabel,
|
||||
position: commandMenuItemEntity.position,
|
||||
isPinned: commandMenuItemEntity.isPinned,
|
||||
payload: commandMenuItemEntity.payload,
|
||||
hotKeys: commandMenuItemEntity.hotKeys,
|
||||
availabilityType: commandMenuItemEntity.availabilityType,
|
||||
availabilityObjectMetadataId:
|
||||
|
||||
+6
@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
@@ -49,6 +50,11 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
|
||||
shortLabel: createCommandMenuItemInput.shortLabel ?? null,
|
||||
position: createCommandMenuItemInput.position ?? 0,
|
||||
isPinned: createCommandMenuItemInput.isPinned ?? false,
|
||||
payload:
|
||||
createCommandMenuItemInput.engineComponentKey ===
|
||||
EngineComponentKey.NAVIGATION
|
||||
? (createCommandMenuItemInput.payload ?? null)
|
||||
: null,
|
||||
hotKeys: createCommandMenuItemInput.hotKeys ?? null,
|
||||
availabilityType:
|
||||
createCommandMenuItemInput.availabilityType ??
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ export const fromFlatCommandMenuItemToCommandMenuItemDto = (
|
||||
shortLabel: flatCommandMenuItem.shortLabel ?? undefined,
|
||||
position: flatCommandMenuItem.position,
|
||||
isPinned: flatCommandMenuItem.isPinned,
|
||||
payload: flatCommandMenuItem.payload ?? undefined,
|
||||
hotKeys: flatCommandMenuItem.hotKeys ?? undefined,
|
||||
availabilityType: flatCommandMenuItem.availabilityType,
|
||||
conditionalAvailabilityExpression:
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export const seedCompareObjectMetadataForNavigationPosition = (
|
||||
a: { isSystem: boolean },
|
||||
b: { isSystem: boolean },
|
||||
): number => {
|
||||
if (a.isSystem !== b.isSystem) {
|
||||
return a.isSystem ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
+2
@@ -31,9 +31,11 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"conditionalAvailabilityExpression",
|
||||
"availabilityObjectMetadataUniversalIdentifier",
|
||||
"engineComponentKey",
|
||||
"payload",
|
||||
"hotKeys",
|
||||
],
|
||||
"propertiesToStringify": [
|
||||
"payload",
|
||||
"hotKeys",
|
||||
],
|
||||
},
|
||||
|
||||
+5
@@ -1092,6 +1092,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
payload: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
hotKeys: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
|
||||
+15
-128
@@ -254,105 +254,6 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
|
||||
engineComponentKey: EngineComponentKey.HIDE_DELETED_RECORDS,
|
||||
hotKeys: null,
|
||||
},
|
||||
goToPeople: {
|
||||
universalIdentifier: 'dfe5fef8-d42c-40f0-941f-8e3b5eb01daa',
|
||||
label: 'Go to People',
|
||||
icon: 'IconUser',
|
||||
isPinned: false,
|
||||
position: 23,
|
||||
shortLabel: 'People',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: 'targetObjectReadPermissions.person',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_PEOPLE,
|
||||
hotKeys: ['G', 'P'],
|
||||
},
|
||||
goToCompanies: {
|
||||
universalIdentifier: '196e4eec-bfdd-48a6-bcbb-6707ef11951a',
|
||||
label: 'Go to Companies',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isPinned: false,
|
||||
position: 24,
|
||||
shortLabel: 'Companies',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: 'targetObjectReadPermissions.company',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_COMPANIES,
|
||||
hotKeys: ['G', 'C'],
|
||||
},
|
||||
goToDashboards: {
|
||||
universalIdentifier: '11dc07d1-21b2-4f86-af8c-6a664c02f00c',
|
||||
label: 'Go to Dashboards',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: false,
|
||||
position: 25,
|
||||
shortLabel: 'Dashboards',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: 'targetObjectReadPermissions.dashboard',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_DASHBOARDS,
|
||||
hotKeys: ['G', 'D'],
|
||||
},
|
||||
goToOpportunities: {
|
||||
universalIdentifier: 'f04f5e00-a208-422f-acf2-ff189769510d',
|
||||
label: 'Go to Opportunities',
|
||||
icon: 'IconTargetArrow',
|
||||
isPinned: false,
|
||||
position: 26,
|
||||
shortLabel: 'Opportunities',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression:
|
||||
'targetObjectReadPermissions.opportunity',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_OPPORTUNITIES,
|
||||
hotKeys: ['G', 'O'],
|
||||
},
|
||||
goToSettings: {
|
||||
universalIdentifier: 'ef9aba44-0068-453e-930a-f8c182af18ee',
|
||||
label: 'Go to Settings',
|
||||
icon: 'IconSettings',
|
||||
isPinned: false,
|
||||
position: 27,
|
||||
shortLabel: 'Settings',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: null,
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_SETTINGS,
|
||||
hotKeys: ['G', 'S'],
|
||||
},
|
||||
goToTasks: {
|
||||
universalIdentifier: 'e8e3bd0b-5ce9-4577-bb61-588c0b1ad063',
|
||||
label: 'Go to Tasks',
|
||||
icon: 'IconCheckbox',
|
||||
isPinned: false,
|
||||
position: 28,
|
||||
shortLabel: 'Tasks',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: 'targetObjectReadPermissions.task',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_TASKS,
|
||||
hotKeys: ['G', 'T'],
|
||||
},
|
||||
goToNotes: {
|
||||
universalIdentifier: '08e0f0cc-ac2d-46cd-9bca-39a409b9addf',
|
||||
label: 'Go to Notes',
|
||||
icon: 'IconCheckbox',
|
||||
isPinned: false,
|
||||
position: 29,
|
||||
shortLabel: 'Notes',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: 'targetObjectReadPermissions.note',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_NOTES,
|
||||
hotKeys: ['G', 'N'],
|
||||
},
|
||||
editRecordPageLayout: {
|
||||
universalIdentifier: 'd9794c67-1799-424f-8871-5ea771dd4a6d',
|
||||
label: 'Edit Layout',
|
||||
@@ -432,20 +333,6 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
|
||||
engineComponentKey: EngineComponentKey.DUPLICATE_DASHBOARD,
|
||||
hotKeys: null,
|
||||
},
|
||||
goToWorkflows: {
|
||||
universalIdentifier: '4fa778f9-7931-4d18-b895-929e1ef9c31f',
|
||||
label: 'Go to Workflows',
|
||||
icon: 'IconSettingsAutomation',
|
||||
isPinned: false,
|
||||
position: 41,
|
||||
shortLabel: 'Workflows',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: 'targetObjectReadPermissions.workflow',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_WORKFLOWS,
|
||||
hotKeys: ['G', 'W'],
|
||||
},
|
||||
activateWorkflow: {
|
||||
universalIdentifier: '44f19c85-0fd0-482f-a14e-da513c60b1b3',
|
||||
label: 'Activate Workflow',
|
||||
@@ -606,21 +493,6 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
|
||||
engineComponentKey: EngineComponentKey.DUPLICATE_WORKFLOW,
|
||||
hotKeys: null,
|
||||
},
|
||||
goToRuns: {
|
||||
universalIdentifier: '1ba959da-ff49-4c1f-a517-2b78ee200508',
|
||||
label: 'Go to Runs',
|
||||
icon: 'IconHistoryToggle',
|
||||
isPinned: false,
|
||||
position: 52,
|
||||
shortLabel: 'See Runs',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression:
|
||||
'not hasAnySoftDeleteFilterOnView and objectMetadataItem.nameSingular != "workflowRun" and objectMetadataItem.nameSingular != "workflowVersion" and objectMetadataItem.nameSingular != "workflow"',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.GO_TO_RUNS,
|
||||
hotKeys: null,
|
||||
},
|
||||
seeVersionWorkflowRun: {
|
||||
universalIdentifier: 'cc3a065c-c89e-40ac-9449-4272c55b1bb8',
|
||||
label: 'See Version',
|
||||
@@ -816,4 +688,19 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
|
||||
engineComponentKey: EngineComponentKey.COMPOSE_EMAIL,
|
||||
hotKeys: null,
|
||||
},
|
||||
goToSettings: {
|
||||
universalIdentifier: 'ef9aba44-0068-453e-930a-f8c182af18ee',
|
||||
label: 'Go to Settings',
|
||||
icon: 'IconSettings',
|
||||
isPinned: false,
|
||||
position: 27,
|
||||
shortLabel: 'Settings',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
conditionalAvailabilityExpression: null,
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.NAVIGATION,
|
||||
hotKeys: ['G', 'S'],
|
||||
payload: { path: '/settings/profile' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
+37
@@ -1,6 +1,9 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatCommandMenuItemMaps } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item-maps.type';
|
||||
import { buildNavigationFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { seedCompareObjectMetadataForNavigationPosition } from 'src/engine/metadata-modules/flat-command-menu-item/utils/seed-compare-object-metadata-for-navigation-position.util';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
@@ -46,5 +49,39 @@ export const buildStandardFlatCommandMenuItemMaps = ({
|
||||
});
|
||||
}
|
||||
|
||||
const activeObjects = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((flatObject) => flatObject.isActive)
|
||||
.sort(seedCompareObjectMetadataForNavigationPosition);
|
||||
|
||||
const maxStandardPosition = Object.values(
|
||||
flatCommandMenuItemMaps.byUniversalIdentifier,
|
||||
).reduce(
|
||||
(max, item) => (isDefined(item) ? Math.max(max, item.position) : max),
|
||||
-1,
|
||||
);
|
||||
|
||||
let nextPosition = maxStandardPosition + 1;
|
||||
|
||||
for (const flatObject of activeObjects) {
|
||||
const position = nextPosition++;
|
||||
|
||||
const navigationItem = buildNavigationFlatCommandMenuItem({
|
||||
objectMetadata: flatObject,
|
||||
commandMenuItemId: v4(),
|
||||
applicationId: twentyStandardApplicationId,
|
||||
workspaceId,
|
||||
position,
|
||||
now,
|
||||
});
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: navigationItem,
|
||||
flatEntityMapsToMutate: flatCommandMenuItemMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatCommandMenuItemMaps;
|
||||
};
|
||||
|
||||
+4
@@ -65,6 +65,10 @@ export const createStandardCommandMenuItemFlatMetadata = ({
|
||||
frontComponentId: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: definition.engineComponentKey,
|
||||
payload:
|
||||
'payload' in definition && isDefined(definition.payload)
|
||||
? { ...definition.payload }
|
||||
: null,
|
||||
hotKeys: definition.hotKeys ? [...definition.hotKeys] : null,
|
||||
workflowVersionId: null,
|
||||
availabilityObjectMetadataId: resolvedObjectMetadataId,
|
||||
|
||||
+4
-2
@@ -1,9 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
|
||||
import {
|
||||
FlatUpdateCommandMenuItemAction,
|
||||
@@ -56,7 +58,7 @@ export class UpdateCommandMenuItemActionHandlerService extends WorkspaceMigratio
|
||||
|
||||
await commandMenuItemRepository.update(
|
||||
{ id: entityId, workspaceId },
|
||||
update,
|
||||
update as QueryDeepPartialEntity<CommandMenuItemEntity>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user