Skip to content

feat(menus): tree endpoint + get-by-id + icon + admin-ui field names#14

Merged
jlc488 merged 1 commit into
mainfrom
feat/menus-contract-gap
May 27, 2026
Merged

feat(menus): tree endpoint + get-by-id + icon + admin-ui field names#14
jlc488 merged 1 commit into
mainfrom
feat/menus-contract-gap

Conversation

@jlc488

@jlc488 jlc488 commented May 27, 2026

Copy link
Copy Markdown
Contributor

First PR in a small series closing contract gaps between the admin-ui PRs (devslab-kr/devslab-kit-admin-ui#2#7) and the backend on main. The admin-ui's MenusView calls three things this controller didn't expose, plus a column the entity didn't have.

Summary

Endpoints

  • GET /admin/api/v1/menus/tree?tenantId=... — new. Returns the nested tree the TreeTable component needs (each root carries its descendants under children, sorted by displayOrder at every level). Built in-controller from the existing flat repo query — single round-trip.
  • GET /admin/api/v1/menus/{id} — new. Returns 404 on miss.

DTO wire-shape (matches the admin-ui)

  • CreateMenuRequest / UpdateMenuRequest — fields renamed to what the admin UI sends: displayOrder (was sortOrder), requiredPermission (was requiredPermissionCode), plus the new icon. Validation constraints preserved.
  • MenuResponse — two factory methods. flat(entity) for the existing list + new get-by-id (children = null). withChildren(entity, list) for the tree endpoint. Wire field names also follow the UI.

Entity / schema

  • PlatformMenuEntity — new icon field (nullable, VARCHAR(64)). All-args constructor extended.
  • V8__platform_menu_icon.sqlALTER TABLE platform_menu ADD COLUMN icon VARCHAR(64);. Existing rows unaffected; icon-less items render text-only on the UI.

Service layer

  • MenuAdminService.create / update — new icon parameter; partial-update semantics on update (icon updates only if non-null), matching the existing pattern for label/path/sortOrder.
  • MenuAdminService.findById — new read used by GET /menus/{id}.

Tree propagation

  • MenuItem API record — new icon field.
  • MenuTreeBuilder reads it from the entity.
  • PermissionBasedMenuFilter preserves it when pruning by user permissions.
  • DiagnosticsController.filterByCodes does the same when computing menu visibility.

Test fixture

  • MenuTreeBuilderTest entity factory updated to the new constructor (icon = null).

Gotcha worth flagging

The V8 migration comment intentionally avoids ${...} even though it's just a code-comment example. Flyway's PlaceholderReplacingReader scans SQL comments too — my first attempt had pi pi-${icon} in a -- line and the sample-app integration tests blew up with org.flywaydb.core.api.FlywayException. Easy fix once you know.

Test plan

  • ./gradlew build green (55 tasks, all sample-app + menu-core tests pass)
  • Wire up the admin-ui PR stack against this and exercise the Menus page end-to-end (TreeTable renders, add-child / edit (with icon) / delete all hit the right endpoints)

What's next in this series

Other gap closures still queued (separate PRs): AuditLogs (paging + filters + field rename), Policies (PolicyDescriptor list + nested test body), Tenants (status enum + createdAt), Diagnostics + Settings field names.

The admin UI's Menus page calls three things this controller didn't
expose, plus a column the entity didn't have:

  - GET /admin/api/v1/menus/tree?tenantId=...  → nested tree (TreeTable)
  - GET /admin/api/v1/menus/{id}               → single item
  - POST/PUT body fields displayOrder / requiredPermission / icon
    (not sortOrder / requiredPermissionCode / —)
  - MenuItem.icon for sidebar rendering (e.g. "pi-users")

This PR closes that gap while keeping the entity column names
stable, so nothing downstream of MenuAdminService breaks.

Schema
------
- V8__platform_menu_icon.sql adds `icon VARCHAR(64)` to platform_menu,
  nullable. Existing rows unaffected; the column is text-only on the
  UI when null.

Entity / API
------------
- PlatformMenuEntity: new icon field + extended all-args constructor.
- MenuItem: new icon field, propagated by MenuTreeBuilder and
  PermissionBasedMenuFilter so visibility-filtered trees keep icons.
- MenuAdminService.create / update: new icon parameter; partial-
  update semantics on update (icon updates only if non-null).
- MenuAdminService.findById: new read used by GET /menus/{id}.

DTOs
----
- CreateMenuRequest / UpdateMenuRequest: rename to the wire shape the
  admin UI sends — displayOrder (was sortOrder), requiredPermission
  (was requiredPermissionCode), plus icon. Validation constraints
  preserved.
- MenuResponse: two factory methods. `flat(entity)` for the existing
  list + new get-by-id endpoint (children == null). `withChildren`
  for the new tree endpoint (children == nested list). Wire field
  names also follow the UI: displayOrder, requiredPermission, icon.

Controller
----------
- New GET /menus/tree builds the nested response in-controller from
  the same JPA repository the flat list uses (single round-trip,
  sorted by displayOrder at every level).
- New GET /menus/{id} returns 404 on miss.
- Existing POST/PUT/GET/DELETE adjusted to the renamed DTO fields.

Other call-sites
----------------
- DiagnosticsController.filterByCodes preserves the icon when
  pruning the menu tree by user permissions.
- MenuTreeBuilderTest fixture updated to the new entity constructor
  (icon = null).

V8 migration comment intentionally avoids `${...}` because Flyway's
PlaceholderReplacingReader scans SQL comments too — the first run
hit `org.flywaydb.core.api.FlywayException` in sample-app tests
because the original comment contained `pi pi-${icon}`.
@jlc488
jlc488 merged commit 297c9c0 into main May 27, 2026
1 check passed
@jlc488
jlc488 deleted the feat/menus-contract-gap branch May 30, 2026 06:37
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.

1 participant