You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 3739d8d
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec,lint): a kanban board chooses which lanes it draws, and in what order (kanban.lanes) (#28196)
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao7KgVSNdGtTtAQ3esq3Sog4)_Fixes#28018
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #28212
(skills/objectstack-ui/rules/list-views.md, to land with the renderer
half #28211)
## 维护者速读
- 改了什么:看板视图配置新增可选键 `kanban.lanes`(按顺序列出要画的泳道,取分组字段选项的存储值);`os validate`
/ `os build` / 运行时发布闸门会拒收不是该字段选项的泳道值和重复泳道。
- 为什么改:HotCRM 任务看板按状态分组、视图又过滤掉已完成任务,于是永远多一列空的「已完成」泳道,作者无法去掉它,也无法调整泳道顺序。
- 风险与回滚:纯新增可选键,不写默认值,已存的看板解析结果不变;新的两条 lint 规则只在写了 `lanes` 时触发。回滚即 revert
本 PR。控制台渲染器还没读这个键,在 ui 跟进卡落地前写了也仍是每个选项一列。
-
先看:`packages/lint/src/validate-list-view-field-refs.ts`(`checkKanbanLanes`
/ `checkKanbanLaneRepeats`
两段)、`packages/spec/src/ui/view.zod.ts`(`KanbanConfigSchema.lanes`)、`packages/lint/src/validate-list-view-kanban-lanes.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/lint exec vitest run
src/validate-list-view-kanban-lanes.test.ts`,预期 11 个测试全部通过(其中一条把
`inprogress` 判为不是选项,并提示 `Did you mean "in_progress"?`)。
- 需要你做的:无,席位落地。
## What changes
**Spec (`@objectstack/spec`, minor).** `KanbanConfigSchema` (a list
view's `kanban` block) gains one optional key, `lanes: string[]`: the
lanes the board draws, in this order, as stored option values of
`groupByField`.
- A choice of lanes, not a record filter: a record whose value is not
listed lands in the board's `Uncategorized` lane, as an off-lane record
already does. To keep records off the board, filter the view (which is
what the HotCRM task board already does).
- No spec-side default, so every stored board parses unchanged.
- Refused at parse: an empty list (`minItems`), and an entry not shaped
like a stored option value (each entry is `SystemIdentifierSchema`, the
grammar of `SelectOptionSchema.value`, so a label such as `In Progress`
is refused). Both project into the published JSON Schema exactly, so the
key adds no row to `dropped-refinements.baseline.json`.
- `liveness/view.json`: the `kanban` row notes `lanes` at planned
strength (the `viewMode`-on-gantt precedent): at this tree
`ui/packages/plugin-kanban/src/ObjectKanban.tsx#effectiveColumns` still
draws one lane per option and reads no lane list.
**Lint (`@objectstack/lint`, minor).** Spec cannot see the object, so
the object-aware half lives where `groupByField` is already judged
against the object, `validate-list-view-field-refs.ts` (the
reference-integrity suite `os validate`, `os build` and the runtime
publish gate run):
- `list-view-kanban-lane-unknown` (error): a lane that is not an inline
option of the group field, at `kanban.lanes[i]`, with the nearest option
as "Did you mean". A misspelled lane draws a lane no card can land in
while every card with the option it meant falls into `Uncategorized`.
- `list-view-kanban-lane-repeated` (error): a value listed twice, at the
second entry. Needs no field map, so it runs before every skip.
- Both ids exported from the package barrel and explained in `os
explain`.
**Alternative not taken: a `hideEmptyLanes: boolean`.** It hides a lane
whenever its data happens to be empty, so lanes appear and disappear as
records move and an empty lane stops being a drop target; and it cannot
order lanes. The ordered list is stable, says which lanes and in what
order, and closes the measured case by itself.
## Tests
- `packages/spec/src/ui/kanban-config-lanes.test.ts`: member on the bare
block and inside a list view (with an unknown-key control), order kept,
absence writes nothing, empty list refused (`too_small`), label refused
at its own index (`invalid_format`).
- `packages/lint/src/validate-list-view-kanban-lanes.test.ts`: the
HotCRM board shape is clean; a non-option lane is one error at its path
with the nearest option; a label is a non-option; a repeat is one error
at the second entry, with and without an object universe; picklist-bound
and boolean group fields are not judged; an unknown `groupByField` is
reported once at that key and its lanes are not judged; `validate` and
`build` fail; the runtime save door (reference-integrity suite) reports
both ids; both `os explain` entries exist and every verdict and fix is
one line of at most 200 characters.
## Acceptance notes
- **Duplicate check lives in lint, not in the schema.** A `superRefine`
uniqueness check on `lanes` would add a new dropped site to 16 entries
of the shrink-only `dropped-refinements.baseline.json` (measured: the
spec build refused it), and growing that ledger is a decision, not a
side effect. Keeping the duplicate verdict beside the membership verdict
leaves the key fully projectable.
- **Not judged by the membership rule:** a group field whose options
come from a shared `picklist` (its option set is the list plus every
`picklistExtensions` entry, which a stack snapshot need not carry
whole), and a group field with no inline options (boolean, lookup: their
lanes are values). Both are stated in the module docblock and the `os
explain` entry.
- **`packages/lint/src/index.ts`** gained the two id exports:
`rule-explanations.test.ts` requires every explained id to be a
barrel-exported constant.
- **UI half:** the console's lane builder reading `lanes` is the
follow-up card #28211. No `ui/**` edits here.
## Verification
All readings at head `ea4b11b5` (branch merged with `origin/main`
`412908cf`), objectstack-ai/objectstack.
<details><summary>Builds, package tests and typechecks</summary>
- `env -u OS_SKIP_DTS pnpm --filter @objectstack/spec build` then
`check:generated`: exit 0, all artifacts current (reference docs
regenerated in this PR; authorable surface `ui/KanbanConfig:lanes`
added).
- Workspace build `turbo run build` over every non-`ui`, non-docs
package: 77/77 tasks.
- `pnpm --filter @objectstack/lint test`: 179 files, 7650 passed, 5
skipped.
- `pnpm --filter @objectstack/spec exec vitest run src/ui
src/system/metadata-form-zod-reconciliation.test.ts
src/type-alias-convention.pin.test.ts`: 167 files, 5950 passed.
- `pnpm --filter @objectstack/lint typecheck` and `pnpm --filter
@objectstack/spec typecheck` (tsc plus test-typecheck legs): exit 0.
- New files on the final head: lint
`validate-list-view-kanban-lanes.test.ts` 11/11, spec
`kanban-config-lanes.test.ts` 5/5.
</details>
<details><summary>Ablation (lint rule, both verdicts)</summary>
Through `scripts/ablation-replace.mjs` (anchor hit x1, restored to the
HEAD blob `34aa7c35`, `git diff HEAD` empty after each leg):
- membership check short-circuited to `return;`:
`validate-list-view-kanban-lanes.test.ts` 3 failed / 8 passed.
- repeat check made to never fire: 3 failed / 8 passed.
</details>
<details><summary>Derived gates (dispatch-gates --commands, 104
families)</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 104 commands for the 12 changed
paths; every one ran with stdin closed and exited 0. Reconciliation:
`dispatch-gates --ran`: 104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN.
Local-only `check-dev-prereqs.mjs`: exit 1, the `ui/` packages are
unbuilt here (excluded from the local build; this diff touches no
`ui/**`). The full farm is CI's.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibxL3MYcTjfshb8eeK3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec): a kanban board can choose which lanes it draws, and in what order, `kanban.lanes`
7
+
8
+
Clause-②: yes (widening)
9
+
Metadata-protocol: added
10
+
11
+
A board grouped on a select field drew one lane per option, always, so a lane the view's own filter keeps empty (a task board that filters out completed tasks still drawing its Completed lane) could not be left off, and the lanes could not be put in any order but the options' own. `KanbanConfigSchema` (the list view's `kanban` block) gains one optional key:
12
+
13
+
-**`lanes: string[]`.** The lanes the board draws, in this order, as stored option values of the `groupByField` field (the option `value`, not its label). For example `lanes: ['not_started', 'in_progress', 'waiting']` on a task board grouped by `status`.
14
+
-**A choice of lanes, not a record filter.** A record whose value is not listed lands in the board's `Uncategorized` lane, as a record whose value matches no lane already does. To keep records off the board, filter the view.
15
+
-**Absent means one lane per option.** The key has no spec-side default, so a board stored before it existed parses to exactly the block it stored and draws the lanes it always did.
16
+
-**Refused at parse:** an empty list (omit the key instead), and an entry that is not shaped like a stored option value (a display label such as `'In Progress'`).
17
+
-**Refused against the object** (`@objectstack/lint`, so `os validate`, `os build` and the runtime publish gate): a lane that is not an option of the group field (`list-view-kanban-lane-unknown`, which names the nearest option), and a value listed twice (`list-view-kanban-lane-repeated`, at the second entry). A misspelled lane would otherwise draw a lane no card can land in while every card with the option it meant falls into `Uncategorized`. Judged against the group field's inline `options`; a field whose options come from a shared `picklist`, and a group field with no options (a boolean, a lookup), are not judged. `os explain list-view-kanban-lane-unknown` (and `-repeated`) carries the reasoning.
18
+
19
+
The Console renderer reads the key in a follow-up change; until then a board that declares it still draws one lane per option.
Copy file name to clipboardExpand all lines: content/docs/references/ui/view.mdx
+9-6Lines changed: 9 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -676,6 +676,7 @@ HTTP methods a view data source may request — the subset of `HttpMethod` witho
676
676
| Property | Type | Required | Description |
677
677
| :--- | :--- | :--- | :--- |
678
678
|**groupByField**|`string`| ✅ | Field to group columns by (usually status/select). NO leading or trailing whitespace: the board reads this name off every card verbatim, so a padded spelling collapses the whole board into one `Uncategorized` lane. |
679
+
|**lanes**|`string[]`| optional | The lanes the board draws, in this order: stored option values of the groupByField field (the option `value`, not its label). Omit to draw one lane per option in the options' own order. A record whose value is not listed is not hidden: it lands in the `Uncategorized` lane, as a record whose value matches no lane does. To keep records off the board, filter the view. List each value once. |
679
680
|**summarizeField**|`string`| optional | Field to sum at top of column (e.g. amount) |
680
681
|**titleField**|`string`| optional | Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
681
682
|**columns**|`string[]`| ✅ | Fields to show on cards |
|**groupByField**|`string`| ✅ | Field to group columns by (usually status/select). NO leading or trailing whitespace: the board reads this name off every card verbatim, so a padded spelling collapses the whole board into one `Uncategorized` lane. |
906
+
|**lanes**|`string[]`| optional | The lanes the board draws, in this order: stored option values of the groupByField field (the option `value`, not its label). Omit to draw one lane per option in the options' own order. A record whose value is not listed is not hidden: it lands in the `Uncategorized` lane, as a record whose value matches no lane does. To keep records off the board, filter the view. List each value once. |
905
907
|**summarizeField**|`string`| optional | Field to sum at top of column (e.g. amount) |
906
908
|**titleField**|`string`| optional | Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
907
909
|**columns**|`string[]`| ✅ | Fields to show on cards |
|**groupByField**|`string`| ✅ | Field to group columns by (usually status/select). NO leading or trailing whitespace: the board reads this name off every card verbatim, so a padded spelling collapses the whole board into one `Uncategorized` lane. |
1276
+
|**lanes**|`string[]`| optional | The lanes the board draws, in this order: stored option values of the groupByField field (the option `value`, not its label). Omit to draw one lane per option in the options' own order. A record whose value is not listed is not hidden: it lands in the `Uncategorized` lane, as a record whose value matches no lane does. To keep records off the board, filter the view. List each value once. |
1274
1277
|**summarizeField**|`string`| optional | Field to sum at top of column (e.g. amount) |
1275
1278
|**titleField**|`string`| optional | Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
1276
1279
|**columns**|`string[]`| ✅ | Fields to show on cards |
@@ -1730,7 +1733,7 @@ Tab configuration for multi-tab view interface
0 commit comments