Skip to content

Kanban, gantt and timeline lenses over the same task data - #49

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses
Sep 1, 2026
Merged

os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses

Conversation

@os-warren

@os-warren os-warren commented Sep 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #12

Five new lenses over the existing rows — board, schedule, recent, by_unit on duly_task, and catalog_tree on duly_duty — each reachable from app navigation, plus test/views.test.ts.

Gates

All four green on 997782a, the final commit on this branch:

gate result
pnpm validate exit 0 — ✓ Validation passed; --json reports warnings: [], conversions: []
pnpm typecheck exit 0
pnpm test exit 0 — Tests 289 passed (289), Test Files 9 passed (9)
pnpm build exit 0 — ✓ Build complete

The zero-warning validate is the acceptance criterion, not a formality: with the gantt block deleted the same command reports view/layout-without-binding, which is how the reading was confirmed to be a real green rather than an absent check.

The views

board — kanban by status. Columns, their order and their labels come from duly_task.status's own options, so the board cannot drift from the object. Dragging a card writes one field: measured against the renderer's move handler, dataSource.update(object, id, { [groupBy]: toColumnId }) — status and nothing else, the same write the row action does. That write is not gated on inlineEdit; it is gated on update permission. inlineEdit: true is still declared, and it is not inert — the toolbar lets a viewer switch this view to its grid visualisation, and the adapter honours the flag on that branch.

schedule — gantt, visible_from → due_date, grouped by owner, week granularity. The span is the lead time. A filter requires both dates to be present, which is a correctness constraint rather than a scope narrowing: the renderer maps a missing date to new Date(), so an undated one-off would draw a zero-width bar on today and read as load that does not exist.

recent — timeline on last_update_at descending, coloured by status. last_update_at is hook-stamped on a status change or a note edit and deliberately does not advance on an administrative write, so this is "what has been happening", not "what has been touched".

by_unit — grid grouped by business_unit, carrying the bulkActionDefs from #41 unchanged. Those are the declarative data-plane write and were copied, not restructured; they are on this view because the adapter honours bulkActionDefs on the grid branch only.

catalog_tree — duties by business unit, then owner. Shipped as type: 'grid' + two-level grouping, not type: 'tree' — see the deviation below.

Navigation: board under My work; schedule / recent / by_unit under Team; catalog_tree under Setup. Every edit appends to an existing children array so #27 can add its entry beside these without a conflict.

board's placement is a product rule, not taste. Dragging a card writes status, and managers do not enter status — assigning is their only write. A drag-to-done surface in the manager's section invites exactly the write the model refuses them. test/views.test.ts pins the placement so it does not migrate.

Status colour

One source of truth: duly_task.status's options[].color. No view carries a colour map, and the test fails if one appears. Every lens that can name a colour source names status.

The gantt is the deliberate exception, and it inverts. Measured on @objectstack/console 17.2.0: the gantt renderer puts record[colorField] straight into backgroundColor, so colorField: 'status' sets background: "open" — invalid CSS, silently dropped, every bar identical. With the key absent the same renderer falls through to its status-derived palette and the bars separate by state. Declaring the documented key is strictly worse than omitting it. The omission is pinned by a test with the reasoning attached so nobody "fixes" it back, and it is filed upstream as objectstack-ai/objectstack#14110.

Deviation: catalog_tree is a grouped grid, not a tree view

The platform's tree view is a self-referencing hierarchy: TreeConfigSchema takes a parentField single-parent pointer and the renderer nests a record under another record of the same object by matching record[parentField] against sibling ids. duly_duty has no self-reference — its lookups point at sys_business_unit, duly_catalog_item and a user — so a type: 'tree' view here resolves no parent for any row, puts every duty at depth 0 and renders a flat table. Not empty: a complete, correct-looking table that never expands. plugin-tree never reads grouping, so declaring the two levels alongside it changes nothing, and no gate says so — view/layout-without-binding does not cover tree at all.

That is precisely the "renders wrong while authoring reports success" shape this card exists to prevent, so it was not shipped. The grid's grouping hook is the honest expression of the same idea and it is measured, not assumed: it recurses through grouping.fields building nested subgroups with per-level collapse — a real two-level hierarchy. Group keys sort by label, never by bucket size.

Filed upstream as objectstack-ai/objectstack#14109 with two suggested resolutions. When one lands, this view can become a real tree.

Platform gaps filed upstream

Six, all measured against this repo on @objectstack/cli 17.2.0 / @objectstack/console 17.2.0, each with the mutation confirmed on disk before the reading and the tree restored after:

upstream issue gap
objectstack-ai/objectstack#14106 view/layout-without-binding covers only kanban / calendar / gantt. timeline, tree and map have identical literal-default fallbacks in the renderer ('created_at', 'name', 'location') and no gate. Deleting this PR's timeline block leaves validate reporting warnings: [].
objectstack-ai/objectstack#14107 No field reference on a list view is resolved at author time — not columns, filter, sort, grouping, nor any binding block — and pnpm build, the publish gate, does not catch it either. gantt.startDateField: 'B1_no_such_field' builds clean and renders a blank chart.
objectstack-ai/objectstack#14108 An app nav viewName naming a view that does not exist silently opens the default view, keeping its authored label.
objectstack-ai/objectstack#14109 tree has no value-grouped mode, and misapplying it renders flat rather than empty — the catalog_tree deviation above.
objectstack-ai/objectstack#14110 gantt.colorField inverts, as described above. The same key resolves three different ways across plugin-timeline (authored option colours), plugin-calendar (stable hash) and plugin-gantt (raw value).
objectstack-ai/objectstack#14111 (finding) rowColor.colors is optional in the schema but required by the only renderer — rowColor: { field } alone is a parse-clean no-op. This one changed no code here; it is why no view authors rowColor.

The renderer measurements objectstack-ai/objectstack#14074 asked for were posted there rather than filed again.

test/views.test.ts

Two halves, kept apart in the file.

A stopgap for objectstack-ai/objectstack#14106, objectstack-ai/objectstack#14107 and objectstack-ai/objectstack#14108 — binding blocks present and complete for all six binding view types, every field reference on every view surface resolved against the object, every nav viewName resolved against the declared listViews. Written to be deleted when those land, not maintained — the same shape as test/flow-predicates.test.ts for objectstack-ai/objectstack#14089.

Product pins that outlive the platform gaps: the gantt starting at visible_from, the timeline ordering by last_update_at descending, one colour source for status, the gantt's deliberate absent colorField, nothing ordered or totalled by a count, no filter reaching for a derived flag, the board's nav placement.

Every one was reverse-verified — mutate the source, watch the guard turn red, restore:

mutation result
delete the timeline binding block 2 failed / 9 passed
gantt.startDateField → nonexistent field 2 failed / 9 passed
add colorField: 'status' to the gantt 1 failed / 10 passed
nav viewName typo 2 failed / 9 passed
grouping field → nonexistent field 1 failed / 10 passed
add a rowColor map 1 failed / 10 passed

Files

src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts. No other file touched — objectstack.config.ts untouched, bulkActionDefs from #41 unchanged, no changesets in this repo.


Generated by Claude Code

os-warren and others added 2 commits September 1, 2026 05:25
…y stopgap plus product pins

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kanban, gantt and timeline lenses over the same task data

1 participant