feat(lists): saved views — shared/personal, default, fork-to-personal (#51) - #107
Merged
Merged
Conversation
Lists could only ever render one way; they now carry saved views. Adds the
DTOs, domain model, repository calls and a switcher on the list detail screen.
- GET/POST/PUT/DELETE /api/lists/{id}/views, plus POST …/views/{viewId} to fork
a view into a personal copy — the escape hatch when somebody else's shared
view does not suit.
- `scope` is an enum; a missing or unrecognised scope fails locally rather than
spending a request the API answers with a 400.
- `config` is kept as its raw JSON object so keys this client does not model
survive a round-trip; mode/density/filters are projected from it.
- Every write adopts the view the server returned, because the API silently
drops config values it does not recognise instead of rejecting them.
- Renaming and deleting are offered only for views the user owns; somebody
else's shared view is forked instead, and a server refusal is surfaced with
the view left in place.
The switcher is a slot on the stateless detail screen with its own ViewModel on
the same nav entry, so the detail screen and its tests stay independent of it.
Tests: 12 MockWebServer round-trips over the real paths and bodies, 5 mapper
cases, 12 ViewModel cases, and 6 Compose cases for the switcher.
Closes #51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #51. Part of epic #49.
The contract was captured live, because it is documented nowhere
Saved views are absent from the help centre and a bare
objectin the OpenAPI spec. I created aview on the test account, probed it, forked it, and deleted both (account verified back to
{"views":[]}). What that established:GET /api/lists/{id}/views→{ "views": [ … ] }; a view is{ id, listId, userId, name, scope, config, isDefault, position }.POSTrequiresnameandscope;scopeis exactlypersonalorshared(server's own400:
scope must be "personal" or "shared"). Returns 201{ "view": { … } }.configdefaults to{ "mode": "records", "density": "comfortable", "filters": [] }.POST …/views/{viewId}forks → a personal copy named"<name> (copy)", position incremented.DELETE …/views/{viewId}→{ "revoked"… }— actually{ "message": "View deleted" }.configvalues are silently dropped, not rejected.Design
configis a rawJsonObjectwrapper (ListViewConfig) with typed projections (mode,storedMode,density,filters). Keys the client does not model survive a round-tripuntouched — the server already drops what it does not know; the client must not compound that.
ViewModel replaces state with it, never with an optimistic copy. "Set as default" additionally
re-reads the list so a moved default flag is reflected on siblings.
scopeis an enum;fromApireturnsnullfor anything the API would 400 on, and the repositoryrefuses a missing/unknown scope or a blank name before opening a connection.
scopefrom the server reads as
SHARED— the conservative choice, so no destructive action is offeredon a view that may not be the user's. A 403 is surfaced as a message with the view left in place.
ListDetailScreendriven by its ownListViewsViewModel, so the detail screen and its existing tests stay independent of views.Verification
./gradlew :app:assembleDebug testDebugUnitTest→ BUILD SUCCESSFUL, 868 tests, 0 failures(29 new). Coverage includes: view list parse with unknown
configkeys; create/update/deleteround-trips asserting real paths and bodies (including "only the changed field is sent"); create
rejecting both a missing and an unrecognised scope with
server.requestCount == 0; fork fromPOST …/views/{viewId}with an empty body; and a write whose config the server silently alteredending up showing the server's value at both repository and ViewModel level. 6 Compose cases
compile, not executed (no emulator).
No
:apporsettings.gradle.ktschange was needed.Blocking finding for #52 — please read before that issue is attempted
config.modecannot currently carry a view mode other thanrecords. Live probing showedcards,card,grid,table,erd,diagram,board,gallery,kanbanandlistallround-tripping back as
records, with no error — the server silently rewrites them.So either (a) the web's card/grid/ERD modes are client-side-only and not persisted in a saved view,
(b) they live under a different
configkey I did not guess, or (c) they are gated on something thetest account lacks. #52 assumes view modes are configured through
config.mode, and that assumptionis not supported by the live API.
The client is ready either way:
ListViewConfigpreserves unknown keys and exposesstoredMode, sowhatever the real mechanism is, nothing is lost in transit and only the
ListViewModeenum needsextending.
Other notes
rendering is Lists: card, grid and ERD view modes #52's job by design.
offered on any view and a refusal is surfaced gracefully rather than pre-emptively hidden.