diff --git a/apps/e2e/src/tests/workflow-designer-roundtrip.test.ts b/apps/e2e/src/tests/workflow-designer-roundtrip.test.ts index d3b7641d..229b12a6 100644 --- a/apps/e2e/src/tests/workflow-designer-roundtrip.test.ts +++ b/apps/e2e/src/tests/workflow-designer-roundtrip.test.ts @@ -5,8 +5,9 @@ * * Opening a workflow in the designer and saving it keeps what the designer * does not edit — a pipe's timeout, the workflow's version, error handler, - * display name, and the trust its origin records. Saving used to drop all - * five: an imported workflow came back `authored`, renamed to its id. + * display name, the trust its origin records, and a retry policy beyond its + * attempts. Saving used to drop each: an imported workflow came back + * `authored`, renamed to its id, with the engine's default retry. * * Needs the web app at E2E_BASE_URL and workflow-server on :8081 with no * service token (loopback dev); skips when the engine is not there. @@ -30,7 +31,17 @@ const doc = (id: string, extra: Record = {}) => ({ { id: 'main', name: 'Main', - pipes: [{ id: 'start', type: 'source.triggerPayload', role: 'source', config: {}, timeoutMs: 1234 }], + pipes: [ + { + id: 'start', + type: 'source.triggerPayload', + role: 'source', + config: {}, + timeoutMs: 1234, + // Beyond the attempts the canvas edits: a save used to drop the rest. + retry: { attempts: 5, backoff: 'fixed', maxDelayMs: 900, jitter: false, on: 'all' }, + }, + ], edges: [], }, ], @@ -99,6 +110,7 @@ describe('workflow designer round trip', () => { errorHandler: { workflowId: HANDLER }, }); expect(after.pipelines[0].pipes[0].timeoutMs).toBe(1234); + expect(after.pipelines[0].pipes[0].retry).toEqual({ attempts: 5, backoff: 'fixed', maxDelayMs: 900, jitter: false, on: 'all' }); }); it('shows only the trigger sections that have a form, and no dead buttons', async (ctx) => { @@ -117,4 +129,17 @@ describe('workflow designer round trip', () => { ]); await driver.screenshot({ path: process.env.E2E_SHOT ?? '/tmp/workflow-trigger-dialog.png' }); }); + + it('keeps the section you are in while you type in a trigger', async (ctx) => { + // The dialog used to re-sync its selection from props in an effect; with + // the triggers among its dependencies, every keystroke snapped it back to + // General. Mounted only while open, it starts once and stays put. + if (!engineUp) ctx.skip(); + const section = driver.locator('[data-testid="workflow-trigger-config-section-input parameters"]'); + await section.click(); + await driver.locator(`${byTestId('workflow-trigger-config')} .monaco-editor .view-lines`).click(); + await driver.keyboard.type('{"region": "west"}'); + expect(await section.getAttribute('class')).toContain('active'); + expect(await driver.locator(`${byTestId('workflow-trigger-config')} .monaco-editor`).isVisible()).toBe(true); + }); }); diff --git a/apps/web/src/frontend/features/workflow/components/CredentialsPanel.tsx b/apps/web/src/frontend/features/workflow/components/CredentialsPanel.tsx index 52cff2ac..3f9a96d9 100644 --- a/apps/web/src/frontend/features/workflow/components/CredentialsPanel.tsx +++ b/apps/web/src/frontend/features/workflow/components/CredentialsPanel.tsx @@ -25,8 +25,8 @@ const _kindsMatch: SameMembers<(typeof KINDS)[number], CredentialKind> = true; const _sourcesMatch: SameMembers<(typeof SOURCES)[number], Exclude> = true; void [_kindsMatch, _sourcesMatch]; -type Kind = (typeof KINDS)[number]; -type Source = (typeof SOURCES)[number]; +type Kind = CredentialKind; +type Source = Exclude; const KIND_FIELDS: Record> = { database: [ diff --git a/apps/web/src/frontend/features/workflow/components/DelimitedSourceEditor.tsx b/apps/web/src/frontend/features/workflow/components/DelimitedSourceEditor.tsx index c289381c..c0bf0baa 100644 --- a/apps/web/src/frontend/features/workflow/components/DelimitedSourceEditor.tsx +++ b/apps/web/src/frontend/features/workflow/components/DelimitedSourceEditor.tsx @@ -12,10 +12,11 @@ import { compileZodSchema } from '../lib/zodSchema'; import type { ColumnCheck as EngineColumnCheck, ColumnCheckKind, + ColumnRule, ColumnType, FixedWidthColumn, } from '@foxschema/workflow-engine/definitions'; -import type { KnowsEveryField, SameMembers } from '../lib/engineShapes'; +import type { SameMembers } from '../lib/engineShapes'; /** * Config editor for the delimited file sources (`source.file.csv`, @@ -59,33 +60,14 @@ const COLUMN_TYPES = ['string', 'integer', 'number', 'boolean', 'date'] as const const CHECK_KINDS = ['null', 'length', 'format'] as const; const _columnTypesMatch: SameMembers<(typeof COLUMN_TYPES)[number], ColumnType> = true; const _checkKindsMatch: SameMembers<(typeof CHECK_KINDS)[number], ColumnCheckKind> = true; -const _checksKnown: KnowsEveryField = true; -const _fixedKnown: KnowsEveryField = true; -void [_columnTypesMatch, _checkKindsMatch, _checksKnown, _fixedKnown]; - -interface ColumnCheck { - pattern: string; - kind?: string; - message?: string; -} - -interface ColumnRuleFields { - type?: string; - /** Legacy single regex — migrated into `checks` in the editor when present. */ - pattern?: string; - checks?: ColumnCheck[]; - required?: boolean; -} +void [_columnTypesMatch, _checkKindsMatch]; -interface FixedColumn extends ColumnRuleFields { - name: string; - start: number; - length: number; -} - -interface FieldRule extends ColumnRuleFields { - name: string; -} +// The engine's own column shapes, type-only. Hand-kept copies here typed a +// column's `type` as any string, so a value the engine rejects compiled. +type ColumnCheck = EngineColumnCheck; +type ColumnRuleFields = Omit; +type FixedColumn = FixedWidthColumn; +type FieldRule = ColumnRule; const parseChecks = (value: unknown): ColumnCheck[] => { if (!Array.isArray(value)) return []; @@ -103,6 +85,10 @@ const parseChecks = (value: unknown): ColumnCheck[] => { }); }; +/** A column type the engine knows, or undefined — the engine refuses any other. */ +const asColumnType = (value: unknown): ColumnType | undefined => + (COLUMN_TYPES as readonly unknown[]).includes(value) ? (value as ColumnType) : undefined; + /** Normalize rule fields: fold legacy `pattern` into `checks` for editing. */ const ruleFields = (raw: ColumnRuleFields): ColumnRuleFields => { const checks = parseChecks(raw.checks); @@ -111,7 +97,7 @@ const ruleFields = (raw: ColumnRuleFields): ColumnRuleFields => { checks.unshift({ pattern: legacy }); } return { - ...(raw.type ? { type: String(raw.type) } : {}), + ...(asColumnType(raw.type) ? { type: asColumnType(raw.type) } : {}), ...(checks.length > 0 ? { checks } : {}), ...(raw.required ? { required: true } : {}), }; @@ -209,7 +195,7 @@ function ColumnRuleEditor({