From ae437cdfa8124ceff5b38c4c2076bc5e97377655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Thu, 18 Jun 2026 17:57:28 +0800 Subject: [PATCH] fix(spec): declare extended Gantt config fields the renderer reads GanttConfigSchema only declared the 5 core timeline fields (no passthrough), so parentField/typeField/colorField/groupByField/tooltipFields/baseline*/ resourceView/assignee*/quickFilters/autoZoomToFilter were stripped by parse() at compile time and on the runtime /meta/view re-validation, degrading the Gantt to a flat list. Declare them explicitly so the renderer contract round-trips through the spec. --- .changeset/gantt-config-extended-fields.md | 17 +++++++++ packages/spec/src/ui/view.test.ts | 27 +++++++++++++++ packages/spec/src/ui/view.zod.ts | 40 ++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 .changeset/gantt-config-extended-fields.md diff --git a/.changeset/gantt-config-extended-fields.md b/.changeset/gantt-config-extended-fields.md new file mode 100644 index 0000000000..2907489c97 --- /dev/null +++ b/.changeset/gantt-config-extended-fields.md @@ -0,0 +1,17 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): declare the extended Gantt config fields the renderer actually reads + +`GanttConfigSchema` only declared the 5 core timeline fields as a plain +`z.object` (no passthrough), so every other field the Gantt renderer consumes — +`parentField`/`typeField` (two-level summary→step hierarchy), `colorField`, +`groupByField`, `tooltipFields`, `baselineStartField`/`baselineEndField`, +`resourceView`/`assigneeField`/`effortField`/`capacity`, `quickFilters`, +`autoZoomToFilter` — was silently stripped by `.parse()` on both the compile-time +protocol check and the runtime `GET /api/v1/meta/view/:object` re-validation. With +the keys gone before render, the Gantt degraded to a flat list (no parent/child +rows, no summary bars, no expand/collapse). These fields are now declared +explicitly (with descriptions), so the renderer contract round-trips through the +spec instead of requiring downstream patches. diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index 2b06d126c6..cb39c124f8 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -210,6 +210,33 @@ describe('GanttConfigSchema', () => { expect(() => GanttConfigSchema.parse(config)).not.toThrow(); }); + + it('should accept extended renderer fields (hierarchy, baseline, grouping, resource, tooltip, quick filters)', () => { + const config = { + startDateField: 'start_date', + endDateField: 'end_date', + titleField: 'name', + colorField: 'status', + parentField: 'parent_id', + typeField: 'row_type', + baselineStartField: 'planned_start', + baselineEndField: 'planned_end', + groupByField: 'workshop', + resourceView: true, + assigneeField: 'owner', + effortField: 'effort', + capacity: 8, + tooltipFields: ['owner', { field: 'effort', label: '工时' }], + quickFilters: [ + { field: 'status' }, + { field: 'category', label: '类别', options: ['A', { value: 1, label: 'B' }] }, + ], + autoZoomToFilter: false, + }; + + expect(() => GanttConfigSchema.parse(config)).not.toThrow(); + expect(GanttConfigSchema.parse(config)).toMatchObject({ parentField: 'parent_id', typeField: 'row_type' }); + }); }); describe('ListViewSchema', () => { diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 57e62aa730..8daffac9b6 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -378,8 +378,27 @@ export const CalendarConfigSchema = lazySchema(() => z.object({ colorField: z.string().optional(), })); +/** + * Quick filter dimension for the Gantt toolbar. + */ +export const GanttQuickFilterSchema = lazySchema(() => z.object({ + field: z.string().describe('Record field / dot-path the dimension filters on'), + label: z.string().optional().describe('Trigger label (falls back to the field label)'), + options: z.array(z.union([ + z.string(), + z.object({ + value: z.union([z.string(), z.number()]), + label: z.string().optional(), + }), + ])).optional().describe('Explicit option override for fixed enums'), +})); + /** * Gantt Settings + * + * Beyond the core timeline fields, the renderer supports a two-level + * parent/child hierarchy (parentField/typeField), planned-vs-actual baselines, + * dynamic grouping, a resource/workload view, hover tooltips and quick filters. */ export const GanttConfigSchema = lazySchema(() => z.object({ startDateField: z.string(), @@ -387,6 +406,27 @@ export const GanttConfigSchema = lazySchema(() => z.object({ titleField: z.string(), progressField: z.string().optional(), dependenciesField: z.string().optional(), + colorField: z.string().optional().describe('Field that drives the bar color'), + // Two-level hierarchy: a parent task id (summary bar) and a row type. + parentField: z.string().optional().describe('Field holding the parent task id (builds the summary → step tree)'), + typeField: z.string().optional().describe('Field whose value maps to task/summary/milestone'), + // Planned-vs-actual reference bars. + baselineStartField: z.string().optional().describe('Baseline (planned) start field'), + baselineEndField: z.string().optional().describe('Baseline (planned) end field'), + // Dynamic grouping: bucket leaf tasks under one synthesized summary per value. + groupByField: z.string().optional().describe('Field to group leaf tasks by (synthesized summary rows)'), + // Resource / workload view. + resourceView: z.boolean().optional().describe('Render a per-resource workload histogram instead of the timeline'), + assigneeField: z.string().optional().describe('Resource field to bucket load by (resource view)'), + effortField: z.string().optional().describe('Per-task load units (resource view; default 1)'), + capacity: z.number().optional().describe('Per-resource capacity ceiling; loads above this flag overload'), + // Hover tooltip + quick filters. + tooltipFields: z.array(z.union([ + z.string(), + z.object({ field: z.string(), label: z.string().optional() }), + ])).optional().describe('Fields to surface in the hover tooltip, in display order'), + quickFilters: z.array(GanttQuickFilterSchema).optional().describe('Multi-select filter dropdowns rendered above the chart'), + autoZoomToFilter: z.boolean().optional().describe('When true (default), filtering zooms the range to the filtered tasks'), })); /**