diff --git a/.changeset/map-node-config-schema.md b/.changeset/map-node-config-schema.md new file mode 100644 index 0000000000..0cea361c3b --- /dev/null +++ b/.changeset/map-node-config-schema.md @@ -0,0 +1,22 @@ +--- +"@objectstack/service-automation": patch +--- + +feat(automation): publish a configSchema for the `map` node (flow designer parity, #3304) + +The `map` (sequential multi-instance) node shipped no `configSchema`, so the flow +designer fell back to its hardcoded field group online and to raw Advanced-JSON +where that wasn't present. Its descriptor now carries a structured `configSchema` +that mirrors the objectui hardcoded `map` field group field-for-field — +`collection` (marked `xExpression: 'template'`, an `interpolate()` `{items}` +template, same as `loop.collection`), `flowName` + `itemObject` as typed +references (`xRef`), and `iteratorVariable` / `outputVariable` as plain text — so +the online (schema-driven) and offline forms match. + +`map` is the one previously-schemaless flow node whose fields are all scalars and +typed references, so it maps cleanly through objectui's `jsonSchemaToFlowFields` +with zero regression. The remaining schemaless nodes lean on editor kinds the +schema→fields adapter does not yet reproduce (`keyValue` maps, the decision +virtual `target` column, `wait`'s top-level block), and are deferred to #3304 +until that adapter is extended. Additive and backward-compatible: no runtime +behavior change; an older designer that ignores the schema is unaffected. diff --git a/packages/services/service-automation/src/builtin/map-node.test.ts b/packages/services/service-automation/src/builtin/map-node.test.ts index 8a245faa4e..c236acb082 100644 --- a/packages/services/service-automation/src/builtin/map-node.test.ts +++ b/packages/services/service-automation/src/builtin/map-node.test.ts @@ -164,3 +164,27 @@ describe('map node executor (sequential multi-instance)', () => { expect(await store.list()).toHaveLength(0); }); }); + +describe('map descriptor configSchema (objectui #2670 Phase 3 / #3304)', () => { + it('publishes a structured config form matching the objectui `map` field group', () => { + // `map` is the one previously-schemaless node whose fields all map cleanly + // through jsonSchemaToFlowFields (scalars + typed references, no keyValue / + // virtual columns), so shipping this schema makes the online designer form + // match the offline hardcoded one with zero regression. + const engine = new AutomationEngine(silentLogger()); + registerMapNode(engine, ctx()); + const schema = engine.getActionDescriptor('map')?.configSchema as + | { properties?: Record; required?: string[] } + | undefined; + expect(schema).toBeDefined(); + // `collection` is an interpolate() `{items}` template (mono editor + `{var}` + // picker, no CEL brace-trap) — the same marker as loop.collection. + expect(schema?.properties?.collection?.xExpression).toBe('template'); + // `flowName` / `itemObject` are typed references → pickers, not free text. + expect(schema?.properties?.flowName?.xRef?.kind).toBe('flow'); + expect(schema?.properties?.itemObject?.xRef?.kind).toBe('object'); + // Plain scalar text fields carry no authoring marker. + expect(schema?.properties?.iteratorVariable?.xExpression).toBeUndefined(); + expect(schema?.properties?.outputVariable?.xExpression).toBeUndefined(); + }); +}); diff --git a/packages/services/service-automation/src/builtin/map-node.ts b/packages/services/service-automation/src/builtin/map-node.ts index 845dcb6c53..9f92e52555 100644 --- a/packages/services/service-automation/src/builtin/map-node.ts +++ b/packages/services/service-automation/src/builtin/map-node.ts @@ -49,6 +49,25 @@ export function registerMapNode(engine: AutomationEngine, ctx: PluginContext): v // Each item's subflow may pause, so the map suspends and resumes per item. supportsPause: true, isAsync: true, + // Structured config form for the flow designer (ADR-0018). Mirrors the + // objectui hardcoded `map` field group field-for-field, so the online + // (schema-driven) form matches the offline one (objectui #2670 Phase 3 / + // #3304). `map` is the one previously-schemaless node whose fields are all + // scalars / typed references — no `keyValue` map, no virtual columns — so + // it maps cleanly through `jsonSchemaToFlowFields` with zero regression. + configSchema: { + type: 'object', + properties: { + // interpolate() single-brace `{items}` template, not bare CEL — same + // marker + rationale as loop.collection. + collection: { type: 'string', title: 'Collection', description: 'Expression resolving to the array to process, one item at a time.', xExpression: 'template' }, + flowName: { type: 'string', title: 'Per-item flow', description: 'Subflow run for each item — it may pause (e.g. an approval).', xRef: { kind: 'flow' } }, + iteratorVariable: { type: 'string', title: 'Item variable' }, + itemObject: { type: 'string', title: 'Item object', description: 'When items are records, the object they belong to (exposes each item as the child’s record).', xRef: { kind: 'object' } }, + outputVariable: { type: 'string', title: 'Output variable', description: 'Each item’s subflow output, collected in order.' }, + }, + required: ['collection', 'flowName'], + }, }), async execute(node, variables, context) { const cfg = (node.config ?? {}) as Record;