diff --git a/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts b/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts index 5f8ec90939..7cbb686077 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts @@ -1,18 +1,47 @@ import { stepsOutputSchemaFamilyState } from '@/workflow/states/stepsOutputSchemaFamilyState'; -import { type WorkflowVersion } from '@/workflow/types/Workflow'; +import { + type WorkflowActionType, + type WorkflowVersion, +} from '@/workflow/types/Workflow'; import { getStepOutputSchemaFamilyStateKey } from '@/workflow/utils/getStepOutputSchemaFamilyStateKey'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel'; import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon'; +import { isFindRecordsOutputSchema } from '@/workflow/workflow-variables/types/guards/isFindRecordsOutputSchema'; import { type OutputSchemaV2, type StepOutputSchemaV2, } from '@/workflow/workflow-variables/types/StepOutputSchemaV2'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; +import { FeatureFlagKey } from '~/generated/graphql'; +const getFilteredOutputSchema = ({ + stepType, + outputSchema, + isIteratorEnabled, +}: { + stepType: WorkflowActionType; + outputSchema: OutputSchemaV2; + isIteratorEnabled: boolean; +}) => { + if (!isIteratorEnabled && isFindRecordsOutputSchema(stepType, outputSchema)) { + const filteredOutputSchema = { + ...outputSchema, + all: undefined, + }; + + return filteredOutputSchema; + } + + return outputSchema; +}; export const useStepsOutputSchema = () => { + const isIteratorEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED, + ); const populateStepsOutputSchema = useRecoilCallback( ({ set }) => (workflowVersion: WorkflowVersion) => { @@ -22,7 +51,11 @@ export const useStepsOutputSchema = () => { name: step.name, type: step.type, icon: getActionIcon(step.type), - outputSchema: step.settings?.outputSchema as OutputSchemaV2, + outputSchema: getFilteredOutputSchema({ + stepType: step.type, + outputSchema: step.settings?.outputSchema as OutputSchemaV2, + isIteratorEnabled, + }), }; set( @@ -59,7 +92,7 @@ export const useStepsOutputSchema = () => { ); } }, - [], + [isIteratorEnabled], ); const deleteStepsOutputSchema = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx index 992d9be2ca..544681d200 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx @@ -19,6 +19,7 @@ import { WorkflowFindRecordsFilters } from '@/workflow/workflow-steps/workflow-a import { WorkflowFindRecordsFiltersEffect } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFiltersEffect'; import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader'; import { useLingui } from '@lingui/react/macro'; +import { isNumber } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; import { HorizontalSeparator, useIcons } from 'twenty-ui/display'; import { type SelectOption } from 'twenty-ui/input'; @@ -230,8 +231,20 @@ export const WorkflowEditActionFindRecords = ({ label="Limit" defaultValue={formData.limit} placeholder="Enter limit" - onChange={() => {}} - readonly + onChange={(limit) => { + if (isFormDisabled === true || !isNumber(limit)) { + return; + } + + const newFormData: FindRecordsFormData = { + ...formData, + limit, + }; + + setFormData(newFormData); + + saveAction(newFormData); + }} /> {!actionOptions.readonly && } diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/FindRecordsOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/FindRecordsOutputSchema.ts index 3a41b9956b..f2fc9d63db 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/FindRecordsOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/FindRecordsOutputSchema.ts @@ -1,7 +1,8 @@ +import type { Leaf } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type RecordNode } from '@/workflow/workflow-variables/types/RecordNode'; export type FindRecordsOutputSchema = { first: RecordNode; - last: RecordNode; - totalCount: number; + all: Leaf | undefined; + totalCount: Leaf; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughFindRecordsOutputSchema.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughFindRecordsOutputSchema.test.ts index 6ad7dd985e..c96bbabf33 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughFindRecordsOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughFindRecordsOutputSchema.test.ts @@ -36,12 +36,18 @@ describe('searchVariableThroughFindRecordsOutputSchema', () => { label: 'First', value: mockRecordSchema, }, - last: { - isLeaf: false, - label: 'Last', - value: mockRecordSchema, + all: { + isLeaf: true, + label: 'All', + value: 'Returns an array of records', + type: 'array', + }, + totalCount: { + isLeaf: true, + label: 'Total Count', + value: 42, + type: 'number', }, - totalCount: 42, }; it('should handle totalCount variable correctly', () => { @@ -76,20 +82,18 @@ describe('searchVariableThroughFindRecordsOutputSchema', () => { }); }); - it('should handle last record field access correctly', () => { + it('should handle all records access correctly', () => { const result = searchVariableThroughFindRecordsOutputSchema({ stepName: 'Find Companies', searchRecordOutputSchema: mockSearchRecordSchema, - rawVariableName: '{{step1.last.revenue}}', + rawVariableName: '{{step1.all}}', isFullRecord: false, }); expect(result).toEqual({ - variableLabel: 'Revenue', - variablePathLabel: 'Find Companies > Last > Revenue', - variableType: FieldMetadataType.NUMBER, - fieldMetadataId: 'company-revenue-metadata-id', - compositeFieldSubFieldName: undefined, + variableLabel: 'All', + variablePathLabel: 'Find Companies > All', + variableType: FieldMetadataType.ARRAY, }); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughFindRecordsOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughFindRecordsOutputSchema.ts index 311652dfb3..7f8d5200f3 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughFindRecordsOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughFindRecordsOutputSchema.ts @@ -5,7 +5,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow'; -type SearchResultKey = 'first' | 'last' | 'totalCount'; +type SearchResultKey = 'first' | 'all' | 'totalCount'; /** * Parses a variable name to extract its components for SearchRecord outputs @@ -68,15 +68,7 @@ export const searchVariableThroughFindRecordsOutputSchema = ({ }; } - if (searchResultKey === 'totalCount') { - return { - variableLabel: 'Total Count', - variablePathLabel: `${stepName} > Total Count`, - variableType: FieldMetadataType.NUMBER, - }; - } - - if (searchResultKey === 'first' || searchResultKey === 'last') { + if (searchResultKey === 'first') { const recordSchema = searchRecordOutputSchema[searchResultKey]?.value; if (!isDefined(recordSchema) || !isDefined(fieldName)) { @@ -87,7 +79,7 @@ export const searchVariableThroughFindRecordsOutputSchema = ({ } return searchRecordOutputSchemaUtil({ - stepName: `${stepName} > ${searchResultKey === 'first' ? 'First' : 'Last'}`, + stepName: `${stepName} > ${searchRecordOutputSchema[searchResultKey]?.label ?? 'First'}`, recordOutputSchema: recordSchema, selectedField: fieldName, path: pathSegments, @@ -95,6 +87,24 @@ export const searchVariableThroughFindRecordsOutputSchema = ({ }); } + if (searchResultKey === 'totalCount') { + return { + variableLabel: + searchRecordOutputSchema[searchResultKey]?.label ?? 'Total Count', + variablePathLabel: `${stepName} > ${searchRecordOutputSchema[searchResultKey]?.label ?? 'Total Count'}`, + variableType: FieldMetadataType.NUMBER, + }; + } + + if (searchResultKey === 'all') { + return { + variableLabel: + searchRecordOutputSchema[searchResultKey]?.label ?? 'All Records', + variablePathLabel: `${stepName} > ${searchRecordOutputSchema[searchResultKey]?.label ?? 'All Records'}`, + variableType: FieldMetadataType.ARRAY, + }; + } + return { variableLabel: undefined, variablePathLabel: undefined, diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts index 8fa693428d..58635475d5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts @@ -76,6 +76,14 @@ export const searchVariableThroughIteratorOutputSchema = ({ if (iteratorResultKey === 'currentItem') { const schema = iteratorOutputSchema.currentItem.value; + + if (!isDefined(schema)) { + return { + variableLabel: undefined, + variablePathLabel: undefined, + }; + } + if (isRecordOutputSchemaV2(schema) && isDefined(fieldName)) { return searchRecordOutputSchema({ stepName: `${stepName} > Current Item`, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 097ae058e3..3d13e1ac61 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -28,6 +28,7 @@ import { AddPositionsToWorkflowVersionsAndWorkflowRunsCommand } from 'src/databa import { MigrateViewsToCoreCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command'; import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command'; import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command'; +import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -79,6 +80,9 @@ export class UpgradeCommand extends UpgradeCommandRunner { // 1.6 Commands protected readonly fixLabelIdentifierPositionAndVisibilityCommand: FixLabelIdentifierPositionAndVisibilityCommand, + + // 1.7 Commands + protected readonly backfillWorkflowManualTriggerAvailabilityCommand: BackfillWorkflowManualTriggerAvailabilityCommand, ) { super( workspaceRepository, @@ -165,6 +169,13 @@ export class UpgradeCommand extends UpgradeCommandRunner { afterSyncMetadata: [], }; + const commands_170: VersionCommands = { + beforeSyncMetadata: [ + this.backfillWorkflowManualTriggerAvailabilityCommand, + ], + afterSyncMetadata: [], + }; + this.allCommands = { '0.53.0': commands_053, '0.54.0': commands_054, @@ -177,6 +188,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { '1.4.0': commands_140, '1.5.0': commands_150, '1.6.0': commands_160, + '1.7.0': commands_170, }; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index bebac2992d..4ab13b2f69 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -215,20 +215,37 @@ export class WorkflowSchemaWorkspaceService { maxDepth: 0, }); - return { - first: { - isLeaf: false, - icon: 'IconAlpha', - value: recordOutputSchema, - }, - last: { isLeaf: false, icon: 'IconOmega', value: recordOutputSchema }, - totalCount: { - isLeaf: true, - icon: 'IconSum', - type: 'number', - value: generateFakeValue('number'), - }, + const objectMetadataInfo = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + objectType, + workspaceId, + ); + + const first: Node = { + isLeaf: false, + label: `First ${objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular ?? 'Record'}`, + icon: 'IconAlpha', + type: 'object', + value: recordOutputSchema, }; + + const all: Leaf = { + isLeaf: true, + label: `All ${objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural ?? 'Records'}`, + type: 'array', + icon: 'IconListDetails', + value: 'Returns an array of records', + }; + + const totalCount: Leaf = { + isLeaf: true, + label: 'Total Count', + icon: 'IconSum', + type: 'number', + value: 'Count of matching records', + }; + + return { first, all, totalCount } satisfies OutputSchema; } private async computeRecordOutputSchema({ @@ -403,8 +420,36 @@ export class WorkflowSchemaWorkspaceService { } } - // TODO(t.trompette): handle other step types + const step = workflowVersion.steps?.find((step) => step.id === stepId); - return DEFAULT_ITERATOR_CURRENT_ITEM; + if (!isDefined(step)) { + return DEFAULT_ITERATOR_CURRENT_ITEM; + } + + switch (step.type) { + case WorkflowActionType.FIND_RECORDS: { + const objectMetadataInfo = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + step.settings.input.objectName, + workspaceId, + ); + + return { + label: + 'Current Item (' + + objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular + + ')', + isLeaf: false, + type: 'object', + value: await this.computeRecordOutputSchema({ + objectType: step.settings.input.objectName, + workspaceId, + }), + }; + } + default: { + return DEFAULT_ITERATOR_CURRENT_ITEM; + } + } } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index f96eb88e0f..648555fba4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -107,7 +107,7 @@ export class FindRecordsWorkflowAction implements WorkflowAction { return { result: { first: objectRecords[0], - last: objectRecords[objectRecords.length - 1], + all: objectRecords, totalCount, }, }; @@ -144,7 +144,11 @@ export class FindRecordsWorkflowAction implements WorkflowAction { ); return withOrderByQueryBuilder - .take(workflowActionInput.limit ?? QUERY_MAX_RECORDS) + .take( + workflowActionInput.limit + ? Math.min(workflowActionInput.limit, QUERY_MAX_RECORDS) + : QUERY_MAX_RECORDS, + ) .getMany(); }