Super-admin org CRUD review fixes + name-length validation#430
Conversation
The /admin Organizations sidebar link was nested under an org-scoped role gate (isAdminOrSuperAdmin requires hasAccess, false when no org is selected), so a system SuperAdmin who belongs to no org couldn't reach it. Lift the gate to the org-independent altitude and gate only the org-scoped Members sub-item on a selected org. Also loosen isSuperAdmin's organization_id check to == null for consistency with isOrganizationArchived.
Archive/unarchive were raw sub-action POSTs that only revalidated the current ?status= list, leaving the sibling active/archived/all caches stale until an unrelated revalidation. Route them through a new useOrganizationArchiveMutation hook that fires a global cache invalidation, extracting the shared invalidate-by-baseUrl predicate out of useEntityMutation.
The "Archived {date} by {name}" byline could stick on an ellipsis when the
archived_by GET /users/{id} lookup errored. Show the ellipsis only while the
lookup is in flight and degrade to "a former admin" on error or absent
archived_by. Also memoize the org list sort.
Handle the backend's new 422 validation_error on org create/rename (empty or >255-char name): organizationNameInvalidMessage() surfaces it in the same inline name-field slot as the 409 organization_name_taken, scoped to the org form so the shared validation_error discriminator isn't treated globally. Add validateOrganizationName() as a client-side pre-check (non-empty, <=255) to avoid the round-trip; the server 422 stays source of truth for edge cases.
Greptile SummaryThis PR delivers follow-up hardening to the super-admin org CRUD feature, addressing four distinct gaps: sidebar access for org-less SuperAdmins, stale cache after archive/unarchive sub-action POSTs, a byline that could permanently show "…" on archiver-lookup failure, and missing client-side/server-side 422 handling for org-name length violations. The previous P1 (client validating the trimmed length but sending the raw padded string) and the P2 (orphaned JSDoc on
Confidence Score: 5/5Safe to merge — all four fix areas are correct and well-tested; previous round's blocking trim-mismatch issue is resolved. The trim-before-validate-and-send fix is confirmed in the form dialog and verified by a new test. The cache invalidation path correctly fires in a finally-guarded hook, covers all three status-filter SWR keys, and degrades cleanly on error. The sidebar gate change is minimal and the byline degradation logic is straightforward. No regressions detected across any of the changed paths. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[OrganizationRow] -->|uses| B[useOrganizationArchiveMutation]
B -->|calls| C[OrganizationApi.archive / unarchive]
C -->|POST /organizations/:id/archive| D[Backend]
B -->|on success| E[EntityApi.invalidateEntityCache]
E -->|mutate predicate| F[SWR Global Cache]
F -->|revalidates all keys containing ORGANIZATIONS_BASEURL| G[useAllOrganizations active/archived/all]
H[OrganizationFormDialog] -->|handleSubmit| I{client pre-check validateOrganizationName}
I -->|invalid| J[setNameError - no round-trip]
I -->|valid| K[useOrganizationMutation.create/update]
K -->|success| L[toast.success + close dialog]
K -->|409 name taken| M[setNameError org_name_taken]
K -->|422 validation_error| N[organizationNameInvalidMessage setNameError]
K -->|other error| O[toast.error]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[OrganizationRow] -->|uses| B[useOrganizationArchiveMutation]
B -->|calls| C[OrganizationApi.archive / unarchive]
C -->|POST /organizations/:id/archive| D[Backend]
B -->|on success| E[EntityApi.invalidateEntityCache]
E -->|mutate predicate| F[SWR Global Cache]
F -->|revalidates all keys containing ORGANIZATIONS_BASEURL| G[useAllOrganizations active/archived/all]
H[OrganizationFormDialog] -->|handleSubmit| I{client pre-check validateOrganizationName}
I -->|invalid| J[setNameError - no round-trip]
I -->|valid| K[useOrganizationMutation.create/update]
K -->|success| L[toast.success + close dialog]
K -->|409 name taken| M[setNameError org_name_taken]
K -->|422 validation_error| N[organizationNameInvalidMessage setNameError]
K -->|other error| O[toast.error]
Reviews (2): Last reviewed commit: "fix(admin): send trimmed org name; resto..." | Re-trigger Greptile |
Address Greptile review on #430: - organization-form-dialog: trim the name before both the client pre-check and the request payload, so a whitespace-padded value can't pass validation yet differ from what hits the wire (and the stored org name has no stray padding). - entity-api: move the useEntityMutation JSDoc back onto its function; it was left orphaned above the extracted invalidateEntityCache helper.
Follow-up fixes to the super-admin org CRUD feature (#426), which merged before these review-round changes were committed. All wire-compatible with
AdminOrganizationCRUDv2; the name-length handling matches the backend'sadmin_org_name_validation_and_delete_fk_hardeninghardening (PR refactor-platform-rs#366).Fixes
1. Reach
/adminfor super admins with no org selectedThe
/adminOrganizations sidebar link was nested under an org-scoped role gate (isAdminOrSuperAdminrequires an org context), so a system SuperAdmin who belongs to no org couldn't reach it. The gate is lifted to the org-independent altitude; the org-scoped Members sub-item is separately gated on a selected org.isSuperAdmin's null check is loosened to== nullfor consistency withisOrganizationArchived.2. Invalidate all org caches on archive/unarchive
Archive/unarchive were raw sub-action POSTs that only revalidated the current
?status=list, leaving the sibling active/archived/all caches stale. They now route through a newuseOrganizationArchiveMutationhook that fires a global cache invalidation (sharedinvalidateEntityCachepredicate extracted fromuseEntityMutation).3. Degrade the archived byline gracefully
The "Archived {date} by {name}" byline could stick on "…" when the
archived_byGET /users/{id}lookup errored. It now shows "…" only while in flight and degrades to "a former admin" on error or absentarchived_by. Also memoizes the org list sort.4. Validate organization name length (422 + client pre-check)
Handles the backend's new
422 validation_erroron org create/rename (empty or >255-char name) in the same inline name-field slot as the409 organization_name_taken, scoped to the org form so the sharedvalidation_errordiscriminator isn't treated globally. Adds a client-side pre-check (non-empty, ≤255) to avoid the round-trip; the server 422 stays source of truth.Verification
tsc --noEmitclean; 1620 unit tests green.27152e88): archive/unarchive tab-cache invalidation both directions; byline resolves "by Admin" and degrades correctly; org name 422 (empty + >255) both inline with no toast and no round-trip; valid create/edit/delete unaffected. Role gating re-confirmed for org-admin and regular user (both 404 on/admin).