Skip to content

feat(lists): saved list views — shared + personal views, forking, defaults (G40) #81

Closed

Description

@Adron

Surfaced by the 2026-09-14 coverage re-measure. No previous issue or planning-doc revision mentions this feature at all — it was missed by every prior parity sweep.

The gap

Five live operations the client does not build. All are x-auth-type: sync-token (Bearer-reachable) and x-subscription-tier: free, so this is buildable today and gates on nothing.

Operation What the spec says
GET /api/lists/{id}/views "Every shared view on the list, plus this user's own personal views."
POST /api/lists/{id}/views Creates a saved view — {name, scope, config, isDefault}
POST /api/lists/{id}/views/{viewId} "Forks a shared view into a personal copy owned by the caller — the escape hatch."
PUT /api/lists/{id}/views/{viewId} Updates a view's name, config or default flag
DELETE /api/lists/{id}/views/{viewId} Removes a view

Why it matters

The shape implies a real collaboration model, not a cosmetic preference:

  • shared views belong to the list and everyone with access sees them
  • personal views belong to the caller
  • fork exists as a deliberate "escape hatch" — you can take someone else's shared view and make it yours rather than being stuck with how the list owner arranged it
  • isDefault is per-user, so two people can open the same list and correctly see different default arrangements

macOS currently has one hard-coded arrangement per list. On a shared list the owner's choices are simply not visible to collaborators.

⚠️ Probe before modelling — config is a string

config is declared {"type": "string"}, not an object, so the schema says nothing about its grammar. It presumably encodes column order / visibility / sort / filter, but that is an inference.

Capture a real saved view from the web app before writing any decoder. This repo has a documented history of exactly this failure — G21 link metadata (nested metadata decoded to all-nil, so every timeline link rendered host-only) and G25 org members (OrganizationMemberDTO required userId where live rows key by id). Both had green tests against fabricated fixtures. Do not add a third.

Also worth confirming: whether scope is the shared/personal discriminator and what its token set is.

Sequencing

Additive — nothing currently shipped is wrong without it. Suggest after the account cluster (#44 → #47 → #46 → #45).

Acceptance

  • Views listed, created, updated, deleted, and forked from macOS.
  • Shared vs personal is visually distinct; the fork affordance is discoverable.
  • isDefault is honoured per user on list open.
  • config is modelled against a captured payload, with a contract test that fails on a renamed key rather than degrading to nil.
  • BDD quartet across Kit / Domain / App.

Recorded as G40 in work-consolidation.md §1f.

Activity

  1. added
    enhancementNew feature or request
    parityWeb-parity gap with the InterlinedList web app
    on Sep 14, 2026
  2. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    Implemented in PR #105 (G40). Work continues on the PR.

    All five routes, built against captured live payloads rather than the OpenAPI document — which mattered, because the spec is wrong in two places and both were caught:

    1. createdAt/updatedAt are marked required in the ListView schema and absent from every live response. A strict decoder mirroring the spec would fail on every row.
    2. The spec's response example shows config.filters: [{"key":"read","op":"eq","value":false}]. That filter was sent live and dropped to []. There is a test recording it, so nobody builds a fixture from that example.

    The eight live keys decode non-optionally, so a renamed key fails loudly rather than degrading to nil — this issue's explicit acceptance criterion, and the G21/G25 failure mode.

    ⚠️ One premise in this issue is disproved

    "It presumably encodes column order / visibility / sort / filter"

    It does not. config is a server-normalized whitelist of exactly four keys — mode, density, filters, search. Ten column/sort keys were sent and silently stripped without error. Worth correcting in work-consolidation.md §1f when the doc track updates G40.

    The validation asymmetry is encoded in the types: scope is a closed personal|shared enum because the server hard-fails an unknown token (400 scope must be "personal" or "shared"), while mode and density carry .unknown(String) because the server silently defaults — so an unrecognised value is a real state that must not be overwritten on save.

    Still unconfirmed

    The filters element grammar. The recon account's only list has an empty schema, so every filter references an unknown column key and is dropped — grammar failure and column-not-found are indistinguishable. Preserved opaquely and commented as unconfirmed rather than guessed. Re-probe once a schema'd list exists.

    Flagged by the implementer, worth a look in review

    Shared-view writes by a non-owner are untested — the read-only branch hides shared create/rename/delete on a watcher share on the assumption they 403. If they are actually allowed, the gate is too tight (not broken).

  3. added a commit that references this issue on Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestparityWeb-parity gap with the InterlinedList web app

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions