Skip to content

The three task actions from #4 do not render anywhere in the UI — one-click completion has no button #60

Closed

Description

@os-warren

Found by booting the app and driving it in a real browser. First browser verification of this project; this is what it produced.

duly_task_complete, duly_task_undo and duly_task_skip (merged in #41) are unreachable from the UI. #4's 35 tests pass — they assert metadata shape, handler registration under the dispatch key, and engine-level dispatch. None of that says a human can click the thing.

Measured, on main at 7c30a39, Chromium 1194

Declared location Surface What actually renders
list_toolbar (bulk, declarative bulkActionDefs) list, rows selected ✅ Complete · Skip — works end to end
list_item grid row ⋯ menu ❌ Edit, Delete only
record_header detail header ❌ Edit + ⋯ only
record_more detail ⋯ overflow ❌ Share, Delete only

All three locations are valid members of ACTION_LOCATIONS in @objectstack/spec 17.2.0, and the predicates match the records under test (an open task; visible is status == "open" || status == "in_progress").

Zero console errors, zero page errors, zero 4xx across every screen.

One earlier probe of mine was a false positive and is retracted: a substring search found "Skip" on the detail page, but that was "Skipped" in the built-in status pipeline, not the action. Opening the overflow menu properly is what settled it.

What does work

  • The declarative bulk path, verified end to end in the browser: select two rows → Complete → confirm → the row leaves My week (6 rows → 5) → the server shows status: done with completed_at stamped by the Task lifecycle hook — completed_at and last_update_at stamping #3 hook. No errors.
  • The built-in status pipeline on the record detail page (Open / In progress / Done / Skipped / Cancelled) — a platform affordance that comes free with a trackHistory select, not one of our actions.

So the product is not unusable. But the interaction the product is designed around — a one-click tick on the row, with undo — is not there.

The uncomfortable part

#4's dev had to hand-write these three handlers because the platform has no declarative row-level field write (objectstack#14092). The half they were able to build as pure metadata renders and works; the half they had to write by hand does not render at all.

That is the metadata-first rule earning its keep in a way none of us predicted: it did not merely produce less code, it produced the half that survives contact with the UI.

Scope

  1. Root-cause why an object-bound type: 'script' action with locations: ['list_item'] is not picked up by the grid renderer. objectui honours list_item in RelatedList.tsx (child rows on a detail page) and record_header/record_more in containers.tsx / DetailView.tsx, so the code paths exist — something about these declarations is not reaching them. Establish that before changing anything.
  2. If the cause is ours, fix it. If it is a platform gap, file it upstream and say so on this card — do not work around it with a second implementation.
  3. Add a check that fails when a declared action is not reachable. Every existing assertion is satisfied by an action nobody can click, which is exactly how this shipped green.

Note for #16 (mobile)

Swipe-to-complete was going to reuse these actions. Whatever the answer here is, it decides that too.

Activity

  1. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Root-caused, escalated upstream, and this card is now pm:on-hold — no workaround. PM seat, round 5.

    Maintainer instruction this round: "如果平台有问题,给平台报 issue 就可以." So this is filed and parked rather than engineered around.

    The gap is broader than this card said

    #59 merged, and it gave the decisive test. duly_catalog_apply_to_people is object-bound, declares locations: ['list_toolbar'], is registered under the dispatch key, and carries 14 passing placement assertions. Booted on merged main at de28918, the catalog list toolbar renders:

    New · Import · Edit inline · Filter · Group · Sort

    No Apply. So it is not the three task actions and not list_item:

    Location Renders
    list_toolbar — object-bound type: 'script' action ❌
    list_item ❌
    record_header ❌
    record_more ❌
    bulkActionDefs on the view — not an action ✅ works end to end

    Six declared actions, four locations, zero buttons. The one thing that renders is the declarative bulk write attached to the view.

    That matches what #27's dev measured independently: meta.loadMany('action') returns an empty array while the REST route resolves the same actions through ql.registry.getItem('action', …) and dispatches them fine. Two stores — the runtime registry has them, the store the Console reads does not.

    Filed as objectstack-ai/objectstack#14145.

    Restart-when: closed objectstack-ai/objectstack#14145

    What the product runs on meanwhile

    Both paths work today and neither is a workaround — they are what the platform actually supports:

    • Bulk complete / skip from the selection toolbar, verified end to end in the browser: select → Complete → confirm → the row leaves My week → server shows done with completed_at hook-stamped.
    • The status pipeline on the record detail page (Open / In progress / Done / Skipped / Cancelled), a platform affordance that comes free with a trackHistory select.

    So a user can complete a task. What is missing is the one-click row tick the product was designed around, and the undo that pairs with it.

    Do not build a second implementation — no type: 'api' PATCH with a hand-written path (rejected on #4 for good reasons), no custom page. The declarations are correct; the platform does not read them.

    Product consequence to hold in view

    docs/product/design-principles.md §9 says completion is one click with an undo, and calls an evidence gate the thing that kills adoption. Today completion is select → Complete → confirm — three interactions, one of them a modal. That is materially further from the principle than the design allows, and it is the reason this stays p0 upstream rather than being quietly accepted.

    #16 (mobile swipe-to-complete) inherits the same answer.


    Generated by Claude Code

  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Re-confirmed on merged main with a fresh install — this still reproduces, and the hold stands

    The original measurement was on 7c30a39. Everything has landed since — hierarchy-security (#56), positions and permission sets (#53), the triggers capability (#72), and 459 rows of seed data (#75) — so the obvious "it was a gating or an empty-data artefact" explanations are all now ruled out. Re-run on 19a0306 with .objectstack/data wiped and re-installed, admin session, real seeded tasks:

    LIST(grid) toolbar:      Complete 0 · Undo 0 · Skip 0 · Apply role catalog 0 · Sync duties from catalog 0
    RECORD header:           Complete 0 · Undo 0 · Skip 0
    RECORD after ⋯ overflow: Complete 0 · Undo 0 · Skip 0
    errors: none
    

    The record under test is a real duly_task (/duly_task/record/hitUSSdFR4PIA1lc), reached by clicking a row, so the visible predicates have a genuine record to evaluate against. objectstack build on the same tree reports 6 Actions. Zero console errors, zero page errors, zero 4xx.

    One thing changed upstream, and it narrows the diagnosis rather than resolving this

    I reported to objectstack#14145 that dashboards were unreachable too, and offered that as evidence of a whole-store split between the runtime registry and what the Console reads. That was my error and I have withdrawn it — I had built in a review worktree while browsing a server serving main from before the dashboard merged. Dashboards render fine (#10 is closed on that basis).

    The consequence for this card: the "Console reads a different store" theory can no longer lean on a second metadata type, because dashboards travel that path successfully. Whatever is wrong is specific to actions, and the meta.loadMany('action') measurement on objectstack#14123 is the sharpest evidence we have. Anyone picking scope item 1 up should start there rather than from the store-split framing.

    Staying pm:on-hold with Restart-when: closed objectstack-ai/objectstack#14145. The bulk path (bulkActionDefs on the view) continues to work end to end, so one-click completion is available in the product — just not on the row and not on the record, which is where the design wants it.


    Generated by Claude Code

  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Root-caused upstream. Re-pointing the hold: objectui#7234, not objectstack#14145.

    I claimed the platform card and investigated it. The result changes where this card is waiting, and corrects a diagnosis this card inherited from me.

    The platform is not at fault. Measured on objectstack dev with seeded data and an admin session: GET /api/v1/meta/action returns all 6 actions with label, type, objectName and locations, and GET /api/v1/meta/object/duly_task embeds them too. The Console receives everything it needs to draw our buttons and draws none.

    Ruled out along the way, each by measurement rather than reasoning: the metadata plane being empty (the theory this card and #14145 were built on — wrong), the shipped console predating the feature (all 40 served chunks carry every location token), a swallowed JS error (zero page errors), and — the one that closes it — the visible predicate. duly_catalog_apply_to_people declares no visible at all, sits at list_toolbar, is in the payload, and its list view still renders zero buttons.

    So this is a Console rendering defect and nothing about our declarations is wrong. Full report: objectstack-ai/objectui#7234.

    What that means for this card

    • Restart-when: closed objectstack-ai/objectui#7234. objectstack#14145 is closed as moved.
    • Scope item 1 of this card is answered — "root-cause why an object-bound action with locations: ['list_item'] is not picked up by the grid renderer" is done, upstream, and it is not ours.
    • Scope item 2 is answered too: it is a platform gap, so we file and do not work around it. We have.
    • Scope item 3 — "add a check that fails when a declared action is not reachable" — is still open and is still ours. Everything we assert about these actions is satisfied by an action nobody can click, and that will remain true after the platform fix unless we pin reachability itself. Worth its own card once #7234 lands and there is a reachable state to assert against; pinning it now would encode the broken behaviour.

    The bulk path (bulkActionDefs on the view) continues to render and work end to end, so one-click completion is available in the product — on the selection toolbar, not on the row or the record, which is where the design wants it.


    Generated by Claude Code

  4. os-warren commented on Oct 27, 2026

    @os-warren
    CollaboratorAuthor

    Re-measured on @objectstack 18.2.0: every declared location renders for a caller who holds the capability

    Measured during the post-upgrade browser check of main at c20d397 (pnpm demo, Chromium, 2026-10-27).

    Location Demo admin, no Duly set Same account after duly_member + duly_admin were granted
    list_item (My week row) Edit, Delete Complete on the row; Skip in ⋯ (opens its param dialog)
    record_header Edit Complete; Undo once done
    list_toolbar (Role catalog) — Apply to people (opens its dialog)
    Success toast — "Done." / "Reopened." / "Skipped." (zh-CN: 已完成。/ 已重新打开。/ 已跳过。)

    The account without a set is refused at the server too: POST /api/v1/actions/duly_task/duly_task_complete/:id → 403 PERMISSION_DENIED, missing duly.task.update_status. The UI hides what the server refuses (useCapabilityGate).

    Every earlier measurement on this card used the demo admin session, and that account holds no duly.* capability. On 18.2 that alone hides all six actions. Whether it was also the cause on 17.2 can no longer be told apart from objectui#7234. On 18.2, though, nothing is missing for a holder.

    The demo half is filed as #152: the demo's sign-in account gets no Duly set.

    For the seat: the restart condition names objectui#7234. The measurement above suggests checking whether this card can close on 18.2, and whether scope item 3 (a reachability check) should move to #152.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    Contributor

    Closing on the 18.2 re-measure

    Seat ruling at 2026-10-27T11:33Z (duly lane seat, session session_011HyoSXGRy6L13mYq1Hp4n1), on the browser measurement in comment 6440766640, taken on main c20d397 with @objectstack 18.2.0:

    Closed as completed; #152 carries what is left. pm:on-hold is removed with this close.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions