Skip to content

Comments

implement role re-ordering#676

Open
Southclaws wants to merge 1 commit intomainfrom
role-ordering
Open

implement role re-ordering#676
Southclaws wants to merge 1 commit intomainfrom
role-ordering

Conversation

@Southclaws
Copy link
Owner

No description provided.

@vercel
Copy link

vercel bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
storyden Ready Ready Preview, Comment Feb 19, 2026 7:15pm
storyden-homepage Error Error Feb 19, 2026 7:15pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

This PR adds metadata field support to roles across the database, ORM, and API layers, and introduces a new PATCH /roles/order endpoint to reorder custom roles. The frontend adds drag-and-drop UI for role ordering while preventing reordering of default roles (Guest, Member, Admin).

Changes

Cohort / File(s) Summary
Role Model & Core Domain
app/resources/account/role/role.go, app/resources/account/role/role_querier/role_querier.go, app/resources/account/role/role_writer/role_writer.go, app/resources/account/role/default.go
Added Metadata field to Role struct; introduced GetAdminRole() method and enhanced lookupDefaultRoles to return admin role; added Create mutation options support, nextCustomSortKey helper, and new UpdateSortOrder method for reordering with transactional safety; comment-only updates to default role constants.
ORM Schema & Mutations
internal/ent/schema/role.go, internal/ent/migrate/schema.go, internal/ent/role.go, internal/ent/role/role.go, internal/ent/role/where.go, internal/ent/mutation.go, internal/ent/role_create.go, internal/ent/role_update.go
Added metadata JSON column to Role schema; extended mutation types across RoleMutation and other entities with SetMetadata, Metadata, OldMetadata, ClearMetadata, and ResetMetadata methods; integrated metadata field into create and update builders with SQL persistence logic; added MetadataIsNil/MetadataNotNil predicates.
HTTP API & OpenAPI
app/transports/http/bindings/roles.go, app/transports/http/bindings/openapi_rbac/mapping.go, app/transports/http/bindings/openapi_rbac/openapi_rbac_gen.go, app/transports/http/openapi/operation/operation_*_gen.go
Added RoleUpdateOrder HTTP handler with sort order validation and transactional updates; extended roles handler to support metadata in create/update requests and serialize role metadata; added RoleUpdateOrder operation to permission mapping and operation enums.
Frontend API Schema & Client
web/src/api/openapi-schema/roleOrderMutableProps.ts, web/src/api/openapi-schema/roleUpdateOrderBody.ts, web/src/api/openapi-schema/role*.ts, web/src/api/openapi-client/roles.ts, web/src/api/openapi-server/roles.ts
Generated new schema types for role ordering (RoleOrderMutableProps, RoleUpdateOrderBody); extended role props interfaces with optional meta field; added roleUpdateOrder function, hook, and SWR mutation utilities; exported new types from schema index.
Frontend UI Components
web/src/components/role/RoleCard.tsx, web/src/components/role/RoleEdit/RoleEditModal.tsx, web/src/screens/role/RoleScreen.tsx
Enhanced RoleCard with dragHandle prop and top-right badge cluster; replaced EditAction with custom IconButton; refactored RoleScreen with DnD context and sortable grid, adding optimistic UI updates and server mutation handling for role reordering with toast notifications.
Tests & Documentation
tests/account/role/role_test.go, home/content/docs/api/roles/RoleUpdateOrder.mdx
Added role_metadata_roundtrip and role_reorder_custom_roles test cases with multi-step validation and retry logic; created API documentation page for PATCH /roles/order endpoint.

Sequence Diagram

sequenceDiagram
    participant Client as Frontend Client
    participant Handler as HTTP Handler
    participant Writer as Role Writer
    participant Querier as Role Querier
    participant DB as Database

    Client->>Handler: PATCH /roles/order with role IDs
    Handler->>Handler: Validate input (non-empty, unique)
    Handler->>Writer: UpdateSortOrder(ctx, roleIDs)
    
    Writer->>Querier: Retrieve current roles
    Querier->>DB: Query existing roles
    DB-->>Querier: Return roles
    
    Writer->>Writer: Validate custom roles only
    Writer->>Writer: Compute new SortKey values
    
    Writer->>DB: Begin transaction
    loop For each role
        Writer->>DB: SetSortKey update
    end
    Writer->>DB: Commit transaction
    
    Writer-->>Handler: Success
    Handler->>Querier: ListFor() reload
    Querier->>DB: Query updated roles
    DB-->>Querier: Return ordered roles
    Querier-->>Handler: Return role list
    Handler-->>Client: 200 with reordered roles
    Client->>Client: Update UI with new order
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Add role management #277: Modifies role management UI and backend role model with updates to RoleCard and ent schema files, establishing a direct connection to metadata and role management changes.

Poem

🐰 Hop and reorder, roles in a queue,
Metadata whispers of what's old and new,
Drag them with delight, a custom display,
Defaults stay steady while customs hold sway! 🎪

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether it relates to the changeset. Add a pull request description explaining the changes, implementation approach, and any relevant context for reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'implement role re-ordering' directly matches the main objective of the changeset, which adds comprehensive role reordering functionality across the backend and frontend.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch role-ordering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
web/src/components/role/RoleEdit/RoleEditModal.tsx (1)

48-61: Prefer aria-label over title for screen-reader accessibility on this icon-only button.

title is not reliably announced by screen readers (it's a tooltip hint, not an accessible name per WCAG 2.1 SC 4.1.2). An aria-label provides a proper accessible name for assistive technology.

♿ Proposed fix
-      <IconButton
-        variant="ghost"
-        size="xs"
-        minWidth="5"
-        width="5"
-        height="5"
-        padding="0"
-        color="fg.muted"
-        disabled={cannotEdit}
-        title={titleLabel}
-        onClick={disclosure.onOpen}
-      >
+      <IconButton
+        variant="ghost"
+        size="xs"
+        minWidth="5"
+        width="5"
+        height="5"
+        padding="0"
+        color="fg.muted"
+        disabled={cannotEdit}
+        title={titleLabel}
+        aria-label={titleLabel}
+        onClick={disclosure.onOpen}
+      >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/role/RoleEdit/RoleEditModal.tsx` around lines 48 - 61, The
IconButton uses a title prop which is not reliably exposed to assistive tech;
update the IconButton in RoleEditModal to add an aria-label (e.g.,
aria-label={titleLabel}) instead of or in addition to title so the icon-only
button has an accessible name, keeping existing props like
disabled={cannotEdit}, onClick={disclosure.onOpen}, and the EditIcon intact;
ensure the aria-label value is meaningful (use the existing titleLabel
variable).
app/resources/account/role/role_writer/role_writer.go (1)

293-312: nextCustomSortKey correctly computes the next sort key but loads all custom roles just for a max.

This works correctly and is simple. For a future optimization, you could use Order(ent_role.ByField(ent_role.FieldSortKey, sql.OrderDesc())).Limit(1) to only fetch the role with the highest sort key, avoiding loading all custom roles into memory. Not a concern at current scale, just a note.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/resources/account/role/role_writer/role_writer.go` around lines 293 -
312, The nextCustomSortKey function currently loads all custom roles to compute
the max SortKey; change it to fetch only the single role with the highest
SortKey by replacing the .All(ctx) call with a query that applies the same Where
filter and then .Order(ent_role.ByField(ent_role.FieldSortKey,
sql.OrderDesc())).Limit(1).First(ctx) (or .Only/First depending on existing
patterns) and compute the next key from that single result; update imports to
include the sql package used for ordering and handle the case where no role is
returned the same way as before.
web/src/screens/role/RoleScreen.tsx (1)

92-96: Missing KeyboardSensor reduces keyboard accessibility.

Only PointerSensor is configured, so keyboard-only users cannot reorder roles. Adding KeyboardSensor with sortableKeyboardCoordinates is recommended by @dnd-kit for accessible drag-and-drop.

♿ Proposed fix to add keyboard support
 import {
   DndContext,
   DragEndEvent,
+  KeyboardSensor,
   PointerSensor,
   useSensor,
   useSensors,
 } from "@dnd-kit/core";
 import {
   SortableContext,
   arrayMove,
   rectSortingStrategy,
+  sortableKeyboardCoordinates,
   useSortable,
 } from "@dnd-kit/sortable";
   const sensors = useSensors(
     useSensor(PointerSensor, {
       activationConstraint: { distance: 3 },
     }),
+    useSensor(KeyboardSensor, {
+      coordinateGetter: sortableKeyboardCoordinates,
+    }),
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/screens/role/RoleScreen.tsx` around lines 92 - 96, The drag-and-drop
setup in RoleScreen.tsx only registers PointerSensor, which prevents
keyboard-only users from reordering roles; to fix, import KeyboardSensor and
sortableKeyboardCoordinates from `@dnd-kit` (or their respective packages) and add
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) to
the useSensors(...) call alongside the existing useSensor(PointerSensor, ...) so
the sensors constant supports keyboard interaction for the sortable list.
web/src/components/role/RoleCard.tsx (1)

43-55: Unnecessary Fragment wrapper around the conditional badge.

The <>...</> fragment on lines 45-51 wraps a single expression (the ternary). You can remove it and use the ternary directly.

✨ Proposed simplification
         <HStack>
           {isDefault && (
-            <>
-              {isCustomDefault ? (
-                <Badge size="sm">Default + Custom</Badge>
-              ) : (
-                <Badge size="sm">Default</Badge>
-              )}
-            </>
+            isCustomDefault ? (
+              <Badge size="sm">Default + Custom</Badge>
+            ) : (
+              <Badge size="sm">Default</Badge>
+            )
           )}

           {dragHandle}
         </HStack>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/role/RoleCard.tsx` around lines 43 - 55, Remove the
unnecessary React fragment wrapping the ternary inside the HStack: when
rendering the conditional badge based on isDefault and isCustomDefault in
RoleCard (the Badge rendering block), replace the fragment-wrapped ternary with
the ternary expression directly so the code reads {isDefault && (isCustomDefault
? <Badge size="sm">Default + Custom</Badge> : <Badge size="sm">Default</Badge>)}
while keeping dragHandle and HStack unchanged.
app/resources/account/role/role_querier/role_querier.go (1)

55-70: List() calls lookupDefaultRoles three times via individual getters — 3 redundant DB queries.

GetMemberRole, GetGuestRole, and GetAdminRole each internally call lookupDefaultRoles, which executes a DB query. In List() all three are called sequentially, tripling the same query. This was already 2 calls before this PR and is now 3.

Consider calling lookupDefaultRoles once directly and mapping the results inline, similar to how ListFor (line 102) already does it.

♻️ Proposed consolidation
-	defaultRole, err := q.GetMemberRole(ctx)
-	if err != nil {
-		return nil, fault.Wrap(err, fctx.With(ctx))
-	}
-
-	guestRole, err := q.GetGuestRole(ctx)
-	if err != nil {
-		return nil, fault.Wrap(err, fctx.With(ctx))
-	}
-
-	adminRole, err := q.GetAdminRole(ctx)
-	if err != nil {
-		return nil, fault.Wrap(err, fctx.With(ctx))
-	}
-
-	mapped = append(mapped, defaultRole, guestRole, adminRole)
+	guestEnt, memberEnt, adminEnt, err := q.lookupDefaultRoles(ctx)
+	if err != nil {
+		return nil, fault.Wrap(err, fctx.With(ctx))
+	}
+
+	for _, pair := range []struct {
+		ent      *ent.Role
+		fallback *role.Role
+	}{
+		{guestEnt, &role.DefaultRoleGuest},
+		{memberEnt, &role.DefaultRoleMember},
+		{adminEnt, &role.DefaultRoleAdmin},
+	} {
+		if pair.ent != nil {
+			r, err := role.Map(pair.ent)
+			if err != nil {
+				return nil, fault.Wrap(err, fctx.With(ctx))
+			}
+			mapped = append(mapped, r)
+		} else {
+			mapped = append(mapped, pair.fallback)
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/resources/account/role/role_querier/role_querier.go` around lines 55 -
70, List() is making three redundant DB queries by calling GetMemberRole,
GetGuestRole, and GetAdminRole (each calls lookupDefaultRoles); instead call
lookupDefaultRoles(ctx) once, handle its returned slice/map and assign the
defaultRole, guestRole, and adminRole values into mapped inline (like ListFor
does), replacing the three separate calls to
GetMemberRole/GetGuestRole/GetAdminRole and preventing duplicate DB queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/resources/account/role/role_querier/role_querier.go`:
- Around line 55-70: List() is making three redundant DB queries by calling
GetMemberRole, GetGuestRole, and GetAdminRole (each calls lookupDefaultRoles);
instead call lookupDefaultRoles(ctx) once, handle its returned slice/map and
assign the defaultRole, guestRole, and adminRole values into mapped inline (like
ListFor does), replacing the three separate calls to
GetMemberRole/GetGuestRole/GetAdminRole and preventing duplicate DB queries.

In `@app/resources/account/role/role_writer/role_writer.go`:
- Around line 293-312: The nextCustomSortKey function currently loads all custom
roles to compute the max SortKey; change it to fetch only the single role with
the highest SortKey by replacing the .All(ctx) call with a query that applies
the same Where filter and then .Order(ent_role.ByField(ent_role.FieldSortKey,
sql.OrderDesc())).Limit(1).First(ctx) (or .Only/First depending on existing
patterns) and compute the next key from that single result; update imports to
include the sql package used for ordering and handle the case where no role is
returned the same way as before.

In `@web/src/components/role/RoleCard.tsx`:
- Around line 43-55: Remove the unnecessary React fragment wrapping the ternary
inside the HStack: when rendering the conditional badge based on isDefault and
isCustomDefault in RoleCard (the Badge rendering block), replace the
fragment-wrapped ternary with the ternary expression directly so the code reads
{isDefault && (isCustomDefault ? <Badge size="sm">Default + Custom</Badge> :
<Badge size="sm">Default</Badge>)} while keeping dragHandle and HStack
unchanged.

In `@web/src/components/role/RoleEdit/RoleEditModal.tsx`:
- Around line 48-61: The IconButton uses a title prop which is not reliably
exposed to assistive tech; update the IconButton in RoleEditModal to add an
aria-label (e.g., aria-label={titleLabel}) instead of or in addition to title so
the icon-only button has an accessible name, keeping existing props like
disabled={cannotEdit}, onClick={disclosure.onOpen}, and the EditIcon intact;
ensure the aria-label value is meaningful (use the existing titleLabel
variable).

In `@web/src/screens/role/RoleScreen.tsx`:
- Around line 92-96: The drag-and-drop setup in RoleScreen.tsx only registers
PointerSensor, which prevents keyboard-only users from reordering roles; to fix,
import KeyboardSensor and sortableKeyboardCoordinates from `@dnd-kit` (or their
respective packages) and add useSensor(KeyboardSensor, { coordinateGetter:
sortableKeyboardCoordinates }) to the useSensors(...) call alongside the
existing useSensor(PointerSensor, ...) so the sensors constant supports keyboard
interaction for the sortable list.

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