diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/FrontComponentCommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/FrontComponentCommandMenuItem.tsx
new file mode 100644
index 0000000000..13fb5a0f2e
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/FrontComponentCommandMenuItem.tsx
@@ -0,0 +1,69 @@
+import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
+import { Command } from '@/command-menu-item/display/components/Command';
+import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
+import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
+import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
+import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useContext } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+export const FrontComponentCommandMenuItem = ({
+ frontComponentId,
+}: {
+ frontComponentId: string;
+}) => {
+ const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
+ const commandMenuItemConfig = useContext(CommandConfigContext);
+
+ 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 displayLabel =
+ typeof commandMenuItemConfig?.label === 'string'
+ ? commandMenuItemConfig.label
+ : '';
+
+ const Icon = commandMenuItemConfig?.Icon;
+
+ const handleClick = () => {
+ if (!isDefined(Icon)) {
+ return;
+ }
+
+ openFrontComponentInSidePanel({
+ frontComponentId,
+ pageTitle: displayLabel,
+ pageIcon: Icon,
+ recordContext:
+ isDefined(recordId) && isDefined(objectNameSingular)
+ ? { recordId, objectNameSingular }
+ : undefined,
+ });
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx
index c7f0fde91c..0e48277256 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx
@@ -1,19 +1,22 @@
+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,
- recordId,
- objectNameSingular,
}: {
frontComponentId: string;
commandMenuItemId: string;
- recordId?: string;
- objectNameSingular?: string;
}) => {
const mountHeadlessFrontComponent = useMountHeadlessFrontComponent();
@@ -22,6 +25,31 @@ export const HeadlessFrontComponentCommandMenuItem = ({
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,
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/WorkflowCommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/WorkflowCommandMenuItem.tsx
new file mode 100644
index 0000000000..4455816d53
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/WorkflowCommandMenuItem.tsx
@@ -0,0 +1,128 @@
+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
+ >({
+ 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 (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect.tsx
index 4ebe7f75bc..ae9d04840a 100644
--- a/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect.tsx
@@ -7,10 +7,12 @@ import { useEffect } from 'react';
export type HeadlessEngineCommandWrapperEffectProps = {
execute: () => void | Promise;
+ ready?: boolean;
};
export const HeadlessEngineCommandWrapperEffect = ({
execute,
+ ready = true,
}: HeadlessEngineCommandWrapperEffectProps) => {
const { isInitializedRef, setIsInitialized } =
useIsHeadlessEngineCommandEffectInitialized();
@@ -24,7 +26,7 @@ export const HeadlessEngineCommandWrapperEffect = ({
const { enqueueErrorSnackBar } = useSnackBar();
useEffect(() => {
- if (isInitializedRef.current) {
+ if (isInitializedRef.current || !ready) {
return;
}
@@ -39,6 +41,7 @@ export const HeadlessEngineCommandWrapperEffect = ({
run();
}, [
execute,
+ ready,
isInitializedRef,
setIsInitialized,
engineCommandId,
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand.tsx
index 9d4a65121c..8a7cbf9f13 100644
--- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand.tsx
@@ -28,5 +28,10 @@ export const ActivateWorkflowSingleRecordCommand = () => {
});
};
- return ;
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand.tsx
index b3c8072a0f..1bdbbf4f4f 100644
--- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand.tsx
@@ -27,5 +27,10 @@ export const DeactivateWorkflowSingleRecordCommand = () => {
});
};
- return ;
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand.tsx
index d94ebde5ac..aaeee32e7f 100644
--- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand.tsx
@@ -27,5 +27,10 @@ export const DiscardDraftWorkflowSingleRecordCommand = () => {
});
};
- return ;
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand.tsx
index 3b2de6bb74..dc0f3b6c4d 100644
--- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand.tsx
@@ -28,5 +28,10 @@ export const TestWorkflowSingleRecordCommand = () => {
});
};
- return ;
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
index 5c7c51f2ea..36cc6f43e5 100644
--- a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
@@ -1,217 +1,24 @@
-import { Command } from '@/command-menu-item/display/components/Command';
-import { EngineCommandMenuItem } from '@/command-menu-item/display/components/EngineCommandMenuItem';
-import { HeadlessFrontComponentCommandMenuItem } from '@/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem';
-import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
-import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
+import { useConvertBackendItemToCommandMenuItemConfig } from '@/command-menu-item/server-items/hooks/useConvertBackendItemToCommandMenuItemConfig';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
-import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
-import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
-
-import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
-import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
-
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { type CommandMenuContextApi } from 'twenty-shared/types';
-import {
- evaluateConditionalAvailabilityExpression,
- interpolateCommandMenuItemLabel,
- isDefined,
-} from 'twenty-shared/utils';
-import { type IconComponent, useIcons } from 'twenty-ui/display';
-
-import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
import { useQuery } from '@apollo/client/react';
+import { type CommandMenuContextApi } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
import {
- CommandMenuItemAvailabilityType,
type CommandMenuItemFieldsFragment,
- type EngineComponentKey,
FindManyCommandMenuItemsDocument,
} from '~/generated-metadata/graphql';
-type CommandMenuItemWithFrontComponent = CommandMenuItemFieldsFragment & {
- frontComponentId: string;
- conditionalAvailabilityExpression?: string | null;
-};
-
-type CommandMenuItemWithSource = CommandMenuItemFieldsFragment & {
- conditionalAvailabilityExpression?: string | null;
-};
-
-type BuildCommandMenuItemFromFrontComponentParams = {
- item: CommandMenuItemWithFrontComponent;
- type?: CommandMenuItemType;
- scope: CommandMenuItemScope;
- isPinned: boolean;
- getIcon: ReturnType['getIcon'];
- openFrontComponentInSidePanel: (params: {
- frontComponentId: string;
- pageTitle: string;
- pageIcon: IconComponent;
- recordContext?: {
- recordId: string;
- objectNameSingular: string;
- };
- }) => void;
- recordId?: string;
- objectNameSingular?: string;
- commandMenuContextApi: CommandMenuContextApi;
-};
-
-const buildCommandMenuItemFromFrontComponent = ({
- item,
- type = CommandMenuItemType.FrontComponent,
- scope,
- isPinned,
- getIcon,
- openFrontComponentInSidePanel,
- recordId,
- objectNameSingular,
- commandMenuContextApi,
-}: BuildCommandMenuItemFromFrontComponentParams) => {
- const displayLabel = interpolateCommandMenuItemLabel({
- label: item.label,
- context: commandMenuContextApi,
- });
-
- const displayShortLabel = interpolateCommandMenuItemLabel({
- label: item.shortLabel,
- context: commandMenuContextApi,
- });
-
- const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
-
- const isHeadless = item.frontComponent?.isHeadless === true;
-
- const handleNonHeadlessClick = () => {
- openFrontComponentInSidePanel({
- frontComponentId: item.frontComponentId,
- pageTitle: displayLabel ?? '',
- pageIcon: Icon,
- recordContext:
- isDefined(recordId) && isDefined(objectNameSingular)
- ? { recordId, objectNameSingular }
- : undefined,
- });
- };
-
- return {
- type,
- key: `command-menu-item-front-component-${item.id}`,
- scope,
- label: displayLabel,
- shortLabel: displayShortLabel,
- position: item.position,
- isPinned,
- Icon,
- hotKeys: item.hotKeys,
- shouldBeRegistered: () =>
- evaluateConditionalAvailabilityExpression(
- item.conditionalAvailabilityExpression,
- commandMenuContextApi,
- ),
- component: isHeadless ? (
-
- ) : (
-
- ),
- };
-};
-
-type BuildCommandMenuItemFromStandardKeyParams = {
- item: CommandMenuItemWithSource;
- engineComponentKey: EngineComponentKey;
- type?: CommandMenuItemType;
- scope: CommandMenuItemScope;
- isPinned: boolean;
- getIcon: ReturnType['getIcon'];
- commandMenuContextApi: CommandMenuContextApi;
-};
-
-const buildCommandItemFromEngineKey = ({
- item,
- engineComponentKey,
- type = CommandMenuItemType.Standard,
- scope,
- isPinned,
- getIcon,
- commandMenuContextApi,
-}: BuildCommandMenuItemFromStandardKeyParams) => {
- const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
-
- return {
- type,
- key: `command-menu-item-engine-${item.id}`,
- scope,
- label: interpolateCommandMenuItemLabel({
- label: item.label,
- context: commandMenuContextApi,
- }),
- shortLabel: interpolateCommandMenuItemLabel({
- label: item.shortLabel,
- context: commandMenuContextApi,
- }),
- position: item.position,
- isPinned,
- Icon,
- hotKeys: item.hotKeys,
- shouldBeRegistered: () =>
- evaluateConditionalAvailabilityExpression(
- item.conditionalAvailabilityExpression,
- commandMenuContextApi,
- ),
- component: (
-
- ),
- };
-};
-
export const useCommandMenuItemsFromBackend = (
commandMenuContextApi: CommandMenuContextApi,
) => {
- const { getIcon } = useIcons();
- const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
-
- const contextStoreIsPageInEditMode = useAtomComponentStateValue(
- contextStoreIsPageInEditModeComponentState,
- );
+ const { convertBackendItemToCommandMenuItemConfig } =
+ useConvertBackendItemToCommandMenuItemConfig();
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
- const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
- contextStoreTargetedRecordsRuleComponentState,
- );
-
- const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
-
- const currentObjectMetadataItem = objectMetadataItems.find(
- (item) => item.id === contextStoreCurrentObjectMetadataItemId,
- );
-
- const selectedRecordIds =
- contextStoreTargetedRecordsRule.mode === 'selection'
- ? contextStoreTargetedRecordsRule.selectedRecordIds
- : [];
-
- const hasRecordSelection =
- selectedRecordIds.length >= 1 ||
- contextStoreTargetedRecordsRule.mode === 'exclusion';
-
- const recordId =
- selectedRecordIds.length === 1 ? selectedRecordIds[0] : undefined;
-
- const objectNameSingular = currentObjectMetadataItem?.nameSingular;
-
const { data } = useQuery(FindManyCommandMenuItemsDocument);
const allItems = data?.commandMenuItems ?? [];
@@ -221,101 +28,12 @@ export const useCommandMenuItemsFromBackend = (
item.availabilityObjectMetadataId ===
contextStoreCurrentObjectMetadataItemId;
- const itemsWithObjectMatches = allItems.filter(objectMatches);
-
- const buildCommandMenuItem = ({
- item,
- scope,
- isPinned,
- typeOverride,
- }: {
- item: CommandMenuItemFieldsFragment;
- scope: CommandMenuItemScope;
- isPinned: boolean;
- typeOverride?: CommandMenuItemType;
- }) => {
- if (isDefined(item.engineComponentKey)) {
- return buildCommandItemFromEngineKey({
- item,
- engineComponentKey: item.engineComponentKey,
- type: typeOverride,
- scope,
- isPinned,
- getIcon,
- commandMenuContextApi,
- });
- }
-
- if (isDefined(item.frontComponentId)) {
- return buildCommandMenuItemFromFrontComponent({
- item: item as CommandMenuItemWithFrontComponent,
- type: typeOverride,
- scope,
- isPinned,
- getIcon,
- openFrontComponentInSidePanel,
- commandMenuContextApi,
- recordId,
- objectNameSingular,
- });
- }
-
- return null;
- };
-
- const globalItems = itemsWithObjectMatches.filter(
- (item) => item.availabilityType === CommandMenuItemAvailabilityType.GLOBAL,
- );
-
- const recordScopedItems = itemsWithObjectMatches.filter(
- (item) =>
- item.availabilityType ===
- CommandMenuItemAvailabilityType.RECORD_SELECTION,
- );
-
- const fallbackItems = itemsWithObjectMatches.filter(
- (item) =>
- item.availabilityType === CommandMenuItemAvailabilityType.FALLBACK,
- );
-
- const globalCommandMenuItems = globalItems
+ return allItems
+ .filter(objectMatches)
.map((item) =>
- buildCommandMenuItem({
- item,
- scope: CommandMenuItemScope.Global,
- isPinned: !contextStoreIsPageInEditMode && item.isPinned,
- }),
+ convertBackendItemToCommandMenuItemConfig(item, commandMenuContextApi),
)
- .filter(isDefined);
-
- const recordScopedCommandMenuItems = hasRecordSelection
- ? recordScopedItems
- .map((item) =>
- buildCommandMenuItem({
- item,
- scope: CommandMenuItemScope.RecordSelection,
- isPinned: !contextStoreIsPageInEditMode && item.isPinned,
- }),
- )
- .filter(isDefined)
- : [];
-
- const fallbackCommandMenuItems = fallbackItems
- .map((item) =>
- buildCommandMenuItem({
- item,
- scope: CommandMenuItemScope.Global,
- isPinned: false,
- typeOverride: CommandMenuItemType.Fallback,
- }),
- )
- .filter(isDefined);
-
- return [
- ...globalCommandMenuItems,
- ...recordScopedCommandMenuItems,
- ...fallbackCommandMenuItems,
- ]
+ .filter(isDefined)
.filter((item) => item.shouldBeRegistered())
.sort((a, b) => a.position - b.position);
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useConvertBackendItemToCommandMenuItemConfig.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useConvertBackendItemToCommandMenuItemConfig.tsx
new file mode 100644
index 0000000000..8e3e28d43c
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useConvertBackendItemToCommandMenuItemConfig.tsx
@@ -0,0 +1,154 @@
+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 { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
+import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
+import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
+import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
+import { useCallback } from 'react';
+import { type CommandMenuContextApi } from 'twenty-shared/types';
+import {
+ evaluateConditionalAvailabilityExpression,
+ interpolateCommandMenuItemLabel,
+ isDefined,
+} from 'twenty-shared/utils';
+import { useIcons } from 'twenty-ui/display';
+import {
+ CommandMenuItemAvailabilityType,
+ type CommandMenuItemFieldsFragment,
+} from '~/generated-metadata/graphql';
+
+const resolveScope = (
+ availabilityType: CommandMenuItemAvailabilityType,
+): CommandMenuItemScope => {
+ if (availabilityType === CommandMenuItemAvailabilityType.RECORD_SELECTION) {
+ return CommandMenuItemScope.RecordSelection;
+ }
+ return CommandMenuItemScope.Global;
+};
+
+const resolveType = (
+ item: CommandMenuItemFieldsFragment,
+): CommandMenuItemType => {
+ if (item.availabilityType === CommandMenuItemAvailabilityType.FALLBACK) {
+ return CommandMenuItemType.Fallback;
+ }
+ if (isDefined(item.engineComponentKey)) {
+ 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
+// the new types to build command menu items.
+export const useConvertBackendItemToCommandMenuItemConfig = () => {
+ const { getIcon } = useIcons();
+
+ const contextStoreIsPageInEditMode = useAtomComponentStateValue(
+ contextStoreIsPageInEditModeComponentState,
+ );
+
+ const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
+ contextStoreTargetedRecordsRuleComponentState,
+ );
+
+ const selectedRecordIds =
+ contextStoreTargetedRecordsRule.mode === 'selection'
+ ? contextStoreTargetedRecordsRule.selectedRecordIds
+ : [];
+
+ const hasRecordSelection =
+ selectedRecordIds.length >= 1 ||
+ contextStoreTargetedRecordsRule.mode === 'exclusion';
+
+ const convertBackendItemToCommandMenuItemConfig = useCallback(
+ (
+ item: CommandMenuItemFieldsFragment,
+ commandMenuContextApi: CommandMenuContextApi,
+ ) => {
+ const scope = resolveScope(item.availabilityType);
+
+ if (
+ scope === CommandMenuItemScope.RecordSelection &&
+ !hasRecordSelection
+ ) {
+ return null;
+ }
+
+ const isPinned =
+ item.availabilityType !== CommandMenuItemAvailabilityType.FALLBACK &&
+ !contextStoreIsPageInEditMode &&
+ item.isPinned;
+
+ const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
+
+ const label = interpolateCommandMenuItemLabel({
+ label: item.label,
+ context: commandMenuContextApi,
+ });
+
+ const shortLabel = interpolateCommandMenuItemLabel({
+ label: item.shortLabel,
+ context: commandMenuContextApi,
+ });
+
+ const component = isDefined(item.engineComponentKey) ? (
+
+ ) : isDefined(item.frontComponentId) ? (
+ item.frontComponent?.isHeadless === true ? (
+
+ ) : (
+
+ )
+ ) : isDefined(item.workflowVersionId) ? (
+
+ ) : null;
+
+ if (!isDefined(component)) {
+ return;
+ }
+
+ return {
+ type: resolveType(item),
+ key: `command-menu-item-${item.id}`,
+ scope,
+ label,
+ shortLabel,
+ position: item.position,
+ isPinned,
+ Icon,
+ hotKeys: item.hotKeys,
+ shouldBeRegistered: () =>
+ evaluateConditionalAvailabilityExpression(
+ item.conditionalAvailabilityExpression,
+ commandMenuContextApi,
+ ),
+ component,
+ };
+ },
+ [getIcon, contextStoreIsPageInEditMode, hasRecordSelection],
+ );
+
+ return { convertBackendItemToCommandMenuItemConfig };
+};
diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts
index 859ab2c677..1ddbabdc9d 100644
--- a/packages/twenty-front/src/testing/graphqlMocks.ts
+++ b/packages/twenty-front/src/testing/graphqlMocks.ts
@@ -207,6 +207,11 @@ export const graphqlMocks = {
data: { navigationMenuItems: mockedNavigationMenuItems },
});
}),
+ metadataGraphql.query('FindManyCommandMenuItems', () => {
+ return HttpResponse.json({
+ data: { commandMenuItems: [] },
+ });
+ }),
graphql.query('SearchPeople', () => {
return HttpResponse.json({
data: {
diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql
index 5b9c119fff..d7b0110001 100644
--- a/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql
+++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.graphql
@@ -3099,6 +3099,10 @@ type Query {
findManyLogicFunctions: [LogicFunction!]!
getAvailablePackages(input: LogicFunctionIdInput!): JSON!
getLogicFunctionSourceCode(input: LogicFunctionIdInput!): String
+ commandMenuItems: [CommandMenuItem!]!
+ commandMenuItem(id: UUID!): CommandMenuItem
+ frontComponents: [FrontComponent!]!
+ frontComponent(id: UUID!): FrontComponent
objectRecordCounts: [ObjectRecordCount!]!
object(
"""The id of the record to find."""
@@ -3130,10 +3134,6 @@ type Query {
"""Specify to filter the records returned."""
filter: IndexFilter! = {}
): IndexConnection!
- commandMenuItems: [CommandMenuItem!]!
- commandMenuItem(id: UUID!): CommandMenuItem
- frontComponents: [FrontComponent!]!
- frontComponent(id: UUID!): FrontComponent
findManyAgents: [Agent!]!
findOneAgent(input: AgentIdInput!): Agent!
billingPortalSession(returnUrlPath: String): BillingSession!
@@ -3366,6 +3366,12 @@ type Mutation {
createOneLogicFunction(input: CreateLogicFunctionFromSourceInput!): LogicFunction!
executeOneLogicFunction(input: ExecuteOneLogicFunctionInput!): LogicFunctionExecutionResult!
updateOneLogicFunction(input: UpdateLogicFunctionFromSourceInput!): Boolean!
+ createCommandMenuItem(input: CreateCommandMenuItemInput!): CommandMenuItem!
+ updateCommandMenuItem(input: UpdateCommandMenuItemInput!): CommandMenuItem!
+ deleteCommandMenuItem(id: UUID!): CommandMenuItem!
+ createFrontComponent(input: CreateFrontComponentInput!): FrontComponent!
+ updateFrontComponent(input: UpdateFrontComponentInput!): FrontComponent!
+ deleteFrontComponent(id: UUID!): FrontComponent!
createOneObject(input: CreateOneObjectInput!): Object!
deleteOneObject(input: DeleteOneObjectInput!): Object!
updateOneObject(input: UpdateOneObjectInput!): Object!
@@ -3388,12 +3394,6 @@ type Mutation {
deleteViewFieldGroup(input: DeleteViewFieldGroupInput!): ViewFieldGroup!
destroyViewFieldGroup(input: DestroyViewFieldGroupInput!): ViewFieldGroup!
upsertFieldsWidget(input: UpsertFieldsWidgetInput!): View!
- createCommandMenuItem(input: CreateCommandMenuItemInput!): CommandMenuItem!
- updateCommandMenuItem(input: UpdateCommandMenuItemInput!): CommandMenuItem!
- deleteCommandMenuItem(id: UUID!): CommandMenuItem!
- createFrontComponent(input: CreateFrontComponentInput!): FrontComponent!
- updateFrontComponent(input: UpdateFrontComponentInput!): FrontComponent!
- deleteFrontComponent(id: UUID!): FrontComponent!
createOneAgent(input: CreateAgentInput!): Agent!
updateOneAgent(input: UpdateAgentInput!): Agent!
deleteOneAgent(input: AgentIdInput!): Agent!
@@ -3719,6 +3719,57 @@ input UpdateLogicFunctionFromSourceInputUpdates {
httpRouteTriggerSettings: JSON
}
+input CreateCommandMenuItemInput {
+ workflowVersionId: UUID
+ frontComponentId: UUID
+ engineComponentKey: EngineComponentKey
+ label: String!
+ icon: String
+ shortLabel: String
+ position: Float
+ isPinned: Boolean
+ availabilityType: CommandMenuItemAvailabilityType
+ hotKeys: [String!]
+ conditionalAvailabilityExpression: String
+ availabilityObjectMetadataId: UUID
+}
+
+input UpdateCommandMenuItemInput {
+ id: UUID!
+ label: String
+ icon: String
+ shortLabel: String
+ position: Float
+ isPinned: Boolean
+ availabilityType: CommandMenuItemAvailabilityType
+ availabilityObjectMetadataId: UUID
+ engineComponentKey: EngineComponentKey
+ hotKeys: [String!]
+}
+
+input CreateFrontComponentInput {
+ id: UUID
+ name: String!
+ description: String
+ sourceComponentPath: String!
+ builtComponentPath: String!
+ componentName: String!
+ builtComponentChecksum: String!
+}
+
+input UpdateFrontComponentInput {
+ """The id of the front component to update"""
+ id: UUID!
+
+ """The front component fields to update"""
+ update: UpdateFrontComponentInputUpdates!
+}
+
+input UpdateFrontComponentInputUpdates {
+ name: String
+ description: String
+}
+
input CreateOneObjectInput {
"""The object to create"""
object: CreateObjectInput!
@@ -3934,57 +3985,6 @@ input UpsertFieldsWidgetFieldInput {
position: Float!
}
-input CreateCommandMenuItemInput {
- workflowVersionId: UUID
- frontComponentId: UUID
- engineComponentKey: EngineComponentKey
- label: String!
- icon: String
- shortLabel: String
- position: Float
- isPinned: Boolean
- availabilityType: CommandMenuItemAvailabilityType
- hotKeys: [String!]
- conditionalAvailabilityExpression: String
- availabilityObjectMetadataId: UUID
-}
-
-input UpdateCommandMenuItemInput {
- id: UUID!
- label: String
- icon: String
- shortLabel: String
- position: Float
- isPinned: Boolean
- availabilityType: CommandMenuItemAvailabilityType
- availabilityObjectMetadataId: UUID
- engineComponentKey: EngineComponentKey
- hotKeys: [String!]
-}
-
-input CreateFrontComponentInput {
- id: UUID
- name: String!
- description: String
- sourceComponentPath: String!
- builtComponentPath: String!
- componentName: String!
- builtComponentChecksum: String!
-}
-
-input UpdateFrontComponentInput {
- """The id of the front component to update"""
- id: UUID!
-
- """The front component fields to update"""
- update: UpdateFrontComponentInputUpdates!
-}
-
-input UpdateFrontComponentInputUpdates {
- name: String
- description: String
-}
-
input CreateAgentInput {
name: String
label: String!
diff --git a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts
index aa334d4ccb..00b5186d3d 100644
--- a/packages/twenty-sdk/src/clients/generated/metadata/schema.ts
+++ b/packages/twenty-sdk/src/clients/generated/metadata/schema.ts
@@ -2692,6 +2692,10 @@ export interface Query {
findManyLogicFunctions: LogicFunction[]
getAvailablePackages: Scalars['JSON']
getLogicFunctionSourceCode?: Scalars['String']
+ commandMenuItems: CommandMenuItem[]
+ commandMenuItem?: CommandMenuItem
+ frontComponents: FrontComponent[]
+ frontComponent?: FrontComponent
objectRecordCounts: ObjectRecordCount[]
object: Object
objects: ObjectConnection
@@ -2705,10 +2709,6 @@ export interface Query {
getViewFieldGroup?: ViewFieldGroup
index: Index
indexMetadatas: IndexConnection
- commandMenuItems: CommandMenuItem[]
- commandMenuItem?: CommandMenuItem
- frontComponents: FrontComponent[]
- frontComponent?: FrontComponent
findManyAgents: Agent[]
findOneAgent: Agent
billingPortalSession: BillingSession
@@ -2831,6 +2831,12 @@ export interface Mutation {
createOneLogicFunction: LogicFunction
executeOneLogicFunction: LogicFunctionExecutionResult
updateOneLogicFunction: Scalars['Boolean']
+ createCommandMenuItem: CommandMenuItem
+ updateCommandMenuItem: CommandMenuItem
+ deleteCommandMenuItem: CommandMenuItem
+ createFrontComponent: FrontComponent
+ updateFrontComponent: FrontComponent
+ deleteFrontComponent: FrontComponent
createOneObject: Object
deleteOneObject: Object
updateOneObject: Object
@@ -2853,12 +2859,6 @@ export interface Mutation {
deleteViewFieldGroup: ViewFieldGroup
destroyViewFieldGroup: ViewFieldGroup
upsertFieldsWidget: View
- createCommandMenuItem: CommandMenuItem
- updateCommandMenuItem: CommandMenuItem
- deleteCommandMenuItem: CommandMenuItem
- createFrontComponent: FrontComponent
- updateFrontComponent: FrontComponent
- deleteFrontComponent: FrontComponent
createOneAgent: Agent
updateOneAgent: Agent
deleteOneAgent: Agent
@@ -5852,6 +5852,10 @@ export interface QueryGenqlSelection{
findManyLogicFunctions?: LogicFunctionGenqlSelection
getAvailablePackages?: { __args: {input: LogicFunctionIdInput} }
getLogicFunctionSourceCode?: { __args: {input: LogicFunctionIdInput} }
+ commandMenuItems?: CommandMenuItemGenqlSelection
+ commandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
+ frontComponents?: FrontComponentGenqlSelection
+ frontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} })
objectRecordCounts?: ObjectRecordCountGenqlSelection
object?: (ObjectGenqlSelection & { __args: {
/** The id of the record to find. */
@@ -5877,10 +5881,6 @@ export interface QueryGenqlSelection{
paging: CursorPaging,
/** Specify to filter the records returned. */
filter: IndexFilter} })
- commandMenuItems?: CommandMenuItemGenqlSelection
- commandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
- frontComponents?: FrontComponentGenqlSelection
- frontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} })
findManyAgents?: AgentGenqlSelection
findOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
billingPortalSession?: (BillingSessionGenqlSelection & { __args?: {returnUrlPath?: (Scalars['String'] | null)} })
@@ -6034,6 +6034,12 @@ export interface MutationGenqlSelection{
createOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: CreateLogicFunctionFromSourceInput} })
executeOneLogicFunction?: (LogicFunctionExecutionResultGenqlSelection & { __args: {input: ExecuteOneLogicFunctionInput} })
updateOneLogicFunction?: { __args: {input: UpdateLogicFunctionFromSourceInput} }
+ createCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {input: CreateCommandMenuItemInput} })
+ updateCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {input: UpdateCommandMenuItemInput} })
+ deleteCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
+ createFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: CreateFrontComponentInput} })
+ updateFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: UpdateFrontComponentInput} })
+ deleteFrontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} })
createOneObject?: (ObjectGenqlSelection & { __args: {input: CreateOneObjectInput} })
deleteOneObject?: (ObjectGenqlSelection & { __args: {input: DeleteOneObjectInput} })
updateOneObject?: (ObjectGenqlSelection & { __args: {input: UpdateOneObjectInput} })
@@ -6056,12 +6062,6 @@ export interface MutationGenqlSelection{
deleteViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: DeleteViewFieldGroupInput} })
destroyViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: DestroyViewFieldGroupInput} })
upsertFieldsWidget?: (ViewGenqlSelection & { __args: {input: UpsertFieldsWidgetInput} })
- createCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {input: CreateCommandMenuItemInput} })
- updateCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {input: UpdateCommandMenuItemInput} })
- deleteCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
- createFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: CreateFrontComponentInput} })
- updateFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: UpdateFrontComponentInput} })
- deleteFrontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} })
createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} })
updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} })
deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
@@ -6263,6 +6263,20 @@ 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 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)}
+
+export interface CreateFrontComponentInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),sourceComponentPath: Scalars['String'],builtComponentPath: Scalars['String'],componentName: Scalars['String'],builtComponentChecksum: Scalars['String']}
+
+export interface UpdateFrontComponentInput {
+/** The id of the front component to update */
+id: Scalars['UUID'],
+/** The front component fields to update */
+update: UpdateFrontComponentInputUpdates}
+
+export interface UpdateFrontComponentInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null)}
+
export interface CreateOneObjectInput {
/** The object to create */
object: CreateObjectInput}
@@ -6351,20 +6365,6 @@ export interface UpsertFieldsWidgetFieldInput {
/** The id of the view field */
viewFieldId: Scalars['UUID'],isVisible: Scalars['Boolean'],position: Scalars['Float']}
-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 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)}
-
-export interface CreateFrontComponentInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),sourceComponentPath: Scalars['String'],builtComponentPath: Scalars['String'],componentName: Scalars['String'],builtComponentChecksum: Scalars['String']}
-
-export interface UpdateFrontComponentInput {
-/** The id of the front component to update */
-id: Scalars['UUID'],
-/** The front component fields to update */
-update: UpdateFrontComponentInputUpdates}
-
-export interface UpdateFrontComponentInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null)}
-
export interface CreateAgentInput {name?: (Scalars['String'] | null),label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt: Scalars['String'],modelId: Scalars['String'],roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
export interface UpdateAgentInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt?: (Scalars['String'] | null),modelId?: (Scalars['String'] | null),roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
diff --git a/packages/twenty-sdk/src/clients/generated/metadata/types.ts b/packages/twenty-sdk/src/clients/generated/metadata/types.ts
index e61b9346c0..65733312a3 100644
--- a/packages/twenty-sdk/src/clients/generated/metadata/types.ts
+++ b/packages/twenty-sdk/src/clients/generated/metadata/types.ts
@@ -6135,6 +6135,30 @@ export default {
]
}
],
+ "commandMenuItems": [
+ 224
+ ],
+ "commandMenuItem": [
+ 224,
+ {
+ "id": [
+ 3,
+ "UUID!"
+ ]
+ }
+ ],
+ "frontComponents": [
+ 223
+ ],
+ "frontComponent": [
+ 223,
+ {
+ "id": [
+ 3,
+ "UUID!"
+ ]
+ }
+ ],
"objectRecordCounts": [
167
],
@@ -6256,30 +6280,6 @@ export default {
]
}
],
- "commandMenuItems": [
- 224
- ],
- "commandMenuItem": [
- 224,
- {
- "id": [
- 3,
- "UUID!"
- ]
- }
- ],
- "frontComponents": [
- 223
- ],
- "frontComponent": [
- 223,
- {
- "id": [
- 3,
- "UUID!"
- ]
- }
- ],
"findManyAgents": [
25
],
@@ -7324,11 +7324,65 @@ export default {
]
}
],
+ "createCommandMenuItem": [
+ 224,
+ {
+ "input": [
+ 381,
+ "CreateCommandMenuItemInput!"
+ ]
+ }
+ ],
+ "updateCommandMenuItem": [
+ 224,
+ {
+ "input": [
+ 382,
+ "UpdateCommandMenuItemInput!"
+ ]
+ }
+ ],
+ "deleteCommandMenuItem": [
+ 224,
+ {
+ "id": [
+ 3,
+ "UUID!"
+ ]
+ }
+ ],
+ "createFrontComponent": [
+ 223,
+ {
+ "input": [
+ 383,
+ "CreateFrontComponentInput!"
+ ]
+ }
+ ],
+ "updateFrontComponent": [
+ 223,
+ {
+ "input": [
+ 384,
+ "UpdateFrontComponentInput!"
+ ]
+ }
+ ],
+ "deleteFrontComponent": [
+ 223,
+ {
+ "id": [
+ 3,
+ "UUID!"
+ ]
+ }
+ ],
"createOneObject": [
46,
{
"input": [
- 381,
+ 386,
"CreateOneObjectInput!"
]
}
@@ -7337,7 +7391,7 @@ export default {
46,
{
"input": [
- 383,
+ 388,
"DeleteOneObjectInput!"
]
}
@@ -7346,7 +7400,7 @@ export default {
46,
{
"input": [
- 384,
+ 389,
"UpdateOneObjectInput!"
]
}
@@ -7355,7 +7409,7 @@ export default {
50,
{
"input": [
- 386,
+ 391,
"UpdateViewFieldInput!"
]
}
@@ -7364,7 +7418,7 @@ export default {
50,
{
"input": [
- 388,
+ 393,
"CreateViewFieldInput!"
]
}
@@ -7373,7 +7427,7 @@ export default {
50,
{
"inputs": [
- 388,
+ 393,
"[CreateViewFieldInput!]!"
]
}
@@ -7382,7 +7436,7 @@ export default {
50,
{
"input": [
- 389,
+ 394,
"DeleteViewFieldInput!"
]
}
@@ -7391,7 +7445,7 @@ export default {
50,
{
"input": [
- 390,
+ 395,
"DestroyViewFieldInput!"
]
}
@@ -7400,7 +7454,7 @@ export default {
60,
{
"input": [
- 391,
+ 396,
"CreateViewInput!"
]
}
@@ -7413,7 +7467,7 @@ export default {
"String!"
],
"input": [
- 392,
+ 397,
"UpdateViewInput!"
]
}
@@ -7440,7 +7494,7 @@ export default {
57,
{
"input": [
- 393,
+ 398,
"CreateViewSortInput!"
]
}
@@ -7449,7 +7503,7 @@ export default {
57,
{
"input": [
- 394,
+ 399,
"UpdateViewSortInput!"
]
}
@@ -7458,7 +7512,7 @@ export default {
6,
{
"input": [
- 396,
+ 401,
"DeleteViewSortInput!"
]
}
@@ -7467,7 +7521,7 @@ export default {
6,
{
"input": [
- 397,
+ 402,
"DestroyViewSortInput!"
]
}
@@ -7476,7 +7530,7 @@ export default {
59,
{
"input": [
- 398,
+ 403,
"UpdateViewFieldGroupInput!"
]
}
@@ -7485,7 +7539,7 @@ export default {
59,
{
"input": [
- 400,
+ 405,
"CreateViewFieldGroupInput!"
]
}
@@ -7494,7 +7548,7 @@ export default {
59,
{
"inputs": [
- 400,
+ 405,
"[CreateViewFieldGroupInput!]!"
]
}
@@ -7503,7 +7557,7 @@ export default {
59,
{
"input": [
- 401,
+ 406,
"DeleteViewFieldGroupInput!"
]
}
@@ -7512,71 +7566,17 @@ export default {
59,
{
"input": [
- 402,
+ 407,
"DestroyViewFieldGroupInput!"
]
}
],
"upsertFieldsWidget": [
60,
- {
- "input": [
- 403,
- "UpsertFieldsWidgetInput!"
- ]
- }
- ],
- "createCommandMenuItem": [
- 224,
- {
- "input": [
- 406,
- "CreateCommandMenuItemInput!"
- ]
- }
- ],
- "updateCommandMenuItem": [
- 224,
- {
- "input": [
- 407,
- "UpdateCommandMenuItemInput!"
- ]
- }
- ],
- "deleteCommandMenuItem": [
- 224,
- {
- "id": [
- 3,
- "UUID!"
- ]
- }
- ],
- "createFrontComponent": [
- 223,
{
"input": [
408,
- "CreateFrontComponentInput!"
- ]
- }
- ],
- "updateFrontComponent": [
- 223,
- {
- "input": [
- 409,
- "UpdateFrontComponentInput!"
- ]
- }
- ],
- "deleteFrontComponent": [
- 223,
- {
- "id": [
- 3,
- "UUID!"
+ "UpsertFieldsWidgetInput!"
]
}
],
@@ -9477,9 +9477,133 @@ export default {
1
]
},
+ "CreateCommandMenuItemInput": {
+ "workflowVersionId": [
+ 3
+ ],
+ "frontComponentId": [
+ 3
+ ],
+ "engineComponentKey": [
+ 225
+ ],
+ "label": [
+ 1
+ ],
+ "icon": [
+ 1
+ ],
+ "shortLabel": [
+ 1
+ ],
+ "position": [
+ 11
+ ],
+ "isPinned": [
+ 6
+ ],
+ "availabilityType": [
+ 226
+ ],
+ "hotKeys": [
+ 1
+ ],
+ "conditionalAvailabilityExpression": [
+ 1
+ ],
+ "availabilityObjectMetadataId": [
+ 3
+ ],
+ "__typename": [
+ 1
+ ]
+ },
+ "UpdateCommandMenuItemInput": {
+ "id": [
+ 3
+ ],
+ "label": [
+ 1
+ ],
+ "icon": [
+ 1
+ ],
+ "shortLabel": [
+ 1
+ ],
+ "position": [
+ 11
+ ],
+ "isPinned": [
+ 6
+ ],
+ "availabilityType": [
+ 226
+ ],
+ "availabilityObjectMetadataId": [
+ 3
+ ],
+ "engineComponentKey": [
+ 225
+ ],
+ "hotKeys": [
+ 1
+ ],
+ "__typename": [
+ 1
+ ]
+ },
+ "CreateFrontComponentInput": {
+ "id": [
+ 3
+ ],
+ "name": [
+ 1
+ ],
+ "description": [
+ 1
+ ],
+ "sourceComponentPath": [
+ 1
+ ],
+ "builtComponentPath": [
+ 1
+ ],
+ "componentName": [
+ 1
+ ],
+ "builtComponentChecksum": [
+ 1
+ ],
+ "__typename": [
+ 1
+ ]
+ },
+ "UpdateFrontComponentInput": {
+ "id": [
+ 3
+ ],
+ "update": [
+ 385
+ ],
+ "__typename": [
+ 1
+ ]
+ },
+ "UpdateFrontComponentInputUpdates": {
+ "name": [
+ 1
+ ],
+ "description": [
+ 1
+ ],
+ "__typename": [
+ 1
+ ]
+ },
"CreateOneObjectInput": {
"object": [
- 382
+ 387
],
"__typename": [
1
@@ -9539,7 +9663,7 @@ export default {
},
"UpdateOneObjectInput": {
"update": [
- 385
+ 390
],
"id": [
3
@@ -9594,7 +9718,7 @@ export default {
3
],
"update": [
- 387
+ 392
],
"__typename": [
1
@@ -9793,7 +9917,7 @@ export default {
3
],
"update": [
- 395
+ 400
],
"__typename": [
1
@@ -9828,7 +9952,7 @@ export default {
3
],
"update": [
- 399
+ 404
],
"__typename": [
1
@@ -9892,10 +10016,10 @@ export default {
3
],
"groups": [
- 404
+ 409
],
"fields": [
- 405
+ 410
],
"__typename": [
1
@@ -9915,7 +10039,7 @@ export default {
6
],
"fields": [
- 405
+ 410
],
"__typename": [
1
@@ -9935,130 +10059,6 @@ export default {
1
]
},
- "CreateCommandMenuItemInput": {
- "workflowVersionId": [
- 3
- ],
- "frontComponentId": [
- 3
- ],
- "engineComponentKey": [
- 225
- ],
- "label": [
- 1
- ],
- "icon": [
- 1
- ],
- "shortLabel": [
- 1
- ],
- "position": [
- 11
- ],
- "isPinned": [
- 6
- ],
- "availabilityType": [
- 226
- ],
- "hotKeys": [
- 1
- ],
- "conditionalAvailabilityExpression": [
- 1
- ],
- "availabilityObjectMetadataId": [
- 3
- ],
- "__typename": [
- 1
- ]
- },
- "UpdateCommandMenuItemInput": {
- "id": [
- 3
- ],
- "label": [
- 1
- ],
- "icon": [
- 1
- ],
- "shortLabel": [
- 1
- ],
- "position": [
- 11
- ],
- "isPinned": [
- 6
- ],
- "availabilityType": [
- 226
- ],
- "availabilityObjectMetadataId": [
- 3
- ],
- "engineComponentKey": [
- 225
- ],
- "hotKeys": [
- 1
- ],
- "__typename": [
- 1
- ]
- },
- "CreateFrontComponentInput": {
- "id": [
- 3
- ],
- "name": [
- 1
- ],
- "description": [
- 1
- ],
- "sourceComponentPath": [
- 1
- ],
- "builtComponentPath": [
- 1
- ],
- "componentName": [
- 1
- ],
- "builtComponentChecksum": [
- 1
- ],
- "__typename": [
- 1
- ]
- },
- "UpdateFrontComponentInput": {
- "id": [
- 3
- ],
- "update": [
- 410
- ],
- "__typename": [
- 1
- ]
- },
- "UpdateFrontComponentInputUpdates": {
- "name": [
- 1
- ],
- "description": [
- 1
- ],
- "__typename": [
- 1
- ]
- },
"CreateAgentInput": {
"name": [
1
diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts
index 546909536c..1f4093c966 100644
--- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts
@@ -57,6 +57,7 @@ import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-dashboards';
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-opportunities';
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
+import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-migration/constant/default-feature-flags';
@@ -698,6 +699,8 @@ export class WorkspaceService extends TypeOrmQueryService {
flatFieldMetadataMaps,
);
+ await prefillWorkflowCommandMenuItems(queryRunner.manager, workspaceId);
+
await prefillOpportunities(queryRunner.manager, schemaName);
await prefillDashboards(
diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts
index 05eb27d8d5..fe9b24db36 100644
--- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
+import { type QueryRunner } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import {
@@ -87,6 +88,7 @@ export class CommandMenuItemService {
async create(
input: CreateCommandMenuItemInput,
workspaceId: string,
+ queryRunner?: QueryRunner,
): Promise {
const { flatObjectMetadataMaps, flatFrontComponentMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
@@ -124,6 +126,7 @@ export class CommandMenuItemService {
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
+ queryRunner,
},
);
@@ -153,6 +156,7 @@ export class CommandMenuItemService {
async update(
input: UpdateCommandMenuItemInput,
workspaceId: string,
+ queryRunner?: QueryRunner,
): Promise {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
@@ -191,6 +195,7 @@ export class CommandMenuItemService {
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
+ queryRunner,
},
);
@@ -217,7 +222,11 @@ export class CommandMenuItemService {
);
}
- async delete(id: string, workspaceId: string): Promise {
+ async delete(
+ id: string,
+ workspaceId: string,
+ queryRunner?: QueryRunner,
+ ): Promise {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
@@ -251,6 +260,7 @@ export class CommandMenuItemService {
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
+ queryRunner,
},
);
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
index 38e1da9d1f..7750aa7f5d 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
@@ -93,7 +93,7 @@ export const seedFeatureFlags = async ({
{
key: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
workspaceId: workspaceId,
- value: false,
+ value: true,
},
{
key: FeatureFlagKey.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED,
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts
index 24687b2235..87582b7a29 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts
@@ -121,6 +121,7 @@ import {
WORKSPACE_MEMBER_DATA_SEED_COLUMNS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
+import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
import { prefillWorkflows } 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';
@@ -362,6 +363,8 @@ export class DevSeederDataService {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
+
+ await prefillWorkflowCommandMenuItems(entityManager, workspaceId);
},
);
}
diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items.ts
new file mode 100644
index 0000000000..efebd1de9a
--- /dev/null
+++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items.ts
@@ -0,0 +1,69 @@
+import { type EntityManager } from 'typeorm';
+
+import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.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';
+
+const QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =
+ 'a1b2c3d4-e5f6-7890-abcd-1234567890ab';
+
+export const prefillWorkflowCommandMenuItems = async (
+ entityManager: EntityManager,
+ workspaceId: string,
+) => {
+ const applicationRow = await entityManager
+ .createQueryBuilder()
+ .select('app.id')
+ .from('core.application', 'app')
+ .where('app.universalIdentifier = :universalIdentifier', {
+ universalIdentifier: TWENTY_STANDARD_APPLICATION.universalIdentifier,
+ })
+ .andWhere('app.workspaceId = :workspaceId', { workspaceId })
+ .getRawOne();
+
+ if (!applicationRow) {
+ return;
+ }
+
+ await entityManager
+ .createQueryBuilder()
+ .insert()
+ .into('core.commandMenuItem', [
+ 'workspaceId',
+ 'universalIdentifier',
+ 'applicationId',
+ 'workflowVersionId',
+ 'frontComponentId',
+ 'engineComponentKey',
+ 'label',
+ 'icon',
+ 'shortLabel',
+ 'position',
+ 'isPinned',
+ 'availabilityType',
+ 'conditionalAvailabilityExpression',
+ 'availabilityObjectMetadataId',
+ 'hotKeys',
+ ])
+ .values([
+ {
+ workspaceId,
+ universalIdentifier: QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER,
+ applicationId: applicationRow.app_id,
+ workflowVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
+ frontComponentId: null,
+ engineComponentKey: null,
+ label: 'Quick Lead',
+ icon: 'IconUserPlus',
+ shortLabel: 'Quick Lead',
+ position: 100,
+ isPinned: false,
+ availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
+ conditionalAvailabilityExpression: null,
+ availabilityObjectMetadataId: null,
+ hotKeys: null,
+ },
+ ])
+ .orIgnore()
+ .execute();
+};
diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts
index 3347548344..aed38abea4 100644
--- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts
@@ -9,8 +9,9 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
-const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
-const QUICK_LEAD_WORKFLOW_VERSION_ID = 'ac67974f-c524-4288-9d88-af8515400b68';
+export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
+export const QUICK_LEAD_WORKFLOW_VERSION_ID =
+ 'ac67974f-c524-4288-9d88-af8515400b68';
export const prefillWorkflows = async (
entityManager: EntityManager,
diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts
index 8a6af0955d..ca2877d79b 100644
--- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts
@@ -5,11 +5,13 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
+import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
export const standardObjectsPrefillData = async (
dataSource: DataSource,
schemaName: string,
+ workspaceId: string,
flatObjectMetadataMaps: FlatEntityMaps,
flatFieldMetadataMaps: FlatEntityMaps,
) => {
@@ -24,5 +26,7 @@ export const standardObjectsPrefillData = async (
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
+
+ await prefillWorkflowCommandMenuItems(entityManager, workspaceId);
});
};
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts
index 8cc2f8ceed..ec151af84e 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts
@@ -5,6 +5,7 @@ import {
WorkspaceMigrationV2ExceptionCode,
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
+import { type QueryRunner } from 'typeorm';
import { FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -358,12 +359,17 @@ export class WorkspaceMigrationValidateBuildAndRunService {
public async validateBuildAndRunWorkspaceMigrationFromTo(
args: WorkspaceMigrationOrchestratorBuildArgs & {
idByUniversalIdentifierByMetadataName?: IdByUniversalIdentifierByMetadataName;
+ queryRunner?: QueryRunner;
},
): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| WorkspaceMigrationOrchestratorSuccessfulResult
> {
- const { idByUniversalIdentifierByMetadataName, ...buildArgs } = args;
+ const {
+ idByUniversalIdentifierByMetadataName,
+ queryRunner: externalQueryRunner,
+ ...buildArgs
+ } = args;
const validateAndBuildResult =
await this.workspaceMigrationBuildOrchestratorService
@@ -396,6 +402,7 @@ export class WorkspaceMigrationValidateBuildAndRunService {
{
workspaceId: args.workspaceId,
workspaceMigration,
+ queryRunner: externalQueryRunner,
},
);
@@ -416,7 +423,10 @@ export class WorkspaceMigrationValidateBuildAndRunService {
workspaceId,
isSystemBuild = false,
applicationUniversalIdentifier,
- }: ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs): Promise<
+ queryRunner,
+ }: ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs & {
+ queryRunner?: QueryRunner;
+ }): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| WorkspaceMigrationOrchestratorSuccessfulResult
> {
@@ -443,6 +453,7 @@ export class WorkspaceMigrationValidateBuildAndRunService {
dependencyAllFlatEntityMaps,
additionalCacheDataMaps,
idByUniversalIdentifierByMetadataName,
+ queryRunner,
});
}
}
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts
index 5d8874791a..1b03e34f64 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts
@@ -3,7 +3,7 @@ import { InjectDataSource } from '@nestjs/typeorm';
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
-import { DataSource } from 'typeorm';
+import { DataSource, type QueryRunner } from 'typeorm';
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
@@ -154,9 +154,11 @@ export class WorkspaceMigrationRunnerService {
run = async ({
workspaceMigration: { actions, applicationUniversalIdentifier },
workspaceId,
+ queryRunner: externalQueryRunner,
}: {
workspaceMigration: WorkspaceMigration;
workspaceId: string;
+ queryRunner?: QueryRunner;
}): Promise<{
allFlatEntityMaps: AllFlatEntityMaps;
metadataEvents: MetadataEvent[];
@@ -164,7 +166,10 @@ export class WorkspaceMigrationRunnerService {
this.logger.time('Runner', 'Total execution');
this.logger.time('Runner', 'Initial cache retrieval');
- const queryRunner = this.coreDataSource.createQueryRunner();
+ const queryRunner =
+ externalQueryRunner ?? this.coreDataSource.createQueryRunner();
+ const isTransactionAlreadyActive = queryRunner.isTransactionActive;
+
const actionMetadataNames = [
...new Set(actions.flatMap((action) => action.metadataName)),
];
@@ -214,10 +219,12 @@ export class WorkspaceMigrationRunnerService {
this.logger.time('Runner', 'Transaction execution');
- try {
+ if (!isTransactionAlreadyActive) {
await queryRunner.connect();
await queryRunner.startTransaction();
+ }
+ try {
const allMetadataEvents: MetadataEvent[] = [];
for (const action of actions) {
@@ -243,7 +250,9 @@ export class WorkspaceMigrationRunnerService {
allMetadataEvents.push(...metadataEvents);
}
- await queryRunner.commitTransaction();
+ if (!isTransactionAlreadyActive) {
+ await queryRunner.commitTransaction();
+ }
this.logger.timeEnd('Runner', 'Transaction execution');
@@ -256,10 +265,12 @@ export class WorkspaceMigrationRunnerService {
return { allFlatEntityMaps, metadataEvents: allMetadataEvents };
} catch (error) {
- if (queryRunner.isTransactionActive) {
- await queryRunner.rollbackTransaction().catch((error) =>
+ if (!isTransactionAlreadyActive && queryRunner.isTransactionActive) {
+ await queryRunner.rollbackTransaction().catch((rollbackError) =>
// oxlint-disable-next-line no-console
- console.trace(`Failed to rollback transaction: ${error.message}`),
+ console.trace(
+ `Failed to rollback transaction: ${rollbackError.message}`,
+ ),
);
}
@@ -288,7 +299,9 @@ export class WorkspaceMigrationRunnerService {
code: WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR,
});
} finally {
- await queryRunner.release();
+ if (!isTransactionAlreadyActive) {
+ await queryRunner.release();
+ }
}
};
}
diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-query-hook.module.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-query-hook.module.ts
index ad0a47b500..0bcff6d0b2 100644
--- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-query-hook.module.ts
+++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-query-hook.module.ts
@@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
+import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
+import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
@@ -43,6 +45,8 @@ import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ObjectMetadataModule,
CodeStepBuildModule,
+ CommandMenuItemModule,
+ FeatureFlagModule,
],
providers: [
WorkflowCreateOnePreQueryHook,
diff --git a/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts b/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts
index 526d202532..5c318100fc 100644
--- a/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts
+++ b/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts
@@ -1,5 +1,7 @@
import { Module } from '@nestjs/common';
+import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
+import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { WorkflowQueryHookModule } from 'src/modules/workflow/common/query-hooks/workflow-query-hook.module';
@@ -10,6 +12,8 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
WorkflowQueryHookModule,
LogicFunctionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
+ CommandMenuItemModule,
+ FeatureFlagModule,
],
providers: [WorkflowCommonWorkspaceService],
exports: [WorkflowCommonWorkspaceService],
diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts
index 367373778e..dc36e83273 100644
--- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts
+++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts
@@ -1,7 +1,10 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, Logger } from '@nestjs/common';
+import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
+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 { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
@@ -40,10 +43,14 @@ export type ObjectMetadataInfo = {
@Injectable()
export class WorkflowCommonWorkspaceService {
+ private readonly logger = new Logger(WorkflowCommonWorkspaceService.name);
+
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
+ private readonly commandMenuItemService: CommandMenuItemService,
+ private readonly featureFlagService: FeatureFlagService,
) {}
async getWorkflowVersionOrFail({
@@ -262,38 +269,100 @@ export class WorkflowCommonWorkspaceService {
{ shouldBypassPermissionChecks: true },
);
- const workflow = await workflowRepository.findOne({
- where: { id: workflowId },
- withDeleted: true,
- });
-
- if (workflow?.statuses?.includes(WorkflowStatus.ACTIVE)) {
- const newStatuses = [
- ...workflow.statuses.filter(
- (status) => status !== WorkflowStatus.ACTIVE,
- ),
- WorkflowStatus.DEACTIVATED,
- ];
-
- await workflowRepository.update(workflowId, {
- statuses: newStatuses,
- });
- }
-
const workflowVersions = await workflowVersionRepository.find({
- where: {
- workflowId,
- },
+ where: { workflowId },
withDeleted: true,
});
for (const workflowVersion of workflowVersions) {
if (workflowVersion.status === WorkflowVersionStatus.ACTIVE) {
- await workflowVersionRepository.update(workflowVersion.id, {
- status: WorkflowVersionStatus.DEACTIVATED,
- });
+ await this.cleanupCommandMenuItemForVersion(
+ workflowVersion.id,
+ workspaceId,
+ );
}
}
+
+ const workspaceDataSource =
+ await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
+
+ const queryRunner = workspaceDataSource.createQueryRunner();
+
+ await queryRunner.connect();
+ await queryRunner.startTransaction();
+
+ try {
+ const workflow = await workflowRepository.findOne(
+ {
+ where: { id: workflowId },
+ withDeleted: true,
+ },
+ queryRunner.manager,
+ );
+
+ if (workflow?.statuses?.includes(WorkflowStatus.ACTIVE)) {
+ const newStatuses = [
+ ...workflow.statuses.filter(
+ (status) => status !== WorkflowStatus.ACTIVE,
+ ),
+ WorkflowStatus.DEACTIVATED,
+ ];
+
+ await workflowRepository.update(
+ workflowId,
+ { statuses: newStatuses },
+ queryRunner.manager,
+ );
+ }
+
+ for (const workflowVersion of workflowVersions) {
+ if (workflowVersion.status === WorkflowVersionStatus.ACTIVE) {
+ await workflowVersionRepository.update(
+ workflowVersion.id,
+ { status: WorkflowVersionStatus.DEACTIVATED },
+ queryRunner.manager,
+ );
+ }
+ }
+
+ await queryRunner.commitTransaction();
+ } catch (error) {
+ if (queryRunner.isTransactionActive) {
+ await queryRunner.rollbackTransaction();
+ }
+
+ throw error;
+ } finally {
+ await queryRunner.release();
+ }
+ }
+
+ private async cleanupCommandMenuItemForVersion(
+ workflowVersionId: string,
+ workspaceId: string,
+ ) {
+ const isCommandMenuItemEnabled =
+ await this.featureFlagService.isFeatureEnabled(
+ FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
+ workspaceId,
+ );
+
+ if (!isCommandMenuItemEnabled) {
+ return;
+ }
+
+ const existingCommandMenuItem =
+ await this.commandMenuItemService.findByWorkflowVersionId(
+ workflowVersionId,
+ workspaceId,
+ );
+
+ if (isDefined(existingCommandMenuItem)) {
+ await this.commandMenuItemService.delete(
+ existingCommandMenuItem.id,
+ workspaceId,
+ );
+ }
}
async handleLogicFunctionSubEntities({
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts
index 2e994f62c2..451f8dcc52 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts
@@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
+import { isDefined } from 'twenty-shared/utils';
+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 { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import {
@@ -19,21 +21,32 @@ export class AutomatedTriggerWorkspaceService {
type,
settings,
workspaceId,
+ entityManager,
}: {
workflowId: string;
type: AutomatedTriggerType;
settings: AutomatedTriggerSettings;
workspaceId: string;
+ entityManager?: WorkspaceEntityManager;
}) {
+ const workflowAutomatedTriggerRepository =
+ await this.globalWorkspaceOrmManager.getRepository(
+ workspaceId,
+ 'workflowAutomatedTrigger',
+ );
+
+ if (isDefined(entityManager)) {
+ await workflowAutomatedTriggerRepository.insert(
+ { type, settings, workflowId },
+ entityManager,
+ );
+
+ return;
+ }
+
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
- const workflowAutomatedTriggerRepository =
- await this.globalWorkspaceOrmManager.getRepository(
- workspaceId,
- 'workflowAutomatedTrigger',
- );
-
await workflowAutomatedTriggerRepository.insert({
type,
settings,
@@ -45,19 +58,30 @@ export class AutomatedTriggerWorkspaceService {
async deleteAutomatedTrigger({
workflowId,
workspaceId,
+ entityManager,
}: {
workflowId: string;
workspaceId: string;
+ entityManager?: WorkspaceEntityManager;
}) {
+ const workflowAutomatedTriggerRepository =
+ await this.globalWorkspaceOrmManager.getRepository(
+ workspaceId,
+ 'workflowAutomatedTrigger',
+ );
+
+ if (isDefined(entityManager)) {
+ await workflowAutomatedTriggerRepository.delete(
+ { workflowId },
+ entityManager,
+ );
+
+ return;
+ }
+
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
- const workflowAutomatedTriggerRepository =
- await this.globalWorkspaceOrmManager.getRepository(
- workspaceId,
- 'workflowAutomatedTrigger',
- );
-
await workflowAutomatedTriggerRepository.delete({ workflowId });
}, authContext);
}
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts
index 00e0cc0d8a..ed995b044d 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts
@@ -40,6 +40,7 @@ export type WorkflowManualTrigger = BaseTrigger & {
settings: BaseWorkflowTriggerSettings & {
objectType?: string;
icon?: string;
+ isPinned?: boolean;
availability?:
| GlobalAvailability
| SingleRecordAvailability
diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts
index 67b8da00c4..c9dac38499 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts
@@ -1,5 +1,4 @@
-import { Injectable } from '@nestjs/common';
-
+import { Injectable, Logger } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { type ActorMetadata, FeatureFlagKey } from 'twenty-shared/types';
@@ -7,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 { 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';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
@@ -39,6 +39,8 @@ import { assertNever } from 'src/utils/assert';
@Injectable()
export class WorkflowTriggerWorkspaceService {
+ private readonly logger = new Logger(WorkflowTriggerWorkspaceService.name);
+
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
@@ -183,31 +185,96 @@ export class WorkflowTriggerWorkspaceService {
workflowVersionRepository: WorkspaceRepository,
workspaceId: string,
) {
+ const previousPublishedVersionId = workflow.lastPublishedVersionId;
+
if (
- workflow.lastPublishedVersionId &&
- workflowVersion.id !== workflow.lastPublishedVersionId
+ previousPublishedVersionId &&
+ workflowVersion.id !== previousPublishedVersionId
) {
await this.performDeactivationSteps(
- workflow.lastPublishedVersionId,
+ previousPublishedVersionId,
workflowVersionRepository,
workspaceId,
);
}
- await this.upgradeWorkflowVersion(
+ await this.createOrUpdateCommandMenuItem(
workflow,
- workflowVersion.id,
- workflowRepository,
- workflowVersionRepository,
- );
-
- await this.setActiveVersionStatus(
workflowVersion,
- workflowVersionRepository,
workspaceId,
);
- await this.enableTrigger(workflow, workflowVersion, workspaceId);
+ const workspaceDataSource =
+ await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
+
+ const queryRunner = workspaceDataSource.createQueryRunner();
+
+ await queryRunner.connect();
+ await queryRunner.startTransaction();
+
+ try {
+ if (workflow.lastPublishedVersionId !== workflowVersion.id) {
+ if (workflow.lastPublishedVersionId) {
+ await workflowVersionRepository.update(
+ { id: workflow.lastPublishedVersionId },
+ { status: WorkflowVersionStatus.ARCHIVED },
+ queryRunner.manager,
+ );
+ }
+
+ await workflowRepository.update(
+ { id: workflow.id },
+ { lastPublishedVersionId: workflowVersion.id },
+ queryRunner.manager,
+ );
+ }
+
+ const activeWorkflowVersions = await workflowVersionRepository.find(
+ {
+ where: {
+ workflowId: workflowVersion.workflowId,
+ status: WorkflowVersionStatus.ACTIVE,
+ },
+ },
+ queryRunner.manager,
+ );
+
+ if (activeWorkflowVersions.length > 0) {
+ throw new WorkflowTriggerException(
+ 'Cannot have more than one active workflow version',
+ WorkflowTriggerExceptionCode.FORBIDDEN,
+ {
+ userFriendlyMessage: msg`Cannot have more than one active workflow version`,
+ },
+ );
+ }
+
+ await workflowVersionRepository.update(
+ { id: workflowVersion.id },
+ { status: WorkflowVersionStatus.ACTIVE },
+ queryRunner.manager,
+ );
+
+ await this.enableAutomatedTrigger(workflowVersion, workspaceId, {
+ entityManager: queryRunner.manager,
+ });
+
+ await queryRunner.commitTransaction();
+ } catch (error) {
+ if (queryRunner.isTransactionActive) {
+ await queryRunner.rollbackTransaction();
+ }
+
+ throw error;
+ } finally {
+ await queryRunner.release();
+ }
+
+ await this.emitStatusUpdateEvents(
+ workflowVersion,
+ WorkflowVersionStatus.ACTIVE,
+ workspaceId,
+ );
}
private async performDeactivationSteps(
@@ -228,69 +295,38 @@ export class WorkflowTriggerWorkspaceService {
return;
}
- await this.setDeactivatedVersionStatus(
- workflowVersion,
- workflowVersionRepository,
- workspaceId,
- );
+ await this.deleteCommandMenuItem(workflowVersion, workspaceId);
- await this.disableTrigger(workflowVersion, workspaceId);
- }
+ const workspaceDataSource =
+ await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
- private async setActiveVersionStatus(
- workflowVersion: WorkflowVersionWorkspaceEntity,
- workflowVersionRepository: WorkspaceRepository,
- workspaceId: string,
- ) {
- const activeWorkflowVersions = await workflowVersionRepository.find({
- where: {
- workflowId: workflowVersion.workflowId,
- status: WorkflowVersionStatus.ACTIVE,
- },
- });
+ const queryRunner = workspaceDataSource.createQueryRunner();
- if (activeWorkflowVersions.length > 0) {
- throw new WorkflowTriggerException(
- 'Cannot have more than one active workflow version',
- WorkflowTriggerExceptionCode.FORBIDDEN,
- {
- userFriendlyMessage: msg`Cannot have more than one active workflow version`,
- },
+ await queryRunner.connect();
+ await queryRunner.startTransaction();
+
+ try {
+ await workflowVersionRepository.update(
+ { id: workflowVersion.id },
+ { status: WorkflowVersionStatus.DEACTIVATED },
+ queryRunner.manager,
);
+
+ await this.disableAutomatedTrigger(workflowVersion, workspaceId, {
+ entityManager: queryRunner.manager,
+ });
+
+ await queryRunner.commitTransaction();
+ } catch (error) {
+ if (queryRunner.isTransactionActive) {
+ await queryRunner.rollbackTransaction();
+ }
+
+ throw error;
+ } finally {
+ await queryRunner.release();
}
- await workflowVersionRepository.update(
- { id: workflowVersion.id },
- { status: WorkflowVersionStatus.ACTIVE },
- );
-
- await this.emitStatusUpdateEvents(
- workflowVersion,
- WorkflowVersionStatus.ACTIVE,
- workspaceId,
- );
- }
-
- private async setDeactivatedVersionStatus(
- workflowVersion: WorkflowVersionWorkspaceEntity,
- workflowVersionRepository: WorkspaceRepository,
- workspaceId: string,
- ) {
- if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
- throw new WorkflowTriggerException(
- 'Cannot disable non-active workflow version',
- WorkflowTriggerExceptionCode.FORBIDDEN,
- {
- userFriendlyMessage: msg`Cannot disable non-active workflow version`,
- },
- );
- }
-
- await workflowVersionRepository.update(
- { id: workflowVersion.id },
- { status: WorkflowVersionStatus.DEACTIVATED },
- );
-
await this.emitStatusUpdateEvents(
workflowVersion,
WorkflowVersionStatus.DEACTIVATED,
@@ -298,101 +334,161 @@ export class WorkflowTriggerWorkspaceService {
);
}
- private async upgradeWorkflowVersion(
- workflow: WorkflowWorkspaceEntity,
- newPublishedVersionId: string,
- workflowRepository: WorkspaceRepository,
- workflowVersionRepository: WorkspaceRepository,
- ) {
- if (workflow.lastPublishedVersionId === newPublishedVersionId) {
- return;
+ private async resolveManualTriggerAvailability(
+ trigger: WorkflowManualTrigger,
+ workspaceId: string,
+ ): Promise<{
+ availabilityType: CommandMenuItemAvailabilityType;
+ availabilityObjectMetadataId: string | undefined;
+ }> {
+ const availability = trigger.settings.availability;
+
+ let availabilityType = CommandMenuItemAvailabilityType.GLOBAL;
+ let availabilityObjectMetadataId: string | undefined;
+
+ if (availability) {
+ switch (availability.type) {
+ case 'GLOBAL':
+ availabilityType = CommandMenuItemAvailabilityType.GLOBAL;
+ break;
+ case 'SINGLE_RECORD':
+ case 'BULK_RECORDS': {
+ availabilityType = CommandMenuItemAvailabilityType.RECORD_SELECTION;
+
+ const { objectIdByNameSingular } =
+ await this.workflowCommonWorkspaceService.getFlatEntityMaps(
+ workspaceId,
+ );
+
+ const objectId =
+ objectIdByNameSingular[availability.objectNameSingular];
+
+ if (!objectId) {
+ throw new WorkflowTriggerException(
+ `Object metadata not found for object: ${availability.objectNameSingular}`,
+ WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
+ );
+ }
+
+ availabilityObjectMetadataId = objectId;
+ break;
+ }
+ }
}
- if (workflow.lastPublishedVersionId) {
- await workflowVersionRepository.update(
- { id: workflow.lastPublishedVersionId },
- { status: WorkflowVersionStatus.ARCHIVED },
- );
- }
-
- await workflowRepository.update(
- { id: workflow.id },
- { lastPublishedVersionId: newPublishedVersionId },
- );
+ return { availabilityType, availabilityObjectMetadataId };
}
- private async enableTrigger(
+ private async createOrUpdateCommandMenuItem(
workflow: WorkflowWorkspaceEntity,
workflowVersion: WorkflowVersionWorkspaceEntity,
workspaceId: string,
) {
assertWorkflowVersionTriggerIsDefined(workflowVersion);
+ if (workflowVersion.trigger.type !== WorkflowTriggerType.MANUAL) {
+ return;
+ }
+
+ const isCommandMenuItemEnabled =
+ await this.featureFlagService.isFeatureEnabled(
+ FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
+ workspaceId,
+ );
+
+ if (!isCommandMenuItemEnabled) {
+ return;
+ }
+
+ const trigger = workflowVersion.trigger as WorkflowManualTrigger;
+
+ const { availabilityType, availabilityObjectMetadataId } =
+ await this.resolveManualTriggerAvailability(trigger, workspaceId);
+
+ const label = isNonEmptyString(workflow.name)
+ ? workflow.name
+ : 'Manual Trigger';
+
+ const existingCommandMenuItem =
+ await this.commandMenuItemService.findByWorkflowVersionId(
+ workflowVersion.id,
+ workspaceId,
+ );
+
+ if (existingCommandMenuItem) {
+ await this.commandMenuItemService.update(
+ {
+ id: existingCommandMenuItem.id,
+ label,
+ shortLabel: label,
+ icon: trigger.settings.icon,
+ isPinned: trigger.settings.isPinned,
+ availabilityType,
+ availabilityObjectMetadataId,
+ },
+ workspaceId,
+ );
+ } else {
+ await this.commandMenuItemService.create(
+ {
+ workflowVersionId: workflowVersion.id,
+ label,
+ shortLabel: label,
+ icon: trigger.settings.icon,
+ isPinned: trigger.settings.isPinned,
+ availabilityType,
+ availabilityObjectMetadataId,
+ },
+ workspaceId,
+ );
+ }
+ }
+
+ private async deleteCommandMenuItem(
+ workflowVersion: WorkflowVersionWorkspaceEntity,
+ workspaceId: string,
+ ) {
+ assertWorkflowVersionTriggerIsDefined(workflowVersion);
+
+ if (workflowVersion.trigger.type !== WorkflowTriggerType.MANUAL) {
+ return;
+ }
+
+ const isCommandMenuItemEnabled =
+ await this.featureFlagService.isFeatureEnabled(
+ FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
+ workspaceId,
+ );
+
+ if (!isCommandMenuItemEnabled) {
+ return;
+ }
+
+ const existingCommandMenuItem =
+ await this.commandMenuItemService.findByWorkflowVersionId(
+ workflowVersion.id,
+ workspaceId,
+ );
+
+ if (existingCommandMenuItem) {
+ await this.commandMenuItemService.delete(
+ existingCommandMenuItem.id,
+ workspaceId,
+ );
+ }
+ }
+
+ private async enableAutomatedTrigger(
+ workflowVersion: WorkflowVersionWorkspaceEntity,
+ workspaceId: string,
+ transactionContext?: {
+ entityManager: WorkspaceEntityManager;
+ },
+ ) {
+ assertWorkflowVersionTriggerIsDefined(workflowVersion);
+
switch (workflowVersion.trigger.type) {
- case WorkflowTriggerType.MANUAL: {
- const isCommandMenuItemEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
- workspaceId,
- );
-
- if (!isCommandMenuItemEnabled) {
- return;
- }
-
- const trigger = workflowVersion.trigger as WorkflowManualTrigger;
-
- const availability = trigger.settings.availability;
-
- let availabilityType = CommandMenuItemAvailabilityType.GLOBAL;
- let availabilityObjectMetadataId: string | undefined;
-
- if (availability) {
- switch (availability.type) {
- case 'GLOBAL':
- availabilityType = CommandMenuItemAvailabilityType.GLOBAL;
- break;
- case 'SINGLE_RECORD':
- case 'BULK_RECORDS': {
- availabilityType =
- CommandMenuItemAvailabilityType.RECORD_SELECTION;
-
- const { objectIdByNameSingular } =
- await this.workflowCommonWorkspaceService.getFlatEntityMaps(
- workspaceId,
- );
-
- const objectId =
- objectIdByNameSingular[availability.objectNameSingular];
-
- if (!objectId) {
- throw new WorkflowTriggerException(
- `Object metadata not found for object: ${availability.objectNameSingular}`,
- WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
- );
- }
-
- availabilityObjectMetadataId = objectId;
- break;
- }
- }
- }
-
- await this.commandMenuItemService.create(
- {
- workflowVersionId: workflowVersion.id,
- label: isNonEmptyString(workflow.name)
- ? workflow.name
- : 'Manual Trigger',
- icon: trigger.settings.icon,
- availabilityType,
- availabilityObjectMetadataId,
- },
- workspaceId,
- );
-
- return;
- }
+ case WorkflowTriggerType.MANUAL:
case WorkflowTriggerType.WEBHOOK:
return;
case WorkflowTriggerType.DATABASE_EVENT: {
@@ -404,6 +500,7 @@ export class WorkflowTriggerWorkspaceService {
type: AutomatedTriggerType.DATABASE_EVENT,
settings,
workspaceId,
+ entityManager: transactionContext?.entityManager,
});
return;
@@ -416,19 +513,22 @@ export class WorkflowTriggerWorkspaceService {
type: AutomatedTriggerType.CRON,
settings: { pattern },
workspaceId,
+ entityManager: transactionContext?.entityManager,
});
return;
}
- default: {
+ default:
assertNever(workflowVersion.trigger);
- }
}
}
- private async disableTrigger(
+ private async disableAutomatedTrigger(
workflowVersion: WorkflowVersionWorkspaceEntity,
workspaceId: string,
+ transactionContext?: {
+ entityManager: WorkspaceEntityManager;
+ },
) {
assertWorkflowVersionTriggerIsDefined(workflowVersion);
@@ -438,35 +538,11 @@ export class WorkflowTriggerWorkspaceService {
await this.automatedTriggerWorkspaceService.deleteAutomatedTrigger({
workflowId: workflowVersion.workflowId,
workspaceId,
+ entityManager: transactionContext?.entityManager,
});
return;
- case WorkflowTriggerType.MANUAL: {
- const isCommandMenuItemEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
- workspaceId,
- );
-
- if (!isCommandMenuItemEnabled) {
- return;
- }
-
- const existingCommandMenuItem =
- await this.commandMenuItemService.findByWorkflowVersionId(
- workflowVersion.id,
- workspaceId,
- );
-
- if (existingCommandMenuItem) {
- await this.commandMenuItemService.delete(
- existingCommandMenuItem.id,
- workspaceId,
- );
- }
-
- return;
- }
+ case WorkflowTriggerType.MANUAL:
case WorkflowTriggerType.WEBHOOK:
return;
default: