Skip to content

Persist the dashboard layout server-side (#213) - #270

Merged
BabuPlk merged 2 commits into
devfrom
feature/213-persist-dashboard-layout
Sep 30, 2026
Merged

BabuPlk merged 2 commits into
devfrom
feature/213-persist-dashboard-layout

Conversation

@BabuPlk

@BabuPlk BabuPlk commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #213

What

Persists the dashboard layout server-side, one per user:

  • GET /api/v1/users/me/dashboard/layout?version=N – the caller's layout
  • PUT /api/v1/users/me/dashboard/layout – replaces it ({ version, items: [{ id, size }] })
  • DELETE /api/v1/users/me/dashboard/layout – forgets it (reset), 204

The per-board half of the issue already exists: areas, folds, sizes, pins etc. are stored per board through /api/v1/onboarding/me/board/structure. So both scopes are covered — one board arrangement per boardId, one dashboard layout per user — without flattening them into one key.

How it maps to the acceptance criteria

  • Read/write own layout – DashboardLayoutController + DashboardLayoutService in the user module, stored as JSON in dashboard_layouts (one row per user, upsert, last write wins – same pattern as board_structures).
  • Per-board and per-user scoping – see above.
  • Versioned – the row stores the client's layout version. A read names the version the client understands (LAYOUT_VERSION in the frontend); a row of any other version answers as the default. The version is the client's rather than a server constant because only the client knows its widget and size vocabulary — otherwise every frontend change to it would need a backend release.
  • No stored layout → default, not an error – 200 with items: [] and updatedAt: null. An empty list with a non-null updatedAt means "removed everything on purpose".
  • No endpoint exposes another user's layout – the user is always resolved from the token; no endpoint takes a user id. PM visibility stays out of scope.
  • Tests – DashboardLayoutServiceTest (round trip, absent layout, version mismatch, unreadable row, cross-user refusal, reset) and DashboardLayoutControllerTest (status codes, validation, auth).

Also: a user's layout is deleted together with the user (UserService.deleteUserById), and V21__add_dashboard_layouts.sql records the table like V11 does for board structures.

Tests

  • ./gradlew clean check passes locally.

Frontend: SprintStartProject/sprintstart-frontend#288

Adds GET/PUT/DELETE /api/v1/users/me/dashboard/layout: one arrangement per
user, stored as JSON with the client's layout version. A missing layout or
one written under another version answers with the default instead of a
404. The caller is always resolved from the token, and a user's layout is
removed together with the user.

The per-board arrangement already lives in /me/board/structure.

@DavidLeuter DavidLeuter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, approving. The API is small and self-serve only, and the "no layout / other version → 200 with updatedAt: null" contract is clear, documented and tested. The upsert follows the board_structures pattern, and the user-deletion hook is covered. I also like that the version is the client's rather than a server constant.

Minor, non-blocking:

  • DashboardLayoutItemPayload / DashboardLayoutLimits live in model.entity, but they're wire DTOs with validation annotations and not JPA entities. model.request.dashboard (or a shared model.dashboard) would be a better home. Fine if you mirrored the board's layout on purpose.
  • there is no endpoint that reads another user's layout by id only proves that an unmapped path answers 404, which would stay true even if such an endpoint existed under a different shape. The service tests cover the real guarantee (user always resolved from the token), so I'd just drop this one or rename it to what it actually checks.
  • As with the other migrations, V21__… is documentation only, since there's no Flyway and ddl-auto creates the table. Mentioning it so nobody expects the unique constraint to exist on a DB that predates ddl-auto picking up the entity. On a fresh schema Hibernate emits it from @UniqueConstraint, so the ON CONFLICT (user_id) upsert is fine.

The frontend half (sprintstart-frontend#288) has one issue around reset + migration that I've requested changes for. It doesn't need any backend change.

DashboardLayoutItemPayload and DashboardLayoutLimits are wire DTOs, not
JPA entities, so they now live in model.request.dashboard. Also drops the
controller test that only proved an unmapped path answers 404; the
service tests cover that a layout is always resolved from the token.
@BabuPlk

BabuPlk commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Picked up the non-blocking points in c9031b1:

  • DashboardLayoutItemPayload / DashboardLayoutLimits moved to model.request.dashboard — they're wire DTOs, not entities.
  • Dropped the controller test that only proved an unmapped path answers 404; the service tests cover the actual guarantee (the user is always resolved from the token).
  • Noted on V21: yes, documentation only, like the other migrations.

./gradlew test --tests '*DashboardLayout*' ktlintCheck passes locally.

@DavidLeuter DavidLeuter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking up the nits. c9031b1 is a pure package move plus dropping the 404 test, so my approval stands. The frontend half is approved as well now. Merge once CI is green.

@BabuPlk
BabuPlk merged commit 391d95b into dev Sep 30, 2026
4 checks passed
@BabuPlk
BabuPlk deleted the feature/213-persist-dashboard-layout branch October 5, 2026 22:28
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.

2 participants