feat(workflow): surface manual-trigger payload + metadata in variable picker (#21692)
## Summary Step 2 of the manual-trigger output schema restructuring (expand → display → migrate → contract). Builds on the now-merged #21676 (which expanded the runtime payload to serve `payload` and `metadata` siblings at the trigger root). This PR **surfaces** those in the variable picker as nested, expandable nodes: - `trigger.payload.{record fields}` — the record(s) that triggered the run - `trigger.metadata.workspaceMemberId` — who triggered it The flat root fields (`trigger.id`, etc.) remain available, so existing saved variable references keep working until a later migration phase moves them. ### Changes - **twenty-shared**: metadata/payload label constants + `build-manual-trigger-metadata-node` util + barrel exports. - **twenty-front**: `computeStepOutputSchema` MANUAL branch now nests `payload` (RecordNode for SINGLE_RECORD, array Node for BULK_RECORDS, omitted for GLOBAL) and `metadata`; `ManualTriggerOutputSchema` type updated to `{ payload?; metadata }`. - **twenty-server**: `computeTriggerOutputSchemaFromAvailability` mirrors the same nested shape for server-side validation. The key is `metadata` (not `_metadata`) — custom fields can't start with `_`, so collision risk was deemed acceptable. ## Test plan - [x] `npx nx build twenty-shared` - [x] `computeStepOutputSchema` unit tests pass (55) - [x] Manual: create a manual-trigger workflow (GLOBAL / single-record / bulk), confirm the picker shows `payload` and `metadata` as expandable folders and that selecting a field yields `{{trigger.payload.<field>}}` / `{{trigger.metadata.workspaceMemberId}}` > Note: server typecheck has pre-existing unrelated failures on main (Stripe billing mocks, gmail mocks); none touch workflow files. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21692?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
-6
@@ -1,6 +0,0 @@
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow';
|
||||
|
||||
export type ManualTriggerOutputSchema =
|
||||
| BaseOutputSchemaV2
|
||||
| RecordOutputSchemaV2;
|
||||
+4
-2
@@ -6,9 +6,11 @@ import { type CodeOutputSchema } from '@/workflow/workflow-variables/types/CodeO
|
||||
import { type FindRecordsOutputSchema } from '@/workflow/workflow-variables/types/FindRecordsOutputSchema';
|
||||
import { type FormOutputSchema } from '@/workflow/workflow-variables/types/FormOutputSchema';
|
||||
import { type IteratorOutputSchema } from '@/workflow/workflow-variables/types/IteratorOutputSchema';
|
||||
import { type ManualTriggerOutputSchema } from '@/workflow/workflow-variables/types/ManualTriggerOutputSchema';
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow';
|
||||
import {
|
||||
type BaseOutputSchemaV2,
|
||||
type ManualTriggerOutputSchema,
|
||||
} from 'twenty-shared/workflow';
|
||||
|
||||
export type OutputSchemaV2 =
|
||||
| BaseOutputSchemaV2
|
||||
|
||||
+1
-1
@@ -2,8 +2,8 @@ import {
|
||||
type WorkflowActionType,
|
||||
type WorkflowTriggerType,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { type ManualTriggerOutputSchema } from '@/workflow/workflow-variables/types/ManualTriggerOutputSchema';
|
||||
import { type OutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
import { type ManualTriggerOutputSchema } from 'twenty-shared/workflow';
|
||||
|
||||
export const isManualTriggerOutputSchema = (
|
||||
stepType: WorkflowActionType | WorkflowTriggerType,
|
||||
|
||||
+29
-8
@@ -159,7 +159,7 @@ describe('computeStepOutputSchema', () => {
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should return empty object for GLOBAL availability', () => {
|
||||
it('should expose only metadata for GLOBAL availability', () => {
|
||||
const result = computeStepOutputSchema({
|
||||
step: {
|
||||
type: 'MANUAL',
|
||||
@@ -168,10 +168,22 @@ describe('computeStepOutputSchema', () => {
|
||||
objectMetadataItems: [mockCompanyObjectMetadataItem],
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(result).not.toHaveProperty('payload');
|
||||
expect((result as any).metadata).toMatchObject({
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'Metadata',
|
||||
value: {
|
||||
workspaceMemberId: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Workspace Member Id',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return record output schema for SINGLE_RECORD availability', () => {
|
||||
it('should nest the record under payload and expose metadata for SINGLE_RECORD availability', () => {
|
||||
const result = computeStepOutputSchema({
|
||||
step: {
|
||||
type: 'MANUAL',
|
||||
@@ -185,11 +197,19 @@ describe('computeStepOutputSchema', () => {
|
||||
objectMetadataItems: [mockCompanyObjectMetadataItem],
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('_outputSchemaType', 'RECORD');
|
||||
expect(result).toHaveProperty('object');
|
||||
expect((result as any).payload).toMatchObject({
|
||||
isLeaf: false,
|
||||
label: 'Payload',
|
||||
});
|
||||
expect((result as any).payload.value).toHaveProperty(
|
||||
'_outputSchemaType',
|
||||
'RECORD',
|
||||
);
|
||||
expect((result as any).payload.value).toHaveProperty('object');
|
||||
expect(result).toHaveProperty('metadata');
|
||||
});
|
||||
|
||||
it('should return array indicator for BULK_RECORDS availability', () => {
|
||||
it('should nest the array indicator under payload and expose metadata for BULK_RECORDS availability', () => {
|
||||
const result = computeStepOutputSchema({
|
||||
step: {
|
||||
type: 'MANUAL',
|
||||
@@ -203,12 +223,13 @@ describe('computeStepOutputSchema', () => {
|
||||
objectMetadataItems: [mockCompanyObjectMetadataItem],
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('companies');
|
||||
expect((result as any).companies).toMatchObject({
|
||||
expect((result as any).payload.value).toHaveProperty('companies');
|
||||
expect((result as any).payload.value.companies).toMatchObject({
|
||||
isLeaf: true,
|
||||
label: 'Companies',
|
||||
type: 'array',
|
||||
});
|
||||
expect(result).toHaveProperty('metadata');
|
||||
});
|
||||
|
||||
it('should return empty object when object metadata is not found for SINGLE_RECORD', () => {
|
||||
|
||||
+32
-8
@@ -11,6 +11,12 @@ import { generateRecordEventOutputSchema } from '@/workflow/workflow-variables/u
|
||||
import { generateRecordOutputSchema } from '@/workflow/workflow-variables/utils/generate/generateRecordOutputSchema';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
buildManualTriggerMetadataNode,
|
||||
WORKFLOW_TRIGGER_METADATA_KEY,
|
||||
WORKFLOW_TRIGGER_PAYLOAD_KEY,
|
||||
WORKFLOW_TRIGGER_PAYLOAD_LABEL,
|
||||
} from 'twenty-shared/workflow';
|
||||
import { DatabaseEventAction } from '~/generated-metadata/graphql';
|
||||
|
||||
const PERSISTED_OUTPUT_SCHEMA_TYPES = [
|
||||
@@ -101,7 +107,9 @@ export const computeStepOutputSchema = ({
|
||||
}
|
||||
|
||||
if (availability.type === 'GLOBAL') {
|
||||
return {};
|
||||
return {
|
||||
[WORKFLOW_TRIGGER_METADATA_KEY]: buildManualTriggerMetadataNode(),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -118,17 +126,33 @@ export const computeStepOutputSchema = ({
|
||||
}
|
||||
|
||||
if (availability.type === 'SINGLE_RECORD') {
|
||||
return generateRecordOutputSchema(objectMetadataItem);
|
||||
return {
|
||||
[WORKFLOW_TRIGGER_PAYLOAD_KEY]: {
|
||||
isLeaf: false,
|
||||
icon: objectMetadataItem.icon ?? undefined,
|
||||
label: WORKFLOW_TRIGGER_PAYLOAD_LABEL,
|
||||
value: generateRecordOutputSchema(objectMetadataItem),
|
||||
},
|
||||
[WORKFLOW_TRIGGER_METADATA_KEY]: buildManualTriggerMetadataNode(),
|
||||
};
|
||||
}
|
||||
|
||||
// BULK_RECORDS - return array indicator
|
||||
// BULK_RECORDS - array indicator nested under payload
|
||||
return {
|
||||
[objectMetadataItem.namePlural]: {
|
||||
isLeaf: true,
|
||||
label: objectMetadataItem.labelPlural,
|
||||
type: 'array',
|
||||
value: `Array of ${objectMetadataItem.labelPlural}`,
|
||||
[WORKFLOW_TRIGGER_PAYLOAD_KEY]: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: WORKFLOW_TRIGGER_PAYLOAD_LABEL,
|
||||
value: {
|
||||
[objectMetadataItem.namePlural]: {
|
||||
isLeaf: true,
|
||||
label: objectMetadataItem.labelPlural,
|
||||
type: 'array',
|
||||
value: `Array of ${objectMetadataItem.labelPlural}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
[WORKFLOW_TRIGGER_METADATA_KEY]: buildManualTriggerMetadataNode(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user