Skip to content

Commit d2e2f6f

Browse files
feat(spec): blueprint dashboard widgets say how they aggregate, and calendar views which date field they schedule on (#30484)
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K <!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBUrCRf8HiH2JEanDW9gVbCZ)_ Fixes #30430 Ruling-ref: objectstack-ai/cloud#3607 (row 2 A, record 6656298548; the calendar key B) Clause-②: yes (widening: an optional `aggregate` on the blueprint dashboard widget and an optional `startDateField` on the blueprint view) Metadata-protocol: added Skills-docs: none owed — no skill, eval or doc teaches a blueprint widget or view key (`git grep -n "BlueprintDashboard\|BlueprintView\|measure:\"count\"\|propose_blueprint" -- skills content/docs ':!content/docs/references' ':!content/docs/releases'`: 1 hit, `content/docs/frontend/plugins/plugin-chatbot.mdx:494`, which describes the plan card's `planCard` result and teaches no widget or view key; control `git grep -c "solution-blueprint" -- skills/objectstack-ai/references/_index.md`: 1, an index pointer to the schema file). The generated reference `content/docs/references/ai/solution-blueprint.mdx` is regenerated here. The skills lane is paused; nothing is owed to it from this PR. Prior rulings read: blueprint,aggregate,startdatefield → 17 hits; ADR-0021 D3, ADR-0033 Decision §10, ADR-0033 Decision §5, ADR-0033 Decision §6, ADR-0033 Decision §8, ADR-0045 Decision §1, ADR-0045 Decision §4, ADR-0045 Decision §8, ADR-0060 D6; thread: none; repo: objectstack-ai/objectstack Carried: objectstack-ai/cloud#3607's cloud half takes two acceptance items from this PR. (1) A widget with `aggregate` `sum`, `avg`, `min` or `max` whose `measure` is absent, `"count"` or names a non-numeric field is refused by the compile for that widget, naming it, and is never summed, averaged or ranked as a record count. (2) `startDateField` is read on a `calendar` view only and ignored on every other view type; on a calendar view it binds the calendar's `calendar.startDateField`, and a value that names no date or datetime field of the view's object is refused naming the field, never silently replaced by the first declared date field. ## 维护者速读 - 改了什么:AI 蓝图的仪表盘组件新增可选 `aggregate`(`sum` / `avg` / `min` / `max` / `count`),蓝图视图新增可选 `startDateField`(日历视图按哪个日期字段排程,对应建出视图的 `calendar.startDateField`)。宽松 schema 与给模型用的严格镜像两侧同时加,严格侧可为 null。`measure` 的说明改为「只写字段,怎么聚合看 `aggregate`」,删掉了「按字段类型自动选」和「从标题推断」两句,并标明 `measure: "count"` 是旧写法、应写 `aggregate: "count"`。 - 为什么改:执行 cloud#3607 两条裁决(行 2 选 A、日历键选 B)。原来组件只能写 `measure`,平台按字段类型自动聚合,数字字段一律求和,「平均面试评分」显示成 12 个分数之和 908;日历视图也无处写日期字段。 - 风险与回滚:纯增可选键,旧蓝图原样解析;严格镜像只是模型的结构化输出契约,模型此后每个组件 / 视图都要给出这两个键(可为 null)。本仓无读者,读者是 cloud 半(cloud#3607 跟进)。回滚即还原本 PR。 - 先看:`packages/spec/src/ai/solution-blueprint.zod.ts` 顶部新增的 `WIDGET_AGGREGATE_MEANING` / `VIEW_START_DATE_FIELD_RULE` 两段说明文字(模型读的就是它们),以及组件 `measure` / `aggregate` 两行;`packages/spec/src/ai/solution-blueprint.test.ts` 末尾新增的 describe 块。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/ai/solution-blueprint.test.ts`,预期 85 例全绿(含五个聚合值两侧都收、未知值两侧都拒、非日历视图上的 `startDateField` 被接受而不读)。 - 需要你做的:无,席位落地。 ## What changes - **`@objectstack/spec` (`packages/spec/src/ai/solution-blueprint.zod.ts`).** - Widget `aggregate: 'sum' | 'avg' | 'min' | 'max' | 'count'`: `.optional()` on the lenient `BlueprintDashboardSchema` widget, `.nullable()` (key required) on the strict mirror `SolutionBlueprintStrictSchema.dashboards[].widgets[]`. Both sides spell one module-local tuple `BLUEPRINT_WIDGET_AGGREGATES` and read one meaning sentence `WIDGET_AGGREGATE_MEANING`, so the two describes cannot drift. - Widget `measure`: both describes rewritten through one shared `WIDGET_MEASURE_RULE`. Removed: "The aggregation is chosen automatically from the field type", "Omit to let the builder infer from the title" (lenient) and "or null to infer from the title" (strict). Kept: name the FIELD, not `total_amount`. `measure: "count"` is now marked as the legacy spelling of a record count, with `aggregate: "count"` the one to write, and the describe says what a reader does with `measure: "count"` beside `sum`/`avg`/`min`/`max` (refuses the widget). - View `startDateField`: a snake_case identifier, `.optional()` on `BlueprintViewSchema`, `strictIdentOrNull` on the strict view (so the VALUE-parity walker sees the same `SNAKE_CASE` pattern on both sides). One shared describe, `VIEW_START_DATE_FIELD_RULE`. - **Regenerated:** `packages/spec/authorable-surface/ai.json` (+`ai/BlueprintView:startDateField`; the widget is a nested shape under `ai/BlueprintDashboard:widgets`, so `aggregate` adds no row) and `content/docs/references/ai/solution-blueprint.mdx`. `api-surface/` is unchanged: no export was added (the tuple and the three describe constants are module-local). - **Changeset:** `@objectstack/spec` minor, with both declaration lines. ### The `count` rule `aggregate: 'count'` counts the records the widget covers (after its `condition`) and needs no `measure` field. A field named in `measure` beside it is **not read**: the count is of records, not of a field's non-empty values. `measure: "count"` is the legacy spelling of a record count and still means it; the describe marks it legacy and says to write `aggregate: "count"`. Beside `sum`, `avg`, `min` or `max`, `measure: "count"` names no field to aggregate, and the describe says a reader must refuse that widget, never sum, average or rank a record count. `aggregate: 'sum' | 'avg' | 'min' | 'max'` need `measure` to name a numeric field of the widget's object; the describe says so. With `measure` absent or `"count"` there is no field to aggregate, and **the spec does not refuse that pairing**. Reason: the strict mirror the design model generates against is per-leaf JSON Schema and cannot state a cross-key rule, so a refusal on the lenient side alone would make a proposal legal to generate and illegal to apply, which is the cloud#1967 failure this file already documents (an approved blueprint refused wholesale at apply). A lenient-side `.superRefine` would also leave the published JSON Schema without the rule (a dropped refinement). The reader, cloud#3607's compile ("the compile requires it"), is where the pairing is judged per widget. ### The non-calendar choice: ignored, not refused `startDateField` is read on a `calendar` view and **ignored** on `list`, `form`, `kanban`, `gallery` and `gantt`. Reasons: (1) the same strict-mirror limit as above, since a type-scoped refusal is a cross-key rule; (2) the view already has a kind-scoped key that other kinds ignore, `groupBy` ("REQUIRED for kanban … Null for list/form/calendar/gallery"), so this follows the shape's own precedent. The describe states it ("Ignored on the other view types (a gantt view takes its dates from `columns`, start before end)"), and the strict describe adds "Null on every view that is not a calendar." The key name is the target's own leaf, `calendar.startDateField`, flattened as `groupBy` is. ### Liveness `packages/spec/liveness/` does **not** govern these keys, so no row is added. The ledger's denominator is the registered metadata kinds plus `listUnregisteredKindSchemaTypes()` and the `SPEC_ONLY_SCHEMAS` overrides (`packages/spec/liveness/README.md` § Source of truth); the solution blueprint is none of them (`grep -il blueprint packages/spec/liveness/*.json`: 0 files). It is the AI design tool's I/O contract, not stored metadata. The consumer is cloud: objectstack-ai/cloud#3607's cloud half (the designer emits both keys, the compile reads them). ### Readers in this repo None. `git grep -n "BlueprintDashboard\|BlueprintView\|solution-blueprint" packages` outside the schema file hits only its test, `ai/index.ts` (re-export), generated ledgers, CHANGELOGs and one lint test that reads the strict mirror's object shape (`packages/lint/src/validate-expressions.formula-one-hop.test.ts:128`, not the view or widget). ## Tests `packages/spec/src/ai/solution-blueprint.test.ts`: - New `describe('blueprint widget \`aggregate\` and calendar view \`startDateField\`')`: each of the five values accepted on both sides; `count` with no `measure` on both sides; `average`, `median`, `count_distinct`, `AVG` and `''` refused on both sides with exactly one issue at `dashboards.0.widgets.0.aggregate`; absent parses on the lenient side, `null` parses and a missing key is refused on the strict side; no `measure` / `aggregate` describe on either side says "infer", "chosen automatically" or "title"; `startDateField` on a calendar view accepted on both sides and absent still parses; `42`, `true`, an array, an object and `'Interview Date'` refused at the key on both sides, and a missing key refused on the strict side; the key accepted on each of the five non-calendar types on both sides. - Key parity: a new `the DASHBOARD and WIDGET schemas carry exactly the same keys` pin (no widget-level parity pin existed), with the `aggregate` enum's options compared across sides; the VIEW pin now names `startDateField`. - Existing strict fixtures gain the two required keys (`aggregate`, `startDateField`); the VALUE-parity test is unchanged and green with both new keys on both sides. ## Verification Head `e16fea95` (branch off `origin/main` `c5800818`; review round 1 changed only the `measure` / `aggregate` describes, the no-title-inference pin and the regenerated reference page), under `os-verify-lock`, `NODE_OPTIONS=--max-old-space-size=4096`: - `pnpm build` (full turbo build): Tasks 119 successful, 119 total, VERDICT command-exit 0. - `pnpm --filter @objectstack/spec test`: Test Files 814 passed (814), Tests 25610 passed, 1 todo; `pnpm --filter @objectstack/spec typecheck`: exit 0. - `src/ai/solution-blueprint.test.ts` alone: 85 passed (85) at `e16fea95`. The reverse verification below ran at `4960c1d3`; round 1 touched neither ablated line. - `check-changeset-no-major` with this body as the event: exit 0 on `Metadata-protocol: added`; the control with `none` exits 1 (the authorable-surface baseline moved). <details><summary>Reverse verification (two mutations through scripts/ablation-replace.mjs, each restored to the HEAD blob 1d11f8e)</summary> 1. The strict widget key renamed (`aggregate: z.enum(BLUEPRINT_WIDGET_AGGREGATES).nullable()` → `aggregate_ablated: …`, anchor 1 → 0, blob `1d11f8e14420` → `82c3c8c15327`): **11 failed | 74 passed (85)**: the widget key-parity pin, the five accept cases, the count, unknown-value, absent/null/missing and no-title-inference pins, and the existing strict widget fixture. Restored: blob == HEAD, `git diff HEAD` empty. 2. The lenient view key renamed (`startDateField: z.string().regex(SNAKE_CASE).optional()` → `startDateField_ablated: …`, blob `1d11f8e14420` → `45bbf04c1b37`): **3 failed | 82 passed (85)**: the VIEW key-parity pin, the calendar accept pin and the wrong-type refusal pin. The non-calendar pin stays green, as expected (an unknown key is stripped, so the view still parses). Restored: blob == HEAD, `git diff HEAD` empty. The spec tests import the schema from `src`, so no rebuild was needed for either leg. </details> <details><summary>Gates: dispatch-gates --commands, 102 derived at e16fea9, 102 run, 0 NOT-MEASURED</summary> - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran gates-ran.list`: "102 derived famil(ies) accounted for — 102 run, 0 NOT-MEASURED", exit 0. Every line `exit 0`, including `check-changeset-no-major`, `check:authorable-surface`, `check:api-surface`, `check:generated`, `check:liveness`, `check:empty-state`, `check:docs`, `check:llms-txt`, `check:spec-parsed-alias`, `check:dual-build-cjs-loads` and `check:nul-bytes`. - At `4960c1d3` one prerequisite exited 3: `check-plugin-teardown-shape --self-test` on the shallow clone (its pinned fixture commit `621a4876` absent); after `git fetch origin 621a487…` it exits 0 (48 cases). At `e16fea95` all 102 exited 0 on the first run. - No `!` in the title and no `(narrowing)` arm: no full dogfood run. CI owns the rest of the farm. </details> ## Acceptance notes - Absent `aggregate` leaves the reader's own default in place. Neither describe states what that default is (lenient: "the reader falls back to a default of its own, which may not be the average or extreme a widget means"; strict: "or null for the reader's own default"), because this repo cannot read cloud's default table. Both tell the model to set the key on every widget that aggregates. - Prior rulings: ADR-0021 D3 (dashboard widgets pick dataset measures by name) governs the runtime dashboard, not the blueprint; ADR-0033 Decision §5 names the blueprint and rules nothing on widget aggregation or a view date key. The rest of the hits are blueprint-process decisions (confirm, iterate, open-core boundary, additive materialization). - `count_distinct` (in `AggregationMetricType` and ADR-0021 dataset measures) is not offered: the ruling names five values. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic (an enum tuple, describe strings and schemas). - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (CI-owned run). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block excepted, per project convention). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzhYPN1JqnojTxfq2Pc --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 86ae2ca commit d2e2f6f

5 files changed

Lines changed: 220 additions & 10 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): a blueprint dashboard widget says how it aggregates, and a blueprint calendar view says which date field it schedules on
6+
7+
Clause-②: yes (widening: an optional `aggregate` on the blueprint dashboard widget and an optional `startDateField` on the blueprint view)
8+
Metadata-protocol: added
9+
10+
- **Widget `aggregate`.** `BlueprintDashboardSchema` widgets and the strict mirror `SolutionBlueprintStrictSchema.dashboards[].widgets[]` gain `aggregate: 'sum' | 'avg' | 'min' | 'max' | 'count'` (optional on the lenient side, required-but-nullable on the mirror). Before this, a widget could only name its `measure` and the reader picked the aggregation from the field type, so a widget meant to average a number field summed it. `sum`, `avg`, `min` and `max` aggregate the `measure` field; `count` counts records and needs no `measure` field (a field named beside it is not read). `measure`'s describe no longer says the aggregation is automatic, and neither side tells the model to infer anything from the widget title.
11+
- **View `startDateField`.** `BlueprintViewSchema` and the strict mirror's `views[]` gain `startDateField`, a snake_case field name, carried into the built view as `calendar.startDateField`. It is read on a `calendar` view and ignored on the other view types, not refused: the strict mirror the design model writes through cannot state a cross-key rule, so a refusal on the lenient side alone would refuse at apply a proposal the model was allowed to generate.
12+
- Both keys are optional on the lenient schema, so every existing blueprint parses unchanged. The strict mirror is only the design model's structured-output contract; a model generating against it now states both keys, `null` when it has nothing to say.

‎content/docs/references/ai/solution-blueprint.mdx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ const result = BlueprintAppSchema.parse(data);
9191
| **title** | `string` | optional | Widget title |
9292
| **object** | `string` | optional | Source object for the widget |
9393
| **chart** | `Enum<'metric' \| 'bar' \| 'line' \| 'pie' \| 'table'>` | optional | Widget visualization |
94-
| **measure** | `string` | optional | The field this widget aggregates (e.g. "amount", "probability"), or "count" to count records. The aggregation is chosen automatically from the field type — a money field SUMs, a percentage/rate AVERAGEs — so name the FIELD, not "total_amount". A "total revenue" widget sets measure:"amount"; an "average win rate" widget sets measure:"win_rate"; a "number of deals" widget sets measure:"count". Omit to let the builder infer from the title. |
94+
| **measure** | `string` | optional | The field this widget aggregates (e.g. "amount", "score"), or "count" to count records. Name the FIELD, not "total_amount" or "avg_score": how it is aggregated is what `aggregate` says, never this key. A "total revenue" widget sets measure:"amount" with aggregate:"sum"; an "average interview score" widget sets measure:"score" with aggregate:"avg"; a "number of deals" widget sets aggregate:"count" and needs no measure. measure:"count" is the legacy spelling of a record count: write aggregate:"count" instead. Beside "sum", "avg", "min" or "max", measure:"count" names no field to aggregate, so that widget is incomplete: a reader must refuse it, never sum, average or rank a record count. Omit for a count widget. |
95+
| **aggregate** | `Enum<'sum' \| 'avg' \| 'min' \| 'max' \| 'count'>` | optional | How the widget aggregates its records: "sum" = the total of the `measure` field; "avg" = its average; "min" / "max" = its smallest / largest value; "count" = the number of records the widget covers (after its `condition`). "sum", "avg", "min" and "max" need `measure` to name a numeric field of the widget's object. "count" needs no `measure` field: it counts records, and a field named in `measure` beside it is not read. Say what the widget computes: an average → "avg", a total → "sum", a highest / lowest value → "max" / "min", a number of records → "count". SET it on every widget that aggregates; omitted, the reader falls back to a default of its own, which may not be the average or extreme a widget means. |
9596
| **groupBy** | `string` | optional | The field to break the widget down by — the category or time axis (e.g. "stage", "created_at"). A "by status" chart MUST set this to the status field; the title and this field MUST name the SAME field. Omit for a single-number metric. |
9697
| **condition** | `{ field: string; op: Enum<'lt' \| 'lte' \| 'gt' \| 'gte' \| 'eq' \| 'ne'>; value: number \| string \| boolean }` | optional | Restrict WHICH records the widget counts/aggregates when its title implies a threshold or status (e.g. "stock below 10" → `{field:"stock_quantity", op:"lt", value:10}`; "open tickets" → `{field:"status", op:"eq", value:"open"}`). Without it the widget covers ALL records — so a "低于10的备件预警" / "overdue" card would wrongly count everything. Omit when the widget genuinely spans every record. |
9798

@@ -282,6 +283,7 @@ const result = BlueprintAppSchema.parse(data);
282283
| **groupBy** | `string` | optional | REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list. Optional for gantt (groups leaf tasks into summary rows). |
283284
| **filters** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Which records this view shows: the list-view filter rules [`{ field, operator, value }`] the built view carries, ANDed. REQUIRED whenever the view's name or label states a condition — an 「overdue / 逾期」, 「my open / 我的未完成」, 「待审批」 or 「本月」 view MUST carry the rules that make its name true, e.g. overdue → [`{ field: "due_date", operator: "less_than", value: "{today}" }`, `{ field: "status", operator: "not_equals", value: "completed" }`]; my open → [`{ field: "owner", operator: "equals", value: "{current_user_id}" }`, `{ field: "status", operator: "equals", value: "open" }`]. WITHOUT it the view shows EVERY record under a name that promises a subset. For a select field compare its option VALUE, never its label. Omit for a view that genuinely spans every record. |
284285
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Default record order: [`{ field, order }`] with order "asc" or "desc"; the first entry sorts first (e.g. an overdue list → [`{ field: "due_date", order: "asc" }`]). Omit for the platform default order. |
286+
| **startDateField** | `string` | optional | For type:"calendar" only: the date or datetime field of THIS view's object that places each record on the calendar (e.g. "interview_date", "due_date"), carried into the built view as `calendar.startDateField`. snake_case, naming one of the object's own fields. SET it on every calendar view; without it the builder binds a date field of its own choosing, which may not be the one meant. Ignored on the other view types (a gantt view takes its dates from `columns`, start before end). |
285287

286288
### Nested Shape: `BlueprintView.filters[number]`
287289

@@ -357,6 +359,7 @@ Sort field and direction pair
357359
| **groupBy** | `string` | optional | REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list. Optional for gantt (groups leaf tasks into summary rows). |
358360
| **filters** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Which records this view shows: the list-view filter rules [`{ field, operator, value }`] the built view carries, ANDed. REQUIRED whenever the view's name or label states a condition — an 「overdue / 逾期」, 「my open / 我的未完成」, 「待审批」 or 「本月」 view MUST carry the rules that make its name true, e.g. overdue → [`{ field: "due_date", operator: "less_than", value: "{today}" }`, `{ field: "status", operator: "not_equals", value: "completed" }`]; my open → [`{ field: "owner", operator: "equals", value: "{current_user_id}" }`, `{ field: "status", operator: "equals", value: "open" }`]. WITHOUT it the view shows EVERY record under a name that promises a subset. For a select field compare its option VALUE, never its label. Omit for a view that genuinely spans every record. |
359361
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Default record order: [`{ field, order }`] with order "asc" or "desc"; the first entry sorts first (e.g. an overdue list → [`{ field: "due_date", order: "asc" }`]). Omit for the platform default order. |
362+
| **startDateField** | `string` | optional | For type:"calendar" only: the date or datetime field of THIS view's object that places each record on the calendar (e.g. "interview_date", "due_date"), carried into the built view as `calendar.startDateField`. snake_case, naming one of the object's own fields. SET it on every calendar view; without it the builder binds a date field of its own choosing, which may not be the one meant. Ignored on the other view types (a gantt view takes its dates from `columns`, start before end). |
360363

361364
### Nested Shape: `SolutionBlueprint.dashboards[number]`
362365

@@ -423,6 +426,7 @@ Sort field and direction pair
423426
| **groupBy** | `string \| null` | ✅ | REQUIRED for kanban: the select/status field whose options become the board columns (e.g. "stage"). Optional for gantt (groups leaf tasks). Null for list/form/calendar/gallery. |
424427
| **filters** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value: string \| number \| boolean \| (string \| number)[] \| null }[] \| null` | ✅ | Which records this view shows: list-view filter rules [`{field, operator, value}`], ANDed, or null for a view over EVERY record. REQUIRED whenever the view's name or label states a condition — an 「overdue / 逾期」, 「my open / 我的未完成」, 「待审批」 or 「本月」 view MUST carry the rules that make its name true, e.g. overdue → [`{field:"due_date",operator:"less_than",value:"{today}"}`,`{field:"status",operator:"not_equals",value:"completed"}`]; my open → [`{field:"owner",operator:"equals",value:"{current_user_id}"}`,`{field:"status",operator:"equals",value:"open"}`]. Null under such a name ships a view that shows every record while its name promises a subset. |
425428
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[] \| null` | ✅ | Default record order: [`{field, order}`] with order "asc" or "desc"; the first entry sorts first (e.g. an overdue list → [`{field:"due_date",order:"asc"}`]), or null for the platform default order. |
429+
| **startDateField** | `string \| null` | ✅ | For type:"calendar" only: the date or datetime field of THIS view's object that places each record on the calendar (e.g. "interview_date", "due_date"), carried into the built view as `calendar.startDateField`. snake_case, naming one of the object's own fields. SET it on every calendar view; without it the builder binds a date field of its own choosing, which may not be the one meant. Ignored on the other view types (a gantt view takes its dates from `columns`, start before end). Null on every view that is not a calendar. |
426430

427431
### Nested Shape: `SolutionBlueprintStrict.dashboards[number]`
428432

‎packages/spec/authorable-surface/ai.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"ai/BlueprintView:name",
8282
"ai/BlueprintView:object",
8383
"ai/BlueprintView:sort",
84+
"ai/BlueprintView:startDateField",
8485
"ai/BlueprintView:type",
8586
"ai/BlueprintWidgetCondition:field",
8687
"ai/BlueprintWidgetCondition:op",

0 commit comments

Comments
 (0)