Refactor: Unify command menu item execution (#18908)

- Makes engineComponentKey non-nullable on CommandMenuItem. Adds two new
engine component keys: TRIGGER_WORKFLOW_VERSION and
FRONT_COMPONENT_RENDERER to cover the former standalone cases.
- Unifies the previously separated engine, front component and workflow
runners into a single `CommandRunner` component with a unified
`useMountCommand` hook that handles all command types.
This commit is contained in:
Raphaël Bosi
2026-03-24 16:59:52 +01:00
committed by GitHub
parent 27c0ca975f
commit 49afe5dbc4
103 changed files with 1296 additions and 1131 deletions
File diff suppressed because one or more lines are too long
@@ -19,8 +19,7 @@ import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-compo
import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider';
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
import { HeadlessEngineCommandMountRoot } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandMountRoot';
import { HeadlessFrontComponentMountRoot } from '@/front-components/components/HeadlessFrontComponentMountRoot';
import { CommandRunner } from '@/command-menu-item/engine-command/components/CommandRunner';
import { SSEProvider } from '@/sse-db-event/components/SSEProvider';
import { SupportChatEffect } from '@/support/components/SupportChatEffect';
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
@@ -71,8 +70,7 @@ export const AppRouterProviders = () => {
<Outlet />
<GlobalFilePreviewModal />
<CommandMenuConfirmationModalManager />
<HeadlessFrontComponentMountRoot />
<HeadlessEngineCommandMountRoot />
<CommandRunner />
</StrictMode>
</DialogManager>
</DialogComponentInstanceContext.Provider>
@@ -1,254 +0,0 @@
import { CommandLink } from '@/command-menu-item/display/components/CommandLink';
import { CommandMenuItemOpenSidePanelPage } from '@/command-menu-item/display/components/CommandMenuItemOpenSidePanelPage';
import { DeleteMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/DeleteMultipleRecordsCommand';
import { DestroyMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/DestroyMultipleRecordsCommand';
import { ExportMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/ExportMultipleRecordsCommand';
import { MergeMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/MergeMultipleRecordsCommand';
import { RestoreMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/RestoreMultipleRecordsCommand';
import { UpdateMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/UpdateMultipleRecordsCommand';
import { CreateNewIndexRecordNoSelectionRecordCommand } from '@/command-menu-item/record/no-selection/components/CreateNewIndexRecordNoSelectionRecordCommand';
import { CreateNewViewNoSelectionRecordCommand } from '@/command-menu-item/record/no-selection/components/CreateNewViewNoSelectionRecordCommand';
import { HideDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/record/no-selection/components/HideDeletedRecordsNoSelectionRecordCommand';
import { ImportRecordsNoSelectionRecordCommand } from '@/command-menu-item/record/no-selection/components/ImportRecordsNoSelectionRecordCommand';
import { SeeDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/record/no-selection/components/SeeDeletedRecordsNoSelectionRecordCommand';
import { AddToFavoritesSingleRecordCommand } from '@/command-menu-item/record/single-record/components/AddToFavoritesSingleRecordCommand';
import { DeleteSingleRecordCommand } from '@/command-menu-item/record/single-record/components/DeleteSingleRecordCommand';
import { DestroySingleRecordCommand } from '@/command-menu-item/record/single-record/components/DestroySingleRecordCommand';
import { ExportNoteSingleRecordCommand } from '@/command-menu-item/record/single-record/components/ExportNoteSingleRecordCommand';
import { ExportSingleRecordCommand } from '@/command-menu-item/record/single-record/components/ExportSingleRecordCommand';
import { NavigateToNextRecordSingleRecordCommand } from '@/command-menu-item/record/single-record/components/NavigateToNextRecordSingleRecordCommand';
import { NavigateToPreviousRecordSingleRecordCommand } from '@/command-menu-item/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand';
import { RemoveFromFavoritesSingleRecordCommand } from '@/command-menu-item/record/single-record/components/RemoveFromFavoritesSingleRecordCommand';
import { RestoreSingleRecordCommand } from '@/command-menu-item/record/single-record/components/RestoreSingleRecordCommand';
import { CancelDashboardSingleRecordCommand } from '@/command-menu-item/record/single-record/dashboard/components/CancelDashboardSingleRecordCommand';
import { DuplicateDashboardSingleRecordCommand } from '@/command-menu-item/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand';
import { EditDashboardSingleRecordCommand } from '@/command-menu-item/record/single-record/dashboard/components/EditDashboardSingleRecordCommand';
import { SaveDashboardSingleRecordCommand } from '@/command-menu-item/record/single-record/dashboard/components/SaveDashboardSingleRecordCommand';
import { EditRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand';
import { SeeVersionWorkflowRunSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-runs/components/SeeVersionWorkflowRunSingleRecordCommand';
import { SeeWorkflowWorkflowRunSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-runs/components/SeeWorkflowWorkflowRunSingleRecordCommand';
import { StopWorkflowRunSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand';
import { SeeRunsWorkflowVersionSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-versions/components/SeeRunsWorkflowVersionSingleRecordCommand';
import { SeeVersionsWorkflowVersionSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-versions/components/SeeVersionsWorkflowVersionSingleRecordCommand';
import { SeeWorkflowWorkflowVersionSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-versions/components/SeeWorkflowWorkflowVersionSingleRecordCommand';
import { UseAsDraftWorkflowVersionSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-versions/components/UseAsDraftWorkflowVersionSingleRecordCommand';
import { ActivateWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand';
import { AddNodeWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/AddNodeWorkflowSingleRecordCommand';
import { DeactivateWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand';
import { DiscardDraftWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand';
import { DuplicateWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/DuplicateWorkflowSingleRecordCommand';
import { SeeActiveVersionWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/SeeActiveVersionWorkflowSingleRecordCommand';
import { SeeRunsWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/SeeRunsWorkflowSingleRecordCommand';
import { SeeVersionsWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/SeeVersionsWorkflowSingleRecordCommand';
import { TestWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/TestWorkflowSingleRecordCommand';
import { TidyUpWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { msg } from '@lingui/core/macro';
import { AppPath, SettingsPath, SidePanelPages } from 'twenty-shared/types';
import { IconHistory, IconSearch, IconSparkles } from 'twenty-ui/display';
import { EngineComponentKey } from '~/generated-metadata/graphql';
export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record<
EngineComponentKey,
React.ReactNode
> = {
[EngineComponentKey.CREATE_NEW_RECORD]: (
<CreateNewIndexRecordNoSelectionRecordCommand />
),
[EngineComponentKey.DELETE_SINGLE_RECORD]: <DeleteSingleRecordCommand />,
[EngineComponentKey.DELETE_MULTIPLE_RECORDS]: (
<DeleteMultipleRecordsCommand />
),
[EngineComponentKey.RESTORE_SINGLE_RECORD]: <RestoreSingleRecordCommand />,
[EngineComponentKey.RESTORE_MULTIPLE_RECORDS]: (
<RestoreMultipleRecordsCommand />
),
[EngineComponentKey.DESTROY_SINGLE_RECORD]: <DestroySingleRecordCommand />,
[EngineComponentKey.DESTROY_MULTIPLE_RECORDS]: (
<DestroyMultipleRecordsCommand />
),
[EngineComponentKey.ADD_TO_FAVORITES]: <AddToFavoritesSingleRecordCommand />,
[EngineComponentKey.REMOVE_FROM_FAVORITES]: (
<RemoveFromFavoritesSingleRecordCommand />
),
[EngineComponentKey.MERGE_MULTIPLE_RECORDS]: <MergeMultipleRecordsCommand />,
[EngineComponentKey.DUPLICATE_DASHBOARD]: (
<DuplicateDashboardSingleRecordCommand />
),
[EngineComponentKey.DUPLICATE_WORKFLOW]: (
<DuplicateWorkflowSingleRecordCommand />
),
[EngineComponentKey.ACTIVATE_WORKFLOW]: (
<ActivateWorkflowSingleRecordCommand />
),
[EngineComponentKey.DEACTIVATE_WORKFLOW]: (
<DeactivateWorkflowSingleRecordCommand />
),
[EngineComponentKey.DISCARD_DRAFT_WORKFLOW]: (
<DiscardDraftWorkflowSingleRecordCommand />
),
[EngineComponentKey.TEST_WORKFLOW]: <TestWorkflowSingleRecordCommand />,
[EngineComponentKey.STOP_WORKFLOW_RUN]: (
<StopWorkflowRunSingleRecordCommand />
),
[EngineComponentKey.USE_AS_DRAFT_WORKFLOW_VERSION]: (
<UseAsDraftWorkflowVersionSingleRecordCommand />
),
[EngineComponentKey.SAVE_DASHBOARD_LAYOUT]: (
<SaveDashboardSingleRecordCommand />
),
[EngineComponentKey.TIDY_UP_WORKFLOW]: <TidyUpWorkflowSingleRecordCommand />,
[EngineComponentKey.NAVIGATE_TO_NEXT_RECORD]: (
<NavigateToNextRecordSingleRecordCommand />
),
[EngineComponentKey.NAVIGATE_TO_PREVIOUS_RECORD]: (
<NavigateToPreviousRecordSingleRecordCommand />
),
[EngineComponentKey.EXPORT_NOTE_TO_PDF]: <ExportNoteSingleRecordCommand />,
[EngineComponentKey.EXPORT_FROM_RECORD_INDEX]: (
<ExportMultipleRecordsCommand />
),
[EngineComponentKey.EXPORT_FROM_RECORD_SHOW]: <ExportSingleRecordCommand />,
[EngineComponentKey.EXPORT_MULTIPLE_RECORDS]: (
<ExportMultipleRecordsCommand />
),
[EngineComponentKey.UPDATE_MULTIPLE_RECORDS]: (
<UpdateMultipleRecordsCommand />
),
[EngineComponentKey.IMPORT_RECORDS]: (
<ImportRecordsNoSelectionRecordCommand />
),
[EngineComponentKey.EXPORT_VIEW]: <ExportMultipleRecordsCommand />,
[EngineComponentKey.SEE_DELETED_RECORDS]: (
<SeeDeletedRecordsNoSelectionRecordCommand />
),
[EngineComponentKey.CREATE_NEW_VIEW]: (
<CreateNewViewNoSelectionRecordCommand />
),
[EngineComponentKey.HIDE_DELETED_RECORDS]: (
<HideDeletedRecordsNoSelectionRecordCommand />
),
[EngineComponentKey.EDIT_RECORD_PAGE_LAYOUT]: (
<EditRecordPageLayoutSingleRecordCommand />
),
[EngineComponentKey.EDIT_DASHBOARD_LAYOUT]: (
<EditDashboardSingleRecordCommand />
),
[EngineComponentKey.CANCEL_DASHBOARD_LAYOUT]: (
<CancelDashboardSingleRecordCommand />
),
[EngineComponentKey.GO_TO_PEOPLE]: (
<CommandLink
to={AppPath.RecordIndexPage}
params={{ objectNamePlural: CoreObjectNamePlural.Person }}
/>
),
[EngineComponentKey.GO_TO_COMPANIES]: (
<CommandLink
to={AppPath.RecordIndexPage}
params={{ objectNamePlural: CoreObjectNamePlural.Company }}
/>
),
[EngineComponentKey.GO_TO_DASHBOARDS]: (
<CommandLink
to={AppPath.RecordIndexPage}
params={{ objectNamePlural: CoreObjectNamePlural.Dashboard }}
/>
),
[EngineComponentKey.GO_TO_OPPORTUNITIES]: (
<CommandLink
to={AppPath.RecordIndexPage}
params={{
objectNamePlural: CoreObjectNamePlural.Opportunity,
}}
/>
),
[EngineComponentKey.GO_TO_SETTINGS]: (
<CommandLink
to={AppPath.SettingsCatchAll}
params={{
'*': SettingsPath.ProfilePage,
}}
/>
),
[EngineComponentKey.GO_TO_TASKS]: (
<CommandLink
to={AppPath.RecordIndexPage}
params={{ objectNamePlural: CoreObjectNamePlural.Task }}
/>
),
[EngineComponentKey.GO_TO_NOTES]: (
<CommandLink
to={AppPath.RecordIndexPage}
params={{ objectNamePlural: CoreObjectNamePlural.Note }}
/>
),
[EngineComponentKey.GO_TO_WORKFLOWS]: (
<CommandLink
to={AppPath.RecordIndexPage}
params={{ objectNamePlural: CoreObjectNamePlural.Workflow }}
/>
),
[EngineComponentKey.GO_TO_RUNS]: (
<CommandLink
to={AppPath.RecordIndexPage}
params={{ objectNamePlural: CoreObjectNamePlural.WorkflowRun }}
/>
),
[EngineComponentKey.SEARCH_RECORDS]: (
<CommandMenuItemOpenSidePanelPage
page={SidePanelPages.SearchRecords}
pageTitle={msg`Search`}
pageIcon={IconSearch}
shouldResetSearchState={true}
/>
),
[EngineComponentKey.SEARCH_RECORDS_FALLBACK]: (
<CommandMenuItemOpenSidePanelPage
page={SidePanelPages.SearchRecords}
pageTitle={msg`Search`}
pageIcon={IconSearch}
/>
),
[EngineComponentKey.ASK_AI]: (
<CommandMenuItemOpenSidePanelPage
page={SidePanelPages.AskAI}
pageTitle={msg`Ask AI`}
pageIcon={IconSparkles}
/>
),
[EngineComponentKey.VIEW_PREVIOUS_AI_CHATS]: (
<CommandMenuItemOpenSidePanelPage
page={SidePanelPages.ViewPreviousAIChats}
pageTitle={msg`View Previous AI Chats`}
pageIcon={IconHistory}
/>
),
[EngineComponentKey.SEE_ACTIVE_VERSION_WORKFLOW]: (
<SeeActiveVersionWorkflowSingleRecordCommand />
),
[EngineComponentKey.SEE_RUNS_WORKFLOW]: (
<SeeRunsWorkflowSingleRecordCommand />
),
[EngineComponentKey.SEE_VERSIONS_WORKFLOW]: (
<SeeVersionsWorkflowSingleRecordCommand />
),
[EngineComponentKey.ADD_NODE_WORKFLOW]: (
<AddNodeWorkflowSingleRecordCommand />
),
[EngineComponentKey.SEE_VERSION_WORKFLOW_RUN]: (
<SeeVersionWorkflowRunSingleRecordCommand />
),
[EngineComponentKey.SEE_WORKFLOW_WORKFLOW_RUN]: (
<SeeWorkflowWorkflowRunSingleRecordCommand />
),
[EngineComponentKey.SEE_RUNS_WORKFLOW_VERSION]: (
<SeeRunsWorkflowVersionSingleRecordCommand />
),
[EngineComponentKey.SEE_WORKFLOW_WORKFLOW_VERSION]: (
<SeeWorkflowWorkflowVersionSingleRecordCommand />
),
[EngineComponentKey.SEE_VERSIONS_WORKFLOW_VERSION]: (
<SeeVersionsWorkflowVersionSingleRecordCommand />
),
};
@@ -1,22 +1,26 @@
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { type ReactNode } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
type CommandMenuItemErrorBoundaryProps = {
children: ReactNode;
engineCommandId: string;
shouldReportToSentry?: boolean;
onError?: () => void;
};
export const CommandMenuItemErrorBoundary = ({
children,
engineCommandId,
shouldReportToSentry = false,
onError,
}: CommandMenuItemErrorBoundaryProps) => {
const { enqueueErrorSnackBar } = useSnackBar();
const commandMenuItemId = useAvailableComponentInstanceIdOrThrow(
CommandComponentInstanceContext,
);
const handleError = async (error: Error) => {
enqueueErrorSnackBar({ message: error.message });
@@ -41,7 +45,7 @@ export const CommandMenuItemErrorBoundary = ({
<ErrorBoundary
fallbackRender={() => null}
onError={handleError}
resetKeys={[engineCommandId]}
resetKeys={[commandMenuItemId]}
>
{children}
</ErrorBoundary>
@@ -1,43 +0,0 @@
import { isEngineCommandMountedFamilySelector } from '@/command-menu-item/engine-command/selectors/isEngineCommandMountedFamilySelector';
import { useMountEngineCommand } from '@/command-menu-item/engine-command/hooks/useMountEngineCommand';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { type EngineComponentKey } from '~/generated-metadata/graphql';
import { HeadlessCommandMenuItem } from './HeadlessCommandMenuItem';
export const EngineCommandMenuItem = ({
commandMenuItemId,
engineComponentKey,
}: {
commandMenuItemId: string;
engineComponentKey: EngineComponentKey;
}) => {
const mountEngineCommand = useMountEngineCommand();
const contextStoreInstanceId = useAvailableComponentInstanceIdOrThrow(
ContextStoreComponentInstanceContext,
);
const isMounted = useAtomFamilySelectorValue(
isEngineCommandMountedFamilySelector,
commandMenuItemId,
);
const handleClick = () => {
mountEngineCommand(
commandMenuItemId,
contextStoreInstanceId,
engineComponentKey,
);
};
return (
<HeadlessCommandMenuItem
isMounted={isMounted}
commandMenuItemId={commandMenuItemId}
onClick={handleClick}
/>
);
};
@@ -1,22 +1,35 @@
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMenu';
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useContext } from 'react';
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
import { useMountCommand } from '@/command-menu-item/engine-command/hooks/useMountCommand';
import { isEngineCommandMountedFamilySelector } from '@/command-menu-item/engine-command/selectors/isEngineCommandMountedFamilySelector';
import { useCloseCommandMenu } from '@/command-menu-item/hooks/useCloseCommandMenu';
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { isDefined } from 'twenty-shared/utils';
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
import { CommandMenuItemDisplay } from './CommandMenuItemDisplay';
export const HeadlessCommandMenuItem = ({
isMounted,
commandMenuItemId,
onClick,
item,
}: {
isMounted: boolean;
commandMenuItemId: string;
onClick: () => void;
item: CommandMenuItemFieldsFragment;
}) => {
const commandMenuItemConfig = useContext(CommandConfigContext);
const mountCommand = useMountCommand();
const contextStoreInstanceId = useAvailableComponentInstanceIdOrThrow(
ContextStoreComponentInstanceContext,
);
const isMounted = useAtomFamilySelectorValue(
isEngineCommandMountedFamilySelector,
item.id,
);
const { closeCommandMenu } = useCloseCommandMenu({
closeSidePanelOnShowPageOptionsExecution: false,
@@ -25,20 +38,29 @@ export const HeadlessCommandMenuItem = ({
const commandMenuItemProgress = useAtomFamilyStateValue(
commandMenuItemProgressFamilyState,
commandMenuItemId,
item.id,
);
if (!isDefined(commandMenuItemConfig)) {
return null;
}
const handleClick = () => {
const handleClick = async () => {
if (isMounted) {
return;
}
closeCommandMenu();
onClick();
await mountCommand({
engineCommandId: item.id,
contextStoreInstanceId,
engineComponentKey: item.engineComponentKey,
frontComponentId: item.frontComponentId ?? undefined,
workflowVersionId: item.workflowVersionId ?? undefined,
availabilityType: item.availabilityType,
availabilityObjectMetadataId: item.availabilityObjectMetadataId,
});
};
return (
@@ -1,68 +0,0 @@
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useMountHeadlessFrontComponent } from '@/front-components/hooks/useMountHeadlessFrontComponent';
import { isHeadlessFrontComponentMountedFamilySelector } from '@/front-components/selectors/isHeadlessFrontComponentMountedFamilySelector';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { HeadlessCommandMenuItem } from './HeadlessCommandMenuItem';
// TODO: Some code is duplicated in this component and FrontComponentCommandMenuItem
// This will be refactored because the logic will differ between headless and non-headless front components.
export const HeadlessFrontComponentCommandMenuItem = ({
frontComponentId,
commandMenuItemId,
}: {
frontComponentId: string;
commandMenuItemId: string;
}) => {
const mountHeadlessFrontComponent = useMountHeadlessFrontComponent();
const isMounted = useAtomFamilySelectorValue(
isHeadlessFrontComponentMountedFamilySelector,
frontComponentId,
);
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
);
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const currentObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === contextStoreCurrentObjectMetadataItemId,
);
const selectedRecordIds =
contextStoreTargetedRecordsRule.mode === 'selection'
? contextStoreTargetedRecordsRule.selectedRecordIds
: [];
const recordId =
selectedRecordIds.length === 1 ? selectedRecordIds[0] : undefined;
const objectNameSingular = currentObjectMetadataItem?.nameSingular;
const handleClick = () => {
mountHeadlessFrontComponent(frontComponentId, {
commandMenuItemId,
recordId,
objectNameSingular,
});
};
return (
<HeadlessCommandMenuItem
isMounted={isMounted}
commandMenuItemId={commandMenuItemId}
onClick={handleClick}
/>
);
};
@@ -1,128 +0,0 @@
import { Command } from '@/command-menu-item/display/components/Command';
import { isBulkRecordsManualTrigger } from '@/command-menu-item/record/utils/isBulkRecordsManualTrigger';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { type WorkflowVersion } from '@/workflow/types/Workflow';
import { useStore } from 'jotai';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import {
type CommandMenuItemAvailabilityType,
CommandMenuItemAvailabilityType as CommandMenuItemAvailabilityTypeEnum,
} from '~/generated-metadata/graphql';
export const WorkflowCommandMenuItem = ({
workflowVersionId,
availabilityType,
availabilityObjectMetadataId,
}: {
workflowVersionId: string;
availabilityType: CommandMenuItemAvailabilityType;
availabilityObjectMetadataId?: string | null;
}) => {
const store = useStore();
const { runWorkflowVersion } = useRunWorkflowVersion();
const { record: workflowVersion } = useFindOneRecord<
Pick<WorkflowVersion, 'id' | 'workflowId' | 'trigger' | '__typename'>
>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
objectRecordId: workflowVersionId,
recordGqlFields: { id: true, workflowId: true, trigger: true },
});
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const selectedRecordIds =
contextStoreTargetedRecordsRule.mode === 'selection'
? contextStoreTargetedRecordsRule.selectedRecordIds
: [];
const handleClick = async () => {
if (!isDefined(workflowVersion)) {
return;
}
switch (availabilityType) {
case CommandMenuItemAvailabilityTypeEnum.RECORD_SELECTION: {
if (selectedRecordIds.length === 0) {
return;
}
const limitedSelectedRecordIds = selectedRecordIds.slice(
0,
QUERY_MAX_RECORDS,
);
const objectMetadataItem = objectMetadataItems.find(
(metadata) => metadata.id === availabilityObjectMetadataId,
);
if (
isDefined(workflowVersion.trigger) &&
isBulkRecordsManualTrigger(workflowVersion.trigger)
) {
const selectedRecords = limitedSelectedRecordIds
.map((recordId) =>
store.get(recordStoreFamilyState.atomFamily(recordId)),
)
.filter(isDefined);
await runWorkflowVersion({
workflowId: workflowVersion.workflowId,
workflowVersionId: workflowVersion.id,
payload: isDefined(objectMetadataItem)
? { [objectMetadataItem.namePlural]: selectedRecords }
: undefined,
});
return;
}
for (const selectedRecordId of limitedSelectedRecordIds) {
const selectedRecord = store.get(
recordStoreFamilyState.atomFamily(selectedRecordId),
);
if (!isDefined(selectedRecord)) {
continue;
}
await runWorkflowVersion({
workflowId: workflowVersion.workflowId,
workflowVersionId: workflowVersion.id,
payload: selectedRecord,
});
}
return;
}
case CommandMenuItemAvailabilityTypeEnum.GLOBAL:
case CommandMenuItemAvailabilityTypeEnum.FALLBACK: {
await runWorkflowVersion({
workflowId: workflowVersion.workflowId,
workflowVersionId: workflowVersion.id,
});
return;
}
}
};
return (
<Command
onClick={handleClick}
closeSidePanelOnCommandMenuListExecution={false}
/>
);
};
@@ -0,0 +1,38 @@
import { CommandMenuItemErrorBoundary } from '@/command-menu-item/display/components/CommandMenuItemErrorBoundary';
import { ENGINE_COMPONENT_KEY_COMPONENT_MAP } from '@/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap';
import { useUnmountCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
import { headlessCommandContextApisState } from '@/command-menu-item/engine-command/states/headlessCommandContextApisState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const CommandRunner = () => {
const headlessCommandContextApis = useAtomStateValue(
headlessCommandContextApisState,
);
const unmountCommand = useUnmountCommand();
return (
<>
{[...headlessCommandContextApis.entries()].map(
([commandMenuItemId, context]) => (
<ContextStoreComponentInstanceContext.Provider
key={commandMenuItemId}
value={{ instanceId: context.contextStoreInstanceId }}
>
<CommandComponentInstanceContext.Provider
value={{ instanceId: commandMenuItemId }}
>
<CommandMenuItemErrorBoundary
shouldReportToSentry
onError={() => unmountCommand(commandMenuItemId)}
>
{ENGINE_COMPONENT_KEY_COMPONENT_MAP[context.engineComponentKey]}
</CommandMenuItemErrorBoundary>
</CommandComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
),
)}
</>
);
};
@@ -4,8 +4,8 @@ import { type ReactNode, useEffect } from 'react';
import { COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME } from '@/command-menu-item/confirmation-modal/constants/CommandMenuItemConfirmationModalResultBrowserEventName';
import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
import { type CommandMenuConfirmationModalResultBrowserEventDetail } from '@/command-menu-item/confirmation-modal/types/CommandMenuConfirmationModalResultBrowserEventDetail';
import { useUnmountEngineCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
import { useUnmountCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { type ButtonAccent } from 'twenty-ui/input';
@@ -26,10 +26,11 @@ export const HeadlessConfirmationModalEngineCommandEffect = ({
}: HeadlessConfirmationModalEngineCommandEffectProps) => {
const { isInitializedRef, setIsInitialized } =
useIsHeadlessEngineCommandEffectInitialized();
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
EngineCommandComponentInstanceContext,
const commandMenuItemId = useAvailableComponentInstanceIdOrThrow(
CommandComponentInstanceContext,
);
const unmountEngineCommand = useUnmountEngineCommand();
const unmountCommand = useUnmountCommand();
const { openConfirmationModal } = useCommandMenuConfirmationModal();
useEffect(() => {
@@ -40,7 +41,7 @@ export const HeadlessConfirmationModalEngineCommandEffect = ({
setIsInitialized(true);
openConfirmationModal({
caller: { type: 'engineCommand', engineCommandId },
caller: { type: 'commandMenuItem', commandMenuItemId },
title,
subtitle,
confirmButtonText,
@@ -49,7 +50,7 @@ export const HeadlessConfirmationModalEngineCommandEffect = ({
}, [
isInitializedRef,
setIsInitialized,
engineCommandId,
commandMenuItemId,
openConfirmationModal,
title,
subtitle,
@@ -65,8 +66,8 @@ export const HeadlessConfirmationModalEngineCommandEffect = ({
const caller = customEvent.detail.caller;
if (
caller.type !== 'engineCommand' ||
caller.engineCommandId !== engineCommandId
caller.type !== 'commandMenuItem' ||
caller.commandMenuItemId !== commandMenuItemId
) {
return;
}
@@ -75,7 +76,7 @@ export const HeadlessConfirmationModalEngineCommandEffect = ({
await execute();
}
unmountEngineCommand(engineCommandId);
unmountCommand(commandMenuItemId);
};
window.addEventListener(
@@ -89,7 +90,7 @@ export const HeadlessConfirmationModalEngineCommandEffect = ({
handleConfirmationResult,
);
};
}, [engineCommandId, execute, unmountEngineCommand]);
}, [execute, commandMenuItemId, unmountCommand]);
return null;
};
@@ -1,41 +0,0 @@
import { CommandMenuItemErrorBoundary } from '@/command-menu-item/display/components/CommandMenuItemErrorBoundary';
import { ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP } from '@/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap';
import { useUnmountEngineCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const HeadlessEngineCommandMountRoot = () => {
const mountedEngineCommands = useAtomStateValue(mountedEngineCommandsState);
const unmountEngineCommand = useUnmountEngineCommand();
return (
<>
{[...mountedEngineCommands.entries()].map(
([engineCommandId, mountContext]) => (
<CommandMenuItemErrorBoundary
key={engineCommandId}
engineCommandId={engineCommandId}
shouldReportToSentry
onError={() => unmountEngineCommand(engineCommandId)}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: mountContext.contextStoreInstanceId }}
>
<EngineCommandComponentInstanceContext.Provider
value={{ instanceId: engineCommandId }}
>
{
ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP[
mountContext.engineComponentKey
]
}
</EngineCommandComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</CommandMenuItemErrorBoundary>
),
)}
</>
);
};
@@ -1,6 +1,6 @@
import { useIsHeadlessEngineCommandEffectInitialized } from '@/command-menu-item/engine-command/hooks/useIsHeadlessEngineCommandEffectInitialized';
import { useUnmountEngineCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
import { useUnmountCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useEffect } from 'react';
@@ -17,11 +17,11 @@ export const HeadlessEngineCommandWrapperEffect = ({
const { isInitializedRef, setIsInitialized } =
useIsHeadlessEngineCommandEffectInitialized();
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
EngineCommandComponentInstanceContext,
const commandMenuItemId = useAvailableComponentInstanceIdOrThrow(
CommandComponentInstanceContext,
);
const unmountEngineCommand = useUnmountEngineCommand();
const unmountCommand = useUnmountCommand();
const { enqueueErrorSnackBar } = useSnackBar();
@@ -35,7 +35,7 @@ export const HeadlessEngineCommandWrapperEffect = ({
const run = async () => {
await execute();
unmountEngineCommand(engineCommandId);
unmountCommand(commandMenuItemId);
};
run();
@@ -44,8 +44,8 @@ export const HeadlessEngineCommandWrapperEffect = ({
ready,
isInitializedRef,
setIsInitialized,
engineCommandId,
unmountEngineCommand,
commandMenuItemId,
unmountCommand,
enqueueErrorSnackBar,
]);
@@ -0,0 +1,60 @@
import { Suspense, lazy } from 'react';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
import { isHeadlessFrontComponentCommandContextApi } from '@/command-menu-item/engine-command/utils/isHeadlessFrontComponentCommandContextApi';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
const FrontComponentRenderer = lazy(() =>
import('@/front-components/components/FrontComponentRenderer').then(
(module) => ({ default: module.FrontComponentRenderer }),
),
);
export const HeadlessFrontComponentRendererEngineCommand = () => {
const commandMenuItemId = useAvailableComponentInstanceIdOrThrow(
CommandComponentInstanceContext,
);
const context = useHeadlessCommandContextApi();
if (!isHeadlessFrontComponentCommandContextApi(context)) {
throw new Error(
'Context is not a headless front component command context API',
);
}
const objectNameSingular = context.objectMetadataItem?.nameSingular;
const recordId =
context.selectedRecords.length === 1
? context.selectedRecords[0].id
: undefined;
// TODO: Remove layout rendering provider once we have refactored FrontComponentRenderer to have one headless renderer and a standard renderer
return (
<Suspense fallback={null}>
<LayoutRenderingProvider
value={{
targetRecordIdentifier:
isDefined(objectNameSingular) && isDefined(recordId)
? {
id: recordId,
targetObjectNameSingular: objectNameSingular,
}
: undefined,
layoutType: PageLayoutType.DASHBOARD,
isInSidePanel: false,
}}
>
<FrontComponentRenderer
frontComponentId={context.frontComponentId}
commandMenuItemId={commandMenuItemId}
/>
</LayoutRenderingProvider>
</Suspense>
);
};
@@ -42,13 +42,15 @@ 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 { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { msg } from '@lingui/core/macro';
import { AppPath, SettingsPath, SidePanelPages } from 'twenty-shared/types';
import { IconHistory, IconSearch, IconSparkles } from 'twenty-ui/display';
import { EngineComponentKey } from '~/generated-metadata/graphql';
export const ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP: Record<
export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record<
EngineComponentKey,
React.ReactNode
> = {
@@ -251,4 +253,10 @@ export const ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP: Record<
[EngineComponentKey.SEE_VERSIONS_WORKFLOW_VERSION]: (
<SeeVersionsWorkflowVersionSingleRecordCommand />
),
[EngineComponentKey.TRIGGER_WORKFLOW_VERSION]: (
<TriggerWorkflowVersionEngineCommand />
),
[EngineComponentKey.FRONT_COMPONENT_RENDERER]: (
<HeadlessFrontComponentRendererEngineCommand />
),
};
@@ -0,0 +1,132 @@
import { useCallback } from 'react';
import {
type HeadlessCommandContextApi,
type HeadlessEngineCommandContextApi,
} from '@/command-menu-item/engine-command/types/HeadlessCommandContextApi';
import { buildTriggerWorkflowVersionPayloads } from '@/command-menu-item/engine-command/utils/buildTriggerWorkflowVersionPayloads';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useLazyFindOneRecord } from '@/object-record/hooks/useLazyFindOneRecord';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { type WorkflowVersion } from '@/workflow/types/Workflow';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import {
CommandMenuItemAvailabilityType as CommandMenuItemAvailabilityTypeEnum,
type CommandMenuItemAvailabilityType,
} from '~/generated-metadata/graphql';
type WorkflowVersionRecord = Pick<
WorkflowVersion,
'id' | 'workflowId' | 'trigger' | '__typename'
>;
type BuildTriggerWorkflowVersionCommandStateParams = {
headlessEngineCommandContextApi: HeadlessEngineCommandContextApi;
workflowVersionId: string;
availabilityType: CommandMenuItemAvailabilityType;
availabilityObjectMetadataId?: string | null;
};
export const useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation =
() => {
const store = useStore();
const { enqueueWarningSnackBar } = useSnackBar();
const { findOneRecord: findOneWorkflowVersion } =
useLazyFindOneRecord<WorkflowVersionRecord>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
recordGqlFields: { id: true, workflowId: true, trigger: true },
});
const fetchWorkflowVersion = useCallback(
async (versionId: string): Promise<WorkflowVersionRecord | undefined> => {
let record: WorkflowVersionRecord | undefined;
await findOneWorkflowVersion({
objectRecordId: versionId,
onCompleted: (data) => {
record = data;
},
});
return record;
},
[findOneWorkflowVersion],
);
const enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation =
useCallback(
async ({
headlessEngineCommandContextApi,
workflowVersionId,
availabilityType,
availabilityObjectMetadataId,
}: BuildTriggerWorkflowVersionCommandStateParams): Promise<
HeadlessCommandContextApi | undefined
> => {
const workflowVersion = await fetchWorkflowVersion(workflowVersionId);
if (!isDefined(workflowVersion)) {
return undefined;
}
const selectedRecordIds =
headlessEngineCommandContextApi.targetedRecordsRule.mode ===
'selection'
? headlessEngineCommandContextApi.targetedRecordsRule
.selectedRecordIds
: [];
if (selectedRecordIds.length > QUERY_MAX_RECORDS) {
const selectedCountFormatted =
selectedRecordIds.length.toLocaleString();
const limitFormatted = QUERY_MAX_RECORDS.toLocaleString();
enqueueWarningSnackBar({
message: t`You selected ${selectedCountFormatted} records but manual triggers can run on at most ${limitFormatted} records at once. Only the first ${limitFormatted} records will be processed.`,
options: {
dedupeKey: 'workflow-manual-trigger-selection-limit',
},
});
}
const objectMetadataItems = store.get(
objectMetadataItemsSelector.atom,
);
const payloads = buildTriggerWorkflowVersionPayloads({
store,
trigger: workflowVersion.trigger,
availabilityType,
availabilityObjectMetadataId,
objectMetadataItems,
selectedRecordIds,
});
if (
availabilityType ===
CommandMenuItemAvailabilityTypeEnum.RECORD_SELECTION &&
!isNonEmptyArray(payloads)
) {
return undefined;
}
return {
...headlessEngineCommandContextApi,
workflowId: workflowVersion.workflowId,
workflowVersionId: workflowVersion.id,
payloads,
};
},
[store, fetchWorkflowVersion, enqueueWarningSnackBar],
);
return {
enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation,
};
};
@@ -0,0 +1,25 @@
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
import { headlessCommandContextApisState } from '@/command-menu-item/engine-command/states/headlessCommandContextApisState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isDefined } from 'twenty-shared/utils';
export const useHeadlessCommandContextApi = () => {
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
CommandComponentInstanceContext,
);
const headlessCommandContextApis = useAtomStateValue(
headlessCommandContextApisState,
);
const headlessCommandContextApi =
headlessCommandContextApis.get(engineCommandId);
if (!isDefined(headlessCommandContextApi)) {
throw new Error(
'Headless command context API not found. Make sure the command was mounted via the command mount flow.',
);
}
return headlessCommandContextApi;
};
@@ -0,0 +1,78 @@
import { useCallback } from 'react';
import { useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation } from '@/command-menu-item/engine-command/hooks/useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation';
import { headlessCommandContextApisState } from '@/command-menu-item/engine-command/states/headlessCommandContextApisState';
import { buildHeadlessCommandContextApi } from '@/command-menu-item/engine-command/utils/buildHeadlessCommandContextApi';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import {
type CommandMenuItemAvailabilityType,
type EngineComponentKey,
} from '~/generated-metadata/graphql';
type MountCommandParams = {
engineCommandId: string;
contextStoreInstanceId: string;
engineComponentKey: EngineComponentKey;
frontComponentId?: string;
workflowVersionId?: string;
availabilityType?: CommandMenuItemAvailabilityType;
availabilityObjectMetadataId?: string | null;
};
export const useMountCommand = () => {
const store = useStore();
const {
enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation,
} = useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation();
const mountCommand = useCallback(
async ({
engineCommandId,
contextStoreInstanceId,
engineComponentKey,
frontComponentId,
workflowVersionId,
availabilityType,
availabilityObjectMetadataId,
}: MountCommandParams) => {
const headlessEngineCommandContextApi = buildHeadlessCommandContextApi({
store,
contextStoreInstanceId,
engineComponentKey,
});
const commandState = isDefined(frontComponentId)
? { ...headlessEngineCommandContextApi, frontComponentId }
: isDefined(workflowVersionId) && isDefined(availabilityType)
? await enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(
{
headlessEngineCommandContextApi,
workflowVersionId,
availabilityType,
availabilityObjectMetadataId,
},
)
: headlessEngineCommandContextApi;
if (!isDefined(commandState)) {
return;
}
store.set(headlessCommandContextApisState.atom, (previousMap) => {
const newMap = new Map(previousMap);
newMap.set(engineCommandId, commandState);
return newMap;
});
},
[
store,
enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation,
],
);
return mountCommand;
};
@@ -1,129 +0,0 @@
import { useCallback } from 'react';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { type EngineComponentKey } from '~/generated-metadata/graphql';
export const useMountEngineCommand = () => {
const store = useStore();
const mountEngineCommand = useCallback(
(
engineCommandId: string,
contextStoreInstanceId: string,
engineComponentKey: EngineComponentKey,
) => {
const objectMetadataItemId = store.get(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataItemId,
);
const currentViewId = store.get(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const targetedRecordsRule = store.get(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const selectedRecords = (
targetedRecordsRule.mode === 'selection'
? targetedRecordsRule.selectedRecordIds
: []
)
.map((id) => store.get(recordStoreFamilyState.atomFamily(id)))
.filter(isDefined);
const filters = store.get(
contextStoreFiltersComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const filterGroups = store.get(
contextStoreFilterGroupsComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const anyFieldFilterValue = store.get(
contextStoreAnyFieldFilterValueComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const currentWorkspaceMember = store.get(
currentWorkspaceMemberState.atom,
);
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const userTimezone =
currentWorkspaceMember?.timeZone !== 'system'
? (currentWorkspaceMember?.timeZone ?? systemTimeZone)
: systemTimeZone;
const graphqlFilter = isDefined(objectMetadataItem)
? computeContextStoreFilters({
contextStoreTargetedRecordsRule: targetedRecordsRule,
contextStoreFilters: filters,
contextStoreFilterGroups: filterGroups,
objectMetadataItem,
filterValueDependencies: {
currentWorkspaceMemberId: currentWorkspaceMember?.id,
timeZone: userTimezone,
},
contextStoreAnyFieldFilterValue: anyFieldFilterValue,
})
: null;
const recordIndexId =
objectMetadataItem && currentViewId
? getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
currentViewId,
)
: null;
store.set(mountedEngineCommandsState.atom, (previousMap) => {
const newMap = new Map(previousMap);
newMap.set(engineCommandId, {
engineComponentKey,
contextStoreInstanceId,
objectMetadataItem: objectMetadataItem ?? null,
currentViewId,
recordIndexId,
targetedRecordsRule,
selectedRecords,
graphqlFilter,
});
return newMap;
});
},
[store],
);
return mountEngineCommand;
};
@@ -1,22 +0,0 @@
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isDefined } from 'twenty-shared/utils';
export const useMountedEngineCommandContext = () => {
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
EngineCommandComponentInstanceContext,
);
const mountedEngineCommands = useAtomStateValue(mountedEngineCommandsState);
const context = mountedEngineCommands.get(engineCommandId);
if (!isDefined(context)) {
throw new Error(
'Engine command mount context not found. Make sure the command was mounted via the engine command mount flow.',
);
}
return context;
};
@@ -1,15 +1,15 @@
import { useCallback } from 'react';
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
import { headlessCommandContextApisState } from '@/command-menu-item/engine-command/states/headlessCommandContextApisState';
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
import { useStore } from 'jotai';
export const useUnmountEngineCommand = () => {
export const useUnmountCommand = () => {
const store = useStore();
const unmountEngineCommand = useCallback(
const unmountCommand = useCallback(
(engineCommandId: string) => {
store.set(mountedEngineCommandsState.atom, (previousMap) => {
store.set(headlessCommandContextApisState.atom, (previousMap) => {
const newMap = new Map(previousMap);
newMap.delete(engineCommandId);
@@ -25,5 +25,5 @@ export const useUnmountEngineCommand = () => {
[store],
);
return unmountEngineCommand;
return unmountCommand;
};
@@ -0,0 +1,39 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { isHeadlessTriggerWorkflowVersionCommandContextApi } from '@/command-menu-item/engine-command/utils/isHeadlessTriggerWorkflowVersionCommandContextApi';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useCallback } from 'react';
import { isNonEmptyArray } from 'twenty-shared/utils';
export const TriggerWorkflowVersionEngineCommand = () => {
const mountedCommandState = useHeadlessCommandContextApi();
const { runWorkflowVersion } = useRunWorkflowVersion();
const execute = useCallback(async () => {
if (
!isHeadlessTriggerWorkflowVersionCommandContextApi(mountedCommandState)
) {
return;
}
if (!isNonEmptyArray(mountedCommandState.payloads)) {
await runWorkflowVersion({
workflowId: mountedCommandState.workflowId,
workflowVersionId: mountedCommandState.workflowVersionId,
});
return;
}
for (const payload of mountedCommandState.payloads) {
await runWorkflowVersion({
workflowId: mountedCommandState.workflowId,
workflowVersionId: mountedCommandState.workflowVersionId,
payload,
});
}
}, [runWorkflowVersion, mountedCommandState]);
return <HeadlessEngineCommandWrapperEffect execute={execute} />;
};
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
import { useIncrementalDeleteManyRecords } from '@/object-record/hooks/useIncrementalDeleteManyRecords';
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
@@ -8,7 +8,7 @@ import { isDefined } from 'twenty-shared/utils';
export const DeleteMultipleRecordsCommand = () => {
const { recordIndexId, objectMetadataItem, graphqlFilter } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
if (
!isDefined(recordIndexId) ||
@@ -1,5 +1,5 @@
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
import { useIncrementalDestroyManyRecords } from '@/object-record/hooks/useIncrementalDestroyManyRecords';
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
@@ -10,7 +10,7 @@ import { isDefined } from 'twenty-shared/utils';
export const DestroyMultipleRecordsCommand = () => {
const { recordIndexId, objectMetadataItem, graphqlFilter } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
if (
!isDefined(recordIndexId) ||
@@ -1,6 +1,6 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useRecordIndexExportRecords } from '@/object-record/record-index/export/hooks/useRecordIndexExportRecords';
@@ -43,11 +43,10 @@ const ExportMultipleRecordsCommandContent = ({
};
export const ExportMultipleRecordsCommand = () => {
const { objectMetadataItem, recordIndexId } =
useMountedEngineCommandContext();
const { objectMetadataItem, recordIndexId } = useHeadlessCommandContextApi();
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
EngineCommandComponentInstanceContext,
CommandComponentInstanceContext,
);
const setCommandMenuItemProgress = useSetAtomFamilyState(
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useOpenMergeRecordsPageInSidePanel } from '@/side-panel/hooks/useOpenMergeRecordsPageInSidePanel';
import { isDefined } from 'twenty-shared/utils';
export const MergeMultipleRecordsCommand = () => {
const { objectMetadataItem, selectedRecords } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
const selectedRecordIds = selectedRecords.map((record) => record.id);
@@ -1,5 +1,5 @@
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
@@ -11,7 +11,7 @@ import { isDefined } from 'twenty-shared/utils';
export const RestoreMultipleRecordsCommand = () => {
const { recordIndexId, objectMetadataItem, graphqlFilter } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
if (
!isDefined(recordIndexId) ||
@@ -1,9 +1,9 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useOpenUpdateMultipleRecordsPageInSidePanel } from '@/side-panel/hooks/useOpenUpdateMultipleRecordsPageInSidePanel';
export const UpdateMultipleRecordsCommand = () => {
const { contextStoreInstanceId } = useMountedEngineCommandContext();
const { contextStoreInstanceId } = useHeadlessCommandContextApi();
const { openUpdateMultipleRecordsPageInSidePanel } =
useOpenUpdateMultipleRecordsPageInSidePanel({
@@ -1,11 +1,10 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { isDefined } from 'twenty-shared/utils';
export const CreateNewIndexRecordNoSelectionRecordCommand = () => {
const { objectMetadataItem, recordIndexId } =
useMountedEngineCommandContext();
const { objectMetadataItem, recordIndexId } = useHeadlessCommandContextApi();
if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
throw new Error(
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
@@ -8,7 +8,7 @@ import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/sta
import { isDefined } from 'twenty-shared/utils';
export const CreateNewViewNoSelectionRecordCommand = () => {
const { currentViewId, recordIndexId } = useMountedEngineCommandContext();
const { currentViewId, recordIndexId } = useHeadlessCommandContextApi();
const { openDropdown } = useOpenDropdown();
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useCheckIsSoftDeleteFilter } from '@/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
@@ -8,8 +8,7 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { isDefined } from 'twenty-shared/utils';
export const HideDeletedRecordsNoSelectionRecordCommand = () => {
const { objectMetadataItem, recordIndexId } =
useMountedEngineCommandContext();
const { objectMetadataItem, recordIndexId } = useHeadlessCommandContextApi();
if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
throw new Error(
@@ -1,10 +1,10 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useOpenObjectRecordsSpreadsheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog';
import { isDefined } from 'twenty-shared/utils';
export const ImportRecordsNoSelectionRecordCommand = () => {
const { objectMetadataItem } = useMountedEngineCommandContext();
const { objectMetadataItem } = useHeadlessCommandContextApi();
if (!isDefined(objectMetadataItem)) {
throw new Error('Object metadata item is required to import records');
@@ -1,11 +1,10 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { isDefined } from 'twenty-shared/utils';
export const SeeDeletedRecordsNoSelectionRecordCommand = () => {
const { objectMetadataItem, recordIndexId } =
useMountedEngineCommandContext();
const { objectMetadataItem, recordIndexId } = useHeadlessCommandContextApi();
if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
throw new Error(
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useCreateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useCreateNavigationMenuItem';
import { isDefined } from 'twenty-shared/utils';
export const AddToFavoritesSingleRecordCommand = () => {
const { objectMetadataItem, selectedRecords } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
const selectedRecord = selectedRecords[0];
@@ -1,7 +1,7 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/common/hooks/useRemoveNavigationMenuItemByTargetRecordId';
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
@@ -9,7 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
export const DeleteSingleRecordCommand = () => {
const { recordIndexId, objectMetadataItem, selectedRecords } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
@@ -1,5 +1,5 @@
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
@@ -10,7 +10,7 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
export const DestroySingleRecordCommand = () => {
const { recordIndexId, objectMetadataItem, selectedRecords } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
@@ -1,9 +1,9 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { isDefined } from 'twenty-shared/utils';
export const ExportNoteSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const selectedRecord = selectedRecords[0];
const recordId = selectedRecord?.id;
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useExportSingleRecord } from '@/object-record/record-show/hooks/useExportSingleRecord';
import { isDefined } from 'twenty-shared/utils';
export const ExportSingleRecordCommand = () => {
const { objectMetadataItem, currentViewId, selectedRecords } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { isDefined } from 'twenty-shared/utils';
export const NavigateToNextRecordSingleRecordCommand = () => {
const { objectMetadataItem, selectedRecords } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { isDefined } from 'twenty-shared/utils';
export const NavigateToPreviousRecordSingleRecordCommand = () => {
const { objectMetadataItem, selectedRecords } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
@@ -1,12 +1,12 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem';
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
import { isDefined } from 'twenty-shared/utils';
export const RemoveFromFavoritesSingleRecordCommand = () => {
const { selectedRecords, objectMetadataItem } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
@@ -1,5 +1,5 @@
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
@@ -8,7 +8,7 @@ import { isDefined } from 'twenty-shared/utils';
export const RestoreSingleRecordCommand = () => {
const { recordIndexId, objectMetadataItem, selectedRecords } =
useMountedEngineCommandContext();
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
@@ -8,7 +8,7 @@ import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
export const CancelDashboardSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const selectedRecord = selectedRecords[0];
if (!isDefined(selectedRecord)) {
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useDuplicateDashboard } from '@/dashboards/hooks/useDuplicateDashboard';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro';
@@ -9,7 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const DuplicateDashboardSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const { duplicateDashboard } = useDuplicateDashboard();
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { isDefined } from 'twenty-shared/utils';
import { useResetLocationHash } from 'twenty-ui/utilities';
export const EditDashboardSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const selectedRecord = selectedRecords[0];
if (!isDefined(selectedRecord)) {
@@ -1,12 +1,12 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isDefined } from 'twenty-shared/utils';
export const SaveDashboardSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const selectedRecord = selectedRecords[0];
if (!isDefined(selectedRecord)) {
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
@@ -10,7 +10,7 @@ import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
export const CancelRecordPageLayoutSingleRecordCommand = () => {
const { objectMetadataItem } = useMountedEngineCommandContext();
const { objectMetadataItem } = useHeadlessCommandContextApi();
if (!isDefined(objectMetadataItem)) {
throw new Error(
@@ -1,12 +1,12 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { isDefined } from 'twenty-shared/utils';
import { useResetLocationHash } from 'twenty-ui/utilities';
export const EditRecordPageLayoutSingleRecordCommand = () => {
const { objectMetadataItem } = useMountedEngineCommandContext();
const { objectMetadataItem } = useHeadlessCommandContextApi();
if (!isDefined(objectMetadataItem)) {
throw new Error(
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
@@ -8,7 +8,7 @@ import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isDefined } from 'twenty-shared/utils';
export const SaveRecordPageLayoutSingleRecordCommand = () => {
const { objectMetadataItem } = useMountedEngineCommandContext();
const { objectMetadataItem } = useHeadlessCommandContextApi();
if (!isDefined(objectMetadataItem)) {
throw new Error(
@@ -1,10 +1,10 @@
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const SeeVersionWorkflowRunSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const selectedRecord = selectedRecords[0];
if (
@@ -1,10 +1,10 @@
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { CoreObjectNameSingular, AppPath } from 'twenty-shared/types';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const SeeWorkflowWorkflowRunSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const selectedRecord = selectedRecords[0];
if (!isDefined(selectedRecord) || !isDefined(selectedRecord?.workflow?.id)) {
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { useStopWorkflowRun } from '@/workflow/hooks/useStopWorkflowRun';
@@ -7,8 +7,7 @@ import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const StopWorkflowRunSingleRecordCommand = () => {
const { targetedRecordsRule, graphqlFilter } =
useMountedEngineCommandContext();
const { targetedRecordsRule, graphqlFilter } = useHeadlessCommandContextApi();
const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
@@ -1,5 +1,5 @@
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
@@ -37,7 +37,7 @@ const SeeRunsWorkflowVersionSingleRecordCommandContent = ({
};
export const SeeRunsWorkflowVersionSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const selectedRecord = selectedRecords[0];
@@ -1,5 +1,5 @@
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
@@ -30,7 +30,7 @@ const SeeVersionsWorkflowVersionSingleRecordCommandContent = ({
};
export const SeeVersionsWorkflowVersionSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const selectedRecord = selectedRecords[0];
if (!isDefined(selectedRecord) || !isDefined(selectedRecord.workflowId)) {
@@ -1,10 +1,10 @@
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const SeeWorkflowWorkflowVersionSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const selectedRecord = selectedRecords[0];
if (!isDefined(selectedRecord) || !isDefined(selectedRecord?.workflow?.id)) {
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal';
import { OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID } from '@/workflow/constants/OverrideWorkflowDraftConfirmationModalId';
@@ -66,7 +66,7 @@ const UseAsDraftWorkflowVersionSingleRecordCommandContent = ({
};
export const UseAsDraftWorkflowVersionSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const workflowVersion = useWorkflowVersion(recordId ?? '');
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-shared/utils';
export const ActivateWorkflowSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const { activateWorkflowVersion } = useActivateWorkflowVersion();
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useSidePanelWorkflowNavigation } from '@/side-panel/pages/workflow/hooks/useSidePanelWorkflowNavigation';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-shared/utils';
export const AddNodeWorkflowSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-shared/utils';
export const DeactivateWorkflowSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-shared/utils';
export const DiscardDraftWorkflowSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useDuplicateWorkflow } from '@/workflow/hooks/useDuplicateWorkflow';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
@@ -10,7 +10,7 @@ import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const DuplicateWorkflowSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const workflow = useWorkflowWithCurrentVersion(recordId ?? '');
@@ -1,11 +1,11 @@
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useActiveWorkflowVersion } from '@/workflow/hooks/useActiveWorkflowVersion';
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const SeeActiveVersionWorkflowSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
@@ -1,12 +1,12 @@
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const SeeRunsWorkflowSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
@@ -1,12 +1,12 @@
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const SeeVersionsWorkflowSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
@@ -1,11 +1,11 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { isDefined } from 'twenty-shared/utils';
export const TestWorkflowSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const { runWorkflowVersion } = useRunWorkflowVersion();
@@ -1,5 +1,5 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
@@ -9,7 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
export const TidyUpWorkflowSingleRecordCommand = () => {
const store = useStore();
const { selectedRecords } = useMountedEngineCommandContext();
const { selectedRecords } = useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
const { tidyUpWorkflowVersion } = useTidyUpWorkflowVersion();
@@ -1,4 +1,4 @@
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
import { headlessCommandContextApisState } from '@/command-menu-item/engine-command/states/headlessCommandContextApisState';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
export const isEngineCommandMountedFamilySelector = createAtomFamilySelector<
@@ -9,8 +9,8 @@ export const isEngineCommandMountedFamilySelector = createAtomFamilySelector<
get:
(engineCommandId: string) =>
({ get }) => {
const mountedMap = get(mountedEngineCommandsState);
const headlessCommandContextApis = get(headlessCommandContextApisState);
return mountedMap.has(engineCommandId);
return headlessCommandContextApis.has(engineCommandId);
},
});
@@ -1,4 +1,3 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const EngineCommandComponentInstanceContext =
createComponentInstanceContext();
export const CommandComponentInstanceContext = createComponentInstanceContext();
@@ -0,0 +1,9 @@
import { type HeadlessCommandContextApi } from '@/command-menu-item/engine-command/types/HeadlessCommandContextApi';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const headlessCommandContextApisState = createAtomState<
Map<string, HeadlessCommandContextApi>
>({
key: 'headlessCommandContextApisState',
defaultValue: new Map(),
});
@@ -1,9 +0,0 @@
import { type MountedEngineCommandState } from '@/command-menu-item/engine-command/types/MountedEngineCommandContext';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const mountedEngineCommandsState = createAtomState<
Map<string, MountedEngineCommandState>
>({
key: 'mountedEngineCommandsState',
defaultValue: new Map(),
});
@@ -7,7 +7,7 @@ import {
} from 'twenty-shared/types';
import { type EngineComponentKey } from '~/generated-metadata/graphql';
export type MountedEngineCommandState = {
export type HeadlessEngineCommandContextApi = {
engineComponentKey: EngineComponentKey;
contextStoreInstanceId: string;
objectMetadataItem: Nullable<EnrichedObjectMetadataItem>;
@@ -17,3 +17,20 @@ export type MountedEngineCommandState = {
selectedRecords: ObjectRecord[];
graphqlFilter: Nullable<RecordGqlOperationFilter>;
};
export type HeadlessFrontComponentCommandContextApi =
HeadlessEngineCommandContextApi & {
frontComponentId: string;
};
export type HeadlessTriggerWorkflowVersionCommandContextApi =
HeadlessEngineCommandContextApi & {
workflowId: string;
workflowVersionId: string;
payloads: Record<string, any>[];
};
export type HeadlessCommandContextApi =
| HeadlessEngineCommandContextApi
| HeadlessFrontComponentCommandContextApi
| HeadlessTriggerWorkflowVersionCommandContextApi;
@@ -0,0 +1,119 @@
import type { Store } from 'jotai/vanilla/store';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { type HeadlessEngineCommandContextApi } from '@/command-menu-item/engine-command/types/HeadlessCommandContextApi';
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { isDefined } from 'twenty-shared/utils';
import { type EngineComponentKey } from '~/generated-metadata/graphql';
export const buildHeadlessCommandContextApi = ({
store,
contextStoreInstanceId,
engineComponentKey,
}: {
store: Store;
contextStoreInstanceId: string;
engineComponentKey: EngineComponentKey;
}): HeadlessEngineCommandContextApi => {
const objectMetadataItemId = store.get(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const objectMetadataItem = objectMetadataItems.find(
(item: EnrichedObjectMetadataItem) => item.id === objectMetadataItemId,
);
const currentViewId = store.get(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const targetedRecordsRule = store.get(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const selectedRecords = (
targetedRecordsRule.mode === 'selection'
? targetedRecordsRule.selectedRecordIds
: []
)
.map((id: string) => store.get(recordStoreFamilyState.atomFamily(id)))
.filter(isDefined);
const filters = store.get(
contextStoreFiltersComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const filterGroups = store.get(
contextStoreFilterGroupsComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const anyFieldFilterValue = store.get(
contextStoreAnyFieldFilterValueComponentState.atomFamily({
instanceId: contextStoreInstanceId,
}),
);
const currentWorkspaceMember = store.get(currentWorkspaceMemberState.atom);
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const userTimezone =
currentWorkspaceMember?.timeZone !== 'system'
? (currentWorkspaceMember?.timeZone ?? systemTimeZone)
: systemTimeZone;
const graphqlFilter = isDefined(objectMetadataItem)
? computeContextStoreFilters({
contextStoreTargetedRecordsRule: targetedRecordsRule,
contextStoreFilters: filters,
contextStoreFilterGroups: filterGroups,
objectMetadataItem,
filterValueDependencies: {
currentWorkspaceMemberId: currentWorkspaceMember?.id,
timeZone: userTimezone,
},
contextStoreAnyFieldFilterValue: anyFieldFilterValue,
})
: null;
const recordIndexId =
objectMetadataItem && currentViewId
? getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
currentViewId,
)
: null;
return {
engineComponentKey,
contextStoreInstanceId,
objectMetadataItem: objectMetadataItem ?? null,
currentViewId,
recordIndexId,
targetedRecordsRule,
selectedRecords,
graphqlFilter,
};
};
@@ -0,0 +1,84 @@
import type { Store } from 'jotai/vanilla/store';
import { isBulkRecordsManualTrigger } from '@/command-menu-item/record/utils/isBulkRecordsManualTrigger';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type WorkflowTrigger } from '@/workflow/types/Workflow';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import {
type CommandMenuItemAvailabilityType,
CommandMenuItemAvailabilityType as CommandMenuItemAvailabilityTypeEnum,
} from '~/generated-metadata/graphql';
export const buildTriggerWorkflowVersionPayloads = ({
store,
trigger,
availabilityType,
availabilityObjectMetadataId,
objectMetadataItems,
selectedRecordIds,
}: {
store: Store;
trigger: WorkflowTrigger | null;
availabilityType: CommandMenuItemAvailabilityType;
availabilityObjectMetadataId?: string | null;
objectMetadataItems: EnrichedObjectMetadataItem[];
selectedRecordIds: string[];
}): Record<string, any>[] => {
const payloads: Record<string, any>[] = [];
switch (availabilityType) {
case CommandMenuItemAvailabilityTypeEnum.RECORD_SELECTION: {
if (selectedRecordIds.length === 0) {
return payloads;
}
const limitedSelectedRecordIds = selectedRecordIds.slice(
0,
QUERY_MAX_RECORDS,
);
const objectMetadataItem = objectMetadataItems.find(
(metadata) => metadata.id === availabilityObjectMetadataId,
);
if (isDefined(trigger) && isBulkRecordsManualTrigger(trigger)) {
const selectedRecords = limitedSelectedRecordIds
.map((recordId) =>
store.get(recordStoreFamilyState.atomFamily(recordId)),
)
.filter(isDefined);
if (isDefined(objectMetadataItem)) {
payloads.push({
[objectMetadataItem.namePlural]: selectedRecords,
});
}
return payloads;
}
for (const selectedRecordId of limitedSelectedRecordIds) {
const selectedRecord = store.get(
recordStoreFamilyState.atomFamily(selectedRecordId),
);
if (!isDefined(selectedRecord)) {
continue;
}
payloads.push(selectedRecord);
}
return payloads;
}
case CommandMenuItemAvailabilityTypeEnum.GLOBAL:
case CommandMenuItemAvailabilityTypeEnum.FALLBACK: {
return payloads;
}
default: {
return payloads;
}
}
};
@@ -0,0 +1,9 @@
import {
type HeadlessCommandContextApi,
type HeadlessFrontComponentCommandContextApi,
} from '@/command-menu-item/engine-command/types/HeadlessCommandContextApi';
export const isHeadlessFrontComponentCommandContextApi = (
state: HeadlessCommandContextApi,
): state is HeadlessFrontComponentCommandContextApi =>
'frontComponentId' in state;
@@ -0,0 +1,9 @@
import {
type HeadlessCommandContextApi,
type HeadlessTriggerWorkflowVersionCommandContextApi,
} from '@/command-menu-item/engine-command/types/HeadlessCommandContextApi';
export const isHeadlessTriggerWorkflowVersionCommandContextApi = (
state: HeadlessCommandContextApi,
): state is HeadlessTriggerWorkflowVersionCommandContextApi =>
'workflowId' in state;
@@ -1,7 +1,5 @@
import { EngineCommandMenuItem } from '@/command-menu-item/display/components/EngineCommandMenuItem';
import { FrontComponentCommandMenuItem } from '@/command-menu-item/display/components/FrontComponentCommandMenuItem';
import { HeadlessFrontComponentCommandMenuItem } from '@/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem';
import { WorkflowCommandMenuItem } from '@/command-menu-item/display/components/WorkflowCommandMenuItem';
import { HeadlessCommandMenuItem } from '@/command-menu-item/display/components/HeadlessCommandMenuItem';
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
@@ -19,6 +17,7 @@ import { useIcons } from 'twenty-ui/display';
import {
CommandMenuItemAvailabilityType,
type CommandMenuItemFieldsFragment,
EngineComponentKey,
} from '~/generated-metadata/graphql';
const resolveScope = (
@@ -36,16 +35,15 @@ const resolveType = (
if (item.availabilityType === CommandMenuItemAvailabilityType.FALLBACK) {
return CommandMenuItemType.Fallback;
}
if (isDefined(item.engineComponentKey)) {
return CommandMenuItemType.Standard;
switch (item.engineComponentKey) {
case EngineComponentKey.FRONT_COMPONENT_RENDERER:
return CommandMenuItemType.FrontComponent;
case EngineComponentKey.TRIGGER_WORKFLOW_VERSION:
return CommandMenuItemType.WorkflowRun;
default:
return CommandMenuItemType.Standard;
}
if (isDefined(item.frontComponentId)) {
return CommandMenuItemType.FrontComponent;
}
if (isDefined(item.workflowVersionId)) {
return CommandMenuItemType.WorkflowRun;
}
return CommandMenuItemType.Standard;
};
// TODO: Remove this hook once we finish refactoring and we use
@@ -101,33 +99,22 @@ export const useConvertBackendItemToCommandMenuItemConfig = () => {
context: commandMenuContextApi,
});
const component = isDefined(item.engineComponentKey) ? (
<EngineCommandMenuItem
commandMenuItemId={item.id}
engineComponentKey={item.engineComponentKey}
/>
) : isDefined(item.frontComponentId) ? (
item.frontComponent?.isHeadless === true ? (
<HeadlessFrontComponentCommandMenuItem
frontComponentId={item.frontComponentId}
commandMenuItemId={item.id}
/>
) : (
<FrontComponentCommandMenuItem
frontComponentId={item.frontComponentId}
/>
)
) : isDefined(item.workflowVersionId) ? (
<WorkflowCommandMenuItem
workflowVersionId={item.workflowVersionId}
availabilityType={item.availabilityType}
availabilityObjectMetadataId={item.availabilityObjectMetadataId}
/>
) : null;
const component = (() => {
if (
item.engineComponentKey ===
EngineComponentKey.FRONT_COMPONENT_RENDERER &&
isDefined(item.frontComponentId) &&
item.frontComponent?.isHeadless !== true
) {
return (
<FrontComponentCommandMenuItem
frontComponentId={item.frontComponentId}
/>
);
}
if (!isDefined(component)) {
return;
}
return <HeadlessCommandMenuItem item={item} />;
})();
return {
type: resolveType(item),
@@ -1,60 +0,0 @@
import { Suspense, lazy } from 'react';
import { CommandMenuItemErrorBoundary } from '@/command-menu-item/display/components/CommandMenuItemErrorBoundary';
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
import { mountedHeadlessFrontComponentMapsState } from '@/front-components/states/mountedHeadlessFrontComponentMapsState';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
const FrontComponentRenderer = lazy(() =>
import('@/front-components/components/FrontComponentRenderer').then(
(module) => ({ default: module.FrontComponentRenderer }),
),
);
export const HeadlessFrontComponentMountRoot = () => {
const mountedHeadlessFrontComponentMaps = useAtomStateValue(
mountedHeadlessFrontComponentMapsState,
);
const unmountHeadlessFrontComponent = useUnmountHeadlessFrontComponent();
return (
<>
{[...mountedHeadlessFrontComponentMaps.entries()].map(
([frontComponentId, mountContext]) => (
<CommandMenuItemErrorBoundary
key={frontComponentId}
engineCommandId={frontComponentId}
onError={() => unmountHeadlessFrontComponent(frontComponentId)}
>
<Suspense fallback={null}>
<LayoutRenderingProvider
value={{
targetRecordIdentifier:
isDefined(mountContext.objectNameSingular) &&
isDefined(mountContext.recordId)
? {
id: mountContext.recordId,
targetObjectNameSingular:
mountContext.objectNameSingular,
}
: undefined,
layoutType: PageLayoutType.DASHBOARD,
isInSidePanel: false,
}}
>
<FrontComponentRenderer
frontComponentId={frontComponentId}
commandMenuItemId={mountContext.commandMenuItemId}
/>
</LayoutRenderingProvider>
</Suspense>
</CommandMenuItemErrorBoundary>
),
)}
</>
);
};
@@ -7,9 +7,9 @@ import { type AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types';
import { currentUserState } from '@/auth/states/currentUserState';
import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
import { useUnmountCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
@@ -40,7 +40,7 @@ export const useFrontComponentExecutionContext = ({
const { navigateSidePanel } = useNavigateSidePanel();
const setSidePanelSearch = useSetAtomState(sidePanelSearchState);
const { getIcon } = useIcons();
const unmountHeadlessFrontComponent = useUnmountHeadlessFrontComponent();
const unmountEngineCommand = useUnmountCommand();
const {
enqueueSuccessSnackBar,
enqueueErrorSnackBar,
@@ -133,7 +133,9 @@ export const useFrontComponentExecutionContext = ({
const unmountFrontComponent: FrontComponentHostCommunicationApi['unmountFrontComponent'] =
async () => {
unmountHeadlessFrontComponent(frontComponentId);
if (isDefined(commandMenuItemId)) {
unmountEngineCommand(commandMenuItemId);
}
};
const closeSidePanel: FrontComponentHostCommunicationApi['closeSidePanel'] =
@@ -1,23 +0,0 @@
import { useCallback } from 'react';
import {
type HeadlessFrontComponentMountContext,
mountedHeadlessFrontComponentMapsState,
} from '@/front-components/states/mountedHeadlessFrontComponentMapsState';
import { useStore } from 'jotai';
export const useMountHeadlessFrontComponent = () => {
const store = useStore();
const mountHeadlessFrontComponent = useCallback(
(frontComponentId: string, context: HeadlessFrontComponentMountContext) => {
store.set(mountedHeadlessFrontComponentMapsState.atom, (previousMap) => {
const next = new Map(previousMap);
next.set(frontComponentId, context);
return next;
});
},
[store],
);
return mountHeadlessFrontComponent;
};
@@ -1,33 +0,0 @@
import { useCallback } from 'react';
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
import { mountedHeadlessFrontComponentMapsState } from '@/front-components/states/mountedHeadlessFrontComponentMapsState';
import { useStore } from 'jotai';
export const useUnmountHeadlessFrontComponent = () => {
const store = useStore();
const unmountHeadlessFrontComponent = useCallback(
(frontComponentId: string) => {
const currentMap = store.get(mountedHeadlessFrontComponentMapsState.atom);
const mountContext = currentMap.get(frontComponentId);
store.set(mountedHeadlessFrontComponentMapsState.atom, (previousMap) => {
const next = new Map(previousMap);
next.delete(frontComponentId);
return next;
});
if (mountContext) {
store.set(
commandMenuItemProgressFamilyState.atomFamily(
mountContext.commandMenuItemId,
),
undefined,
);
}
},
[store],
);
return unmountHeadlessFrontComponent;
};
@@ -1,14 +0,0 @@
import { mountedHeadlessFrontComponentMapsState } from '@/front-components/states/mountedHeadlessFrontComponentMapsState';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
export const isHeadlessFrontComponentMountedFamilySelector =
createAtomFamilySelector<boolean, string>({
key: 'isHeadlessFrontComponentMountedFamilySelector',
get:
(frontComponentId: string) =>
({ get }) => {
const mountedMap = get(mountedHeadlessFrontComponentMapsState);
return mountedMap.has(frontComponentId);
},
});
@@ -1,14 +0,0 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export type HeadlessFrontComponentMountContext = {
commandMenuItemId: string;
recordId?: string;
objectNameSingular?: string;
};
export const mountedHeadlessFrontComponentMapsState = createAtomState<
Map<string, HeadlessFrontComponentMountContext>
>({
key: 'mountedHeadlessFrontComponentMapsState',
defaultValue: new Map(),
});
@@ -2067,7 +2067,7 @@ type CommandMenuItem {
workflowVersionId: UUID
frontComponentId: UUID
frontComponent: FrontComponent
engineComponentKey: EngineComponentKey
engineComponentKey: EngineComponentKey!
label: String!
icon: String
shortLabel: String
@@ -2140,6 +2140,8 @@ enum EngineComponentKey {
SEARCH_RECORDS_FALLBACK
ASK_AI
VIEW_PREVIOUS_AI_CHATS
TRIGGER_WORKFLOW_VERSION
FRONT_COMPONENT_RENDERER
}
enum CommandMenuItemAvailabilityType {
@@ -3896,7 +3898,7 @@ input UpdateLogicFunctionFromSourceInputUpdates {
input CreateCommandMenuItemInput {
workflowVersionId: UUID
frontComponentId: UUID
engineComponentKey: EngineComponentKey
engineComponentKey: EngineComponentKey!
label: String!
icon: String
shortLabel: String
@@ -1788,7 +1788,7 @@ export interface CommandMenuItem {
workflowVersionId?: Scalars['UUID']
frontComponentId?: Scalars['UUID']
frontComponent?: FrontComponent
engineComponentKey?: EngineComponentKey
engineComponentKey: EngineComponentKey
label: Scalars['String']
icon?: Scalars['String']
shortLabel?: Scalars['String']
@@ -1804,7 +1804,7 @@ export interface CommandMenuItem {
__typename: 'CommandMenuItem'
}
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'EXPORT_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'
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'EXPORT_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'
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'RECORD_SELECTION' | 'FALLBACK'
@@ -6391,7 +6391,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 | null),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)}
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)}
@@ -9176,7 +9176,9 @@ export const enumEngineComponentKey = {
SEARCH_RECORDS: 'SEARCH_RECORDS' as const,
SEARCH_RECORDS_FALLBACK: 'SEARCH_RECORDS_FALLBACK' as const,
ASK_AI: 'ASK_AI' as const,
VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const
VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const,
TRIGGER_WORKFLOW_VERSION: 'TRIGGER_WORKFLOW_VERSION' as const,
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const
}
export const enumCommandMenuItemAvailabilityType = {
@@ -13,6 +13,7 @@ import { ApplicationService } from 'src/engine/core-modules/application/applicat
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -293,7 +294,7 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
workflowVersionId: workflowVersion.id,
frontComponentId: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: null,
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label,
shortLabel: label,
icon: trigger.settings.icon ?? null,
@@ -0,0 +1,55 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class ConvertEngineComponentKeyToVarchar1774363913813
implements MigrationInterface
{
name = 'ConvertEngineComponentKeyToVarchar1774363913813';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" DROP CONSTRAINT "CHK_CMD_MENU_ITEM_WF_OR_FC_OR_ENGINE_KEY"`,
);
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "engineComponentKey" TYPE character varying USING "engineComponentKey"::text`,
);
await queryRunner.query(
`DROP TYPE "core"."commandMenuItem_enginecomponentkey_enum"`,
);
await queryRunner.query(
`UPDATE "core"."commandMenuItem" SET "engineComponentKey" = 'TRIGGER_WORKFLOW_VERSION' WHERE "workflowVersionId" IS NOT NULL AND "engineComponentKey" IS NULL`,
);
await queryRunner.query(
`UPDATE "core"."commandMenuItem" SET "engineComponentKey" = 'FRONT_COMPONENT_RENDERER' WHERE "frontComponentId" IS NOT NULL AND "engineComponentKey" IS NULL`,
);
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" ALTER COLUMN "engineComponentKey" SET NOT NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "engineComponentKey" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" DROP CONSTRAINT "CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE"`,
);
await queryRunner.query(
`UPDATE "core"."commandMenuItem" SET "engineComponentKey" = NULL WHERE "engineComponentKey" = 'TRIGGER_WORKFLOW_VERSION' AND "workflowVersionId" IS NOT NULL`,
);
await queryRunner.query(
`UPDATE "core"."commandMenuItem" SET "engineComponentKey" = NULL WHERE "engineComponentKey" = 'FRONT_COMPONENT_RENDERER' AND "frontComponentId" IS NOT NULL`,
);
await queryRunner.query(
`CREATE TYPE "core"."commandMenuItem_enginecomponentkey_enum" AS ENUM('ACTIVATE_WORKFLOW', 'ADD_NODE_WORKFLOW', 'ADD_TO_FAVORITES', 'ASK_AI', 'CANCEL_DASHBOARD_LAYOUT', 'CREATE_NEW_RECORD', 'CREATE_NEW_VIEW', 'DEACTIVATE_WORKFLOW', 'DELETE_MULTIPLE_RECORDS', 'DELETE_SINGLE_RECORD', 'DESTROY_MULTIPLE_RECORDS', 'DESTROY_SINGLE_RECORD', 'DISCARD_DRAFT_WORKFLOW', 'DUPLICATE_DASHBOARD', 'DUPLICATE_WORKFLOW', 'EDIT_DASHBOARD_LAYOUT', 'EDIT_RECORD_PAGE_LAYOUT', 'EXPORT_FROM_RECORD_INDEX', 'EXPORT_FROM_RECORD_SHOW', 'EXPORT_MULTIPLE_RECORDS', 'EXPORT_NOTE_TO_PDF', 'EXPORT_VIEW', 'FRONT_COMPONENT_RENDERER', 'GO_TO_COMPANIES', 'GO_TO_DASHBOARDS', 'GO_TO_NOTES', 'GO_TO_OPPORTUNITIES', 'GO_TO_PEOPLE', 'GO_TO_RUNS', 'GO_TO_SETTINGS', 'GO_TO_TASKS', 'GO_TO_WORKFLOWS', 'HIDE_DELETED_RECORDS', 'IMPORT_RECORDS', 'MERGE_MULTIPLE_RECORDS', 'NAVIGATE_TO_NEXT_RECORD', 'NAVIGATE_TO_PREVIOUS_RECORD', 'REMOVE_FROM_FAVORITES', 'RESTORE_MULTIPLE_RECORDS', 'RESTORE_SINGLE_RECORD', 'SAVE_DASHBOARD_LAYOUT', 'SEARCH_RECORDS', 'SEARCH_RECORDS_FALLBACK', 'SEE_ACTIVE_VERSION_WORKFLOW', 'SEE_DELETED_RECORDS', 'SEE_RUNS_WORKFLOW', 'SEE_RUNS_WORKFLOW_VERSION', 'SEE_VERSIONS_WORKFLOW', 'SEE_VERSIONS_WORKFLOW_VERSION', 'SEE_VERSION_WORKFLOW_RUN', 'SEE_WORKFLOW_WORKFLOW_RUN', 'SEE_WORKFLOW_WORKFLOW_VERSION', 'STOP_WORKFLOW_RUN', 'TEST_WORKFLOW', 'TIDY_UP_WORKFLOW', 'TRIGGER_WORKFLOW_VERSION', 'UPDATE_MULTIPLE_RECORDS', 'USE_AS_DRAFT_WORKFLOW_VERSION', 'VIEW_PREVIOUS_AI_CHATS')`,
);
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "engineComponentKey" TYPE "core"."commandMenuItem_enginecomponentkey_enum" USING "engineComponentKey"::"core"."commandMenuItem_enginecomponentkey_enum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" ADD CONSTRAINT "CHK_CMD_MENU_ITEM_WF_OR_FC_OR_ENGINE_KEY" CHECK (((("workflowVersionId" IS NOT NULL) AND ("frontComponentId" IS NULL) AND ("engineComponentKey" IS NULL)) OR (("workflowVersionId" IS NULL) AND ("frontComponentId" IS NOT NULL) AND ("engineComponentKey" IS NULL)) OR (("workflowVersionId" IS NULL) AND ("frontComponentId" IS NULL) AND ("engineComponentKey" IS NOT NULL))))`,
);
}
}
@@ -1,6 +1,7 @@
import { type CommandMenuItemManifest } from 'twenty-shared/application';
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 UniversalFlatCommandMenuItem } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-command-menu-item.type';
const AVAILABILITY_TYPE_MAP: Record<
@@ -38,7 +39,7 @@ export const fromCommandMenuItemManifestToUniversalFlatCommandMenuItem = ({
commandMenuItemManifest.frontComponentUniversalIdentifier,
availabilityObjectMetadataUniversalIdentifier:
commandMenuItemManifest.availabilityObjectUniversalIdentifier ?? null,
engineComponentKey: null,
engineComponentKey: EngineComponentKey.FRONT_COMPONENT_RENDERER,
hotKeys: null,
workflowVersionId: null,
createdAt: now,
@@ -37,9 +37,9 @@ export class CommandMenuItemDTO {
frontComponent?: FrontComponentDTO | null;
@IsEnum(EngineComponentKey)
@IsOptional()
@Field(() => EngineComponentKey, { nullable: true })
engineComponentKey?: EngineComponentKey;
@IsNotEmpty()
@Field(() => EngineComponentKey)
engineComponentKey: EngineComponentKey;
@IsString()
@IsNotEmpty()
@@ -27,9 +27,9 @@ export class CreateCommandMenuItemInput {
frontComponentId?: string;
@IsEnum(EngineComponentKey)
@IsOptional()
@Field(() => EngineComponentKey, { nullable: true })
engineComponentKey?: EngineComponentKey;
@IsNotEmpty()
@Field(() => EngineComponentKey)
engineComponentKey: EngineComponentKey;
@IsString()
@IsNotEmpty()
@@ -30,8 +30,8 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
'availabilityObjectMetadataId',
])
@Check(
'CHK_CMD_MENU_ITEM_WF_OR_FC_OR_ENGINE_KEY',
'("workflowVersionId" IS NOT NULL AND "frontComponentId" IS NULL AND "engineComponentKey" IS NULL) OR ("workflowVersionId" IS NULL AND "frontComponentId" IS NOT NULL AND "engineComponentKey" IS NULL) OR ("workflowVersionId" IS NULL AND "frontComponentId" IS NULL AND "engineComponentKey" IS NOT NULL)',
'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)`,
)
export class CommandMenuItemEntity
extends SyncableEntity
@@ -53,12 +53,8 @@ export class CommandMenuItemEntity
@JoinColumn({ name: 'frontComponentId' })
frontComponent: Relation<FrontComponentEntity> | null;
@Column({
type: 'enum',
enum: Object.values(EngineComponentKey),
nullable: true,
})
engineComponentKey: EngineComponentKey | null;
@Column({ type: 'varchar', nullable: false })
engineComponentKey: EngineComponentKey;
@Column({ nullable: false })
label: string;
@@ -58,6 +58,8 @@ export enum EngineComponentKey {
SEARCH_RECORDS_FALLBACK = 'SEARCH_RECORDS_FALLBACK',
ASK_AI = 'ASK_AI',
VIEW_PREVIOUS_AI_CHATS = 'VIEW_PREVIOUS_AI_CHATS',
TRIGGER_WORKFLOW_VERSION = 'TRIGGER_WORKFLOW_VERSION',
FRONT_COMPONENT_RENDERER = 'FRONT_COMPONENT_RENDERER',
}
registerEnumType(EngineComponentKey, {
@@ -1,11 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import {
CommandMenuItemException,
CommandMenuItemExceptionCode,
} from 'src/engine/metadata-modules/command-menu-item/command-menu-item.exception';
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 { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
@@ -26,29 +21,6 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
AllFlatEntityMaps,
'flatObjectMetadataMaps' | 'flatFrontComponentMaps'
>): FlatCommandMenuItem => {
const hasWorkflowVersionId = isDefined(
createCommandMenuItemInput.workflowVersionId,
);
const hasFrontComponentId = isDefined(
createCommandMenuItemInput.frontComponentId,
);
const hasEngineComponentKey = isDefined(
createCommandMenuItemInput.engineComponentKey,
);
const sourceCount = [
hasWorkflowVersionId,
hasFrontComponentId,
hasEngineComponentKey,
].filter(Boolean).length;
if (sourceCount !== 1) {
throw new CommandMenuItemException(
'Exactly one of workflowVersionId, frontComponentId or engineComponentKey is required',
CommandMenuItemExceptionCode.WORKFLOW_OR_FRONT_COMPONENT_REQUIRED,
);
}
const id = uuidv4();
const now = new Date().toISOString();
@@ -71,7 +43,7 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
workflowVersionId: createCommandMenuItemInput.workflowVersionId ?? null,
frontComponentId: createCommandMenuItemInput.frontComponentId ?? null,
frontComponentUniversalIdentifier,
engineComponentKey: createCommandMenuItemInput.engineComponentKey ?? null,
engineComponentKey: createCommandMenuItemInput.engineComponentKey,
label: createCommandMenuItemInput.label,
icon: createCommandMenuItemInput.icon ?? null,
shortLabel: createCommandMenuItemInput.shortLabel ?? null,
@@ -7,7 +7,7 @@ export const fromFlatCommandMenuItemToCommandMenuItemDto = (
id: flatCommandMenuItem.id,
workflowVersionId: flatCommandMenuItem.workflowVersionId ?? undefined,
frontComponentId: flatCommandMenuItem.frontComponentId ?? undefined,
engineComponentKey: flatCommandMenuItem.engineComponentKey ?? undefined,
engineComponentKey: flatCommandMenuItem.engineComponentKey,
label: flatCommandMenuItem.label,
icon: flatCommandMenuItem.icon ?? undefined,
shortLabel: flatCommandMenuItem.shortLabel ?? undefined,
@@ -1,6 +1,7 @@
import { type EntityManager } from 'typeorm';
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 { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
@@ -52,7 +53,7 @@ export const prefillWorkflowCommandMenuItems = async (
applicationId: applicationRow.app_id,
workflowVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
frontComponentId: null,
engineComponentKey: null,
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Quick Lead',
icon: 'IconUserPlus',
shortLabel: 'Quick Lead',
@@ -6,6 +6,7 @@ import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { CommandMenuItemExceptionCode } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.exception';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
@@ -35,30 +36,22 @@ export class FlatCommandMenuItemValidatorService {
});
}
const hasWorkflowVersionId = isDefined(
flatCommandMenuItem.workflowVersionId,
);
const hasFrontComponentUniversalIdentifier = isDefined(
flatCommandMenuItem.frontComponentUniversalIdentifier,
);
const hasEngineComponentKey = isDefined(
flatCommandMenuItem.engineComponentKey,
);
const sourceCount = [
hasWorkflowVersionId,
hasFrontComponentUniversalIdentifier,
hasEngineComponentKey,
].filter(Boolean).length;
if (sourceCount !== 1) {
if (!isDefined(flatCommandMenuItem.engineComponentKey)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.WORKFLOW_OR_FRONT_COMPONENT_REQUIRED,
message: t`Exactly one of workflowVersionId, frontComponentUniversalIdentifier or engineComponentKey is required`,
userFriendlyMessage: msg`Exactly one of workflow version, front component or engine component key is required`,
code: CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT,
message: t`engineComponentKey is required`,
userFriendlyMessage: msg`Engine component key is required`,
});
}
this.validateEngineComponentKeyCoherence({
engineComponentKey: flatCommandMenuItem.engineComponentKey,
workflowVersionId: flatCommandMenuItem.workflowVersionId,
frontComponentUniversalIdentifier:
flatCommandMenuItem.frontComponentUniversalIdentifier,
validationResult,
});
return validationResult;
}
@@ -136,6 +129,97 @@ export class FlatCommandMenuItemValidatorService {
});
}
const engineComponentKey =
flatEntityUpdate.engineComponentKey ??
fromFlatCommandMenuItem.engineComponentKey;
this.validateEngineComponentKeyCoherence({
engineComponentKey,
workflowVersionId: fromFlatCommandMenuItem.workflowVersionId,
frontComponentUniversalIdentifier:
fromFlatCommandMenuItem.frontComponentUniversalIdentifier,
validationResult,
});
return validationResult;
}
private validateEngineComponentKeyCoherence({
engineComponentKey,
workflowVersionId,
frontComponentUniversalIdentifier,
validationResult,
}: {
engineComponentKey: EngineComponentKey | null;
workflowVersionId: string | null;
frontComponentUniversalIdentifier: string | null;
validationResult: FailedFlatEntityValidation<
'commandMenuItem',
'create' | 'update'
>;
}): void {
if (!isDefined(engineComponentKey)) {
return;
}
switch (engineComponentKey) {
case EngineComponentKey.TRIGGER_WORKFLOW_VERSION: {
if (!isNonEmptyString(workflowVersionId)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT,
message: t`workflowVersionId is required when engineComponentKey is TRIGGER_WORKFLOW_VERSION`,
userFriendlyMessage: msg`Workflow version is required for workflow trigger items`,
});
}
if (isNonEmptyString(frontComponentUniversalIdentifier)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT,
message: t`frontComponentId must not be set when engineComponentKey is TRIGGER_WORKFLOW_VERSION`,
userFriendlyMessage: msg`Front component must not be set for workflow trigger items`,
});
}
break;
}
case EngineComponentKey.FRONT_COMPONENT_RENDERER: {
if (!isNonEmptyString(frontComponentUniversalIdentifier)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT,
message: t`frontComponentId is required when engineComponentKey is FRONT_COMPONENT_RENDERER`,
userFriendlyMessage: msg`Front component is required for front component renderer items`,
});
}
if (isNonEmptyString(workflowVersionId)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT,
message: t`workflowVersionId must not be set when engineComponentKey is FRONT_COMPONENT_RENDERER`,
userFriendlyMessage: msg`Workflow version must not be set for front component renderer items`,
});
}
break;
}
default: {
if (isNonEmptyString(workflowVersionId)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT,
message: t`workflowVersionId must not be set for engine component key ${engineComponentKey}`,
userFriendlyMessage: msg`Workflow version must not be set for this item type`,
});
}
if (isNonEmptyString(frontComponentUniversalIdentifier)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT,
message: t`frontComponentId must not be set for engine component key ${engineComponentKey}`,
userFriendlyMessage: msg`Front component must not be set for this item type`,
});
}
break;
}
}
}
}
@@ -6,6 +6,7 @@ import { type ActorMetadata, FeatureFlagKey } from 'twenty-shared/types';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { CommandMenuItemService } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.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 WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
@@ -432,6 +433,7 @@ export class WorkflowTriggerWorkspaceService {
await this.commandMenuItemService.create(
{
workflowVersionId: workflowVersion.id,
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label,
shortLabel: label,
icon: trigger.settings.icon,
@@ -1,17 +1,5 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`CommandMenuItem creation should fail when creating with both workflowVersionId and frontComponentId 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "WORKFLOW_OR_FRONT_COMPONENT_REQUIRED",
"userFriendlyMessage": "Either workflow version or front component is required.",
},
"message": "Exactly one of workflowVersionId, frontComponentId or engineComponentKey is required",
"name": "UserInputError",
}
`;
exports[`CommandMenuItem creation should fail when creating with empty label 1`] = `
{
"extensions": {
@@ -47,6 +35,34 @@ exports[`CommandMenuItem creation should fail when creating with empty label 1`]
}
`;
exports[`CommandMenuItem creation should fail when creating with missing label 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Field "label" of required type "String!" was not provided.",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem creation should fail when creating with missing engineComponentKey 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Field "engineComponentKey" of required type "EngineComponentKey!" was not provided.",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem creation should fail when creating with empty workflowVersionId 1`] = `
{
"extensions": {
@@ -77,28 +93,143 @@ exports[`CommandMenuItem creation should fail when creating with invalid workflo
}
`;
exports[`CommandMenuItem creation should fail when creating with missing label 1`] = `
exports[`CommandMenuItem creation should fail when creating TRIGGER_WORKFLOW_VERSION without workflowVersionId 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"commandMenuItem": [
{
"errors": [
{
"code": "INVALID_COMMAND_MENU_ITEM_INPUT",
"message": "workflowVersionId is required when engineComponentKey is TRIGGER_WORKFLOW_VERSION",
"userFriendlyMessage": "Workflow version is required for workflow trigger items",
},
],
"flatEntityMinimalInformation": {
"universalIdentifier": Any<String>,
},
"metadataName": "commandMenuItem",
"status": "fail",
"type": "create",
},
],
},
"userFriendlyMessage": "An error occurred.",
"message": "Validation failed for 1 commandMenuItem",
"summary": {
"commandMenuItem": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Field "label" of required type "String!" was not provided.",
"message": "Multiple validation errors occurred while creating command menu item",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem creation should fail when creating with missing workflowVersionId 1`] = `
exports[`CommandMenuItem creation should fail when creating TRIGGER_WORKFLOW_VERSION with frontComponentId 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "WORKFLOW_OR_FRONT_COMPONENT_REQUIRED",
"userFriendlyMessage": "Either workflow version or front component is required.",
"code": "NOT_FOUND",
"subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND",
"userFriendlyMessage": "An error occurred.",
},
"message": "Exactly one of workflowVersionId, frontComponentId or engineComponentKey is required",
"name": "UserInputError",
"message": "Could not find frontComponent for given frontComponentId",
"name": "NotFoundError",
}
`;
exports[`CommandMenuItem creation should fail when creating FRONT_COMPONENT_RENDERER without frontComponentId 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"commandMenuItem": [
{
"errors": [
{
"code": "INVALID_COMMAND_MENU_ITEM_INPUT",
"message": "frontComponentId is required when engineComponentKey is FRONT_COMPONENT_RENDERER",
"userFriendlyMessage": "Front component is required for front component renderer items",
},
],
"flatEntityMinimalInformation": {
"universalIdentifier": Any<String>,
},
"metadataName": "commandMenuItem",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 1 commandMenuItem",
"summary": {
"commandMenuItem": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Multiple validation errors occurred while creating command menu item",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem creation should fail when creating FRONT_COMPONENT_RENDERER with workflowVersionId 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND",
"userFriendlyMessage": "An error occurred.",
},
"message": "Could not find frontComponent for given frontComponentId",
"name": "NotFoundError",
}
`;
exports[`CommandMenuItem creation should fail when creating standard key with workflowVersionId 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"commandMenuItem": [
{
"errors": [
{
"code": "INVALID_COMMAND_MENU_ITEM_INPUT",
"message": "workflowVersionId must not be set for engine component key GO_TO_PEOPLE",
"userFriendlyMessage": "Workflow version must not be set for this item type",
},
],
"flatEntityMinimalInformation": {
"universalIdentifier": Any<String>,
},
"metadataName": "commandMenuItem",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 1 commandMenuItem",
"summary": {
"commandMenuItem": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Multiple validation errors occurred while creating command menu item",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem creation should fail when creating standard key with frontComponentId 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND",
"userFriendlyMessage": "An error occurred.",
},
"message": "Could not find frontComponent for given frontComponentId",
"name": "NotFoundError",
}
`;
@@ -90,3 +90,38 @@ exports[`CommandMenuItem update should fail when updating with missing id 1`] =
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem update should fail when changing engineComponentKey to a standard key on an item with workflowVersionId 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"commandMenuItem": [
{
"errors": [
{
"code": "INVALID_COMMAND_MENU_ITEM_INPUT",
"message": "workflowVersionId must not be set for engine component key GO_TO_PEOPLE",
"userFriendlyMessage": "Workflow version must not be set for this item type",
},
],
"flatEntityMinimalInformation": {
"universalIdentifier": Any<String>,
},
"metadataName": "commandMenuItem",
"status": "fail",
"type": "update",
},
],
},
"message": "Validation failed for 1 commandMenuItem",
"summary": {
"commandMenuItem": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Multiple validation errors occurred while updating command menu item",
"name": "GraphQLError",
}
`;
@@ -9,6 +9,7 @@ import {
import { FeatureFlagKey } from 'twenty-shared/types';
import { type CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
type TestContext = {
input: CreateCommandMenuItemInput;
@@ -21,6 +22,7 @@ const failingCommandMenuItemCreationTestCases: EachTestingContext<TestContext>[]
context: {
input: {
workflowVersionId: faker.string.uuid(),
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: '',
},
},
@@ -30,11 +32,12 @@ const failingCommandMenuItemCreationTestCases: EachTestingContext<TestContext>[]
context: {
input: {
workflowVersionId: faker.string.uuid(),
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
} as CreateCommandMenuItemInput,
},
},
{
title: 'when creating with missing workflowVersionId',
title: 'when creating with missing engineComponentKey',
context: {
input: {
label: 'Test Label',
@@ -46,6 +49,7 @@ const failingCommandMenuItemCreationTestCases: EachTestingContext<TestContext>[]
context: {
input: {
workflowVersionId: '',
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Test Label',
},
},
@@ -55,16 +59,71 @@ const failingCommandMenuItemCreationTestCases: EachTestingContext<TestContext>[]
context: {
input: {
workflowVersionId: 'not-a-valid-uuid',
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Test Label',
},
},
},
{
title: 'when creating with both workflowVersionId and frontComponentId',
title:
'when creating TRIGGER_WORKFLOW_VERSION without workflowVersionId',
context: {
input: {
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Test Label',
},
},
},
{
title:
'when creating TRIGGER_WORKFLOW_VERSION with frontComponentId',
context: {
input: {
workflowVersionId: faker.string.uuid(),
frontComponentId: faker.string.uuid(),
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Test Label',
},
},
},
{
title:
'when creating FRONT_COMPONENT_RENDERER without frontComponentId',
context: {
input: {
engineComponentKey: EngineComponentKey.FRONT_COMPONENT_RENDERER,
label: 'Test Label',
},
},
},
{
title:
'when creating FRONT_COMPONENT_RENDERER with workflowVersionId',
context: {
input: {
frontComponentId: faker.string.uuid(),
workflowVersionId: faker.string.uuid(),
engineComponentKey: EngineComponentKey.FRONT_COMPONENT_RENDERER,
label: 'Test Label',
},
},
},
{
title: 'when creating standard key with workflowVersionId',
context: {
input: {
workflowVersionId: faker.string.uuid(),
engineComponentKey: EngineComponentKey.GO_TO_PEOPLE,
label: 'Test Label',
},
},
},
{
title: 'when creating standard key with frontComponentId',
context: {
input: {
frontComponentId: faker.string.uuid(),
engineComponentKey: EngineComponentKey.GO_TO_PEOPLE,
label: 'Test Label',
},
},
@@ -10,6 +10,7 @@ import {
} from 'twenty-shared/testing';
import { FeatureFlagKey } from 'twenty-shared/types';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { type UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input';
type TestContext = {
@@ -44,6 +45,7 @@ describe('CommandMenuItem update should fail', () => {
expectToFail: false,
input: {
workflowVersionId: faker.string.uuid(),
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Test Command Menu Item To Update',
icon: 'IconOriginal',
isPinned: false,
@@ -109,6 +111,16 @@ describe('CommandMenuItem update should fail', () => {
}),
},
},
{
title:
'when changing engineComponentKey to a standard key on an item with workflowVersionId',
context: {
input: (testSetup) => ({
id: testSetup.testCommandMenuItemId,
engineComponentKey: EngineComponentKey.GO_TO_PEOPLE,
}),
},
},
];
it.each(eachTestingContextFilter(failingCommandMenuItemUpdateTestCases))(
@@ -10,6 +10,7 @@ import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util
import { FeatureFlagKey } from 'twenty-shared/types';
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';
describe('CommandMenuItem creation should succeed', () => {
let createdCommandMenuItemId: string;
@@ -93,6 +94,7 @@ describe('CommandMenuItem creation should succeed', () => {
expectToFail: false,
input: {
workflowVersionId,
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Test Command Menu Item',
},
});
@@ -117,6 +119,7 @@ describe('CommandMenuItem creation should succeed', () => {
expectToFail: false,
input: {
workflowVersionId,
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Full Command Menu Item',
icon: 'IconSparkles',
isPinned: true,
@@ -145,6 +148,7 @@ describe('CommandMenuItem creation should succeed', () => {
expectToFail: false,
input: {
workflowVersionId,
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Bulk Records Command',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
availabilityObjectMetadataId: personObjectMetadataId,
@@ -169,6 +173,7 @@ describe('CommandMenuItem creation should succeed', () => {
expectToFail: false,
input: {
workflowVersionId,
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Global Command',
},
});
@@ -202,6 +207,7 @@ describe('CommandMenuItem creation should succeed', () => {
expectToFail: false,
input: {
frontComponentId: createdFrontComponentId,
engineComponentKey: EngineComponentKey.FRONT_COMPONENT_RENDERER,
label: 'Front Component Command',
},
});

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