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: