Task #98: Warn admins before renaming a role used in code references
- Added GET /api/roles/:id/usage endpoint returning {userCount, groupCount},
guarded by requireAdmin and validating the role id.
- Updated PATCH /api/roles/:id audit metadata to record renamed=true,
renamedFrom, and renamedTo when the role name changes (in addition to
existing name fields), satisfying the traceability requirement.
- Added RoleUsage schema + /roles/{id}/usage operation to openapi.yaml,
regenerated lib/api-zod via orval. Codegen script in lib/api-spec was
updated intentionally to preserve `export * from './manual'` in
api-zod's index, which orval was overwriting.
- RolesPanel edit dialog (admin.tsx) now tracks the original role name
on open and shows an amber warning banner with from/to text plus a
usage line (users + groups counts) whenever the trimmed name input
differs from the original. Warning is suppressed for system or
protected roles since their name input is disabled.
- Added en/ar locale strings: renameWarningTitle, renameWarningBody,
renameUsage.
- Reverted accidental opengraph.jpg change picked up earlier in the
session — it is unrelated to this task.
Verification: e2e test created a non-system role, assigned the admin
user to it, opened the edit dialog, observed the warning + usage line,
saved the rename, and confirmed both the role name persisted and the
audit log captured renamed/renamedFrom/renamedTo.
Note: The pnpm `test` workflow shows pre-existing failures in
executive-meetings.ts that are unrelated to this task (verified via
git stash before changes).
Replit-Task-Id: c1ee048d-50c6-4439-a430-ee0b7ec09959
This commit is contained in:
@@ -45,6 +45,8 @@ import {
|
||||
getListPermissionsQueryKey,
|
||||
useGetRolePermissions,
|
||||
getGetRolePermissionsQueryKey,
|
||||
useGetRoleUsage,
|
||||
getGetRoleUsageQueryKey,
|
||||
useReplaceRolePermissions,
|
||||
useListAuditLogs,
|
||||
getListAuditLogsQueryKey,
|
||||
@@ -3226,6 +3228,7 @@ function RolesPanel() {
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editingIsSystem, setEditingIsSystem] = useState(false);
|
||||
const [editForm, setEditForm] = useState({ name: "", descriptionAr: "", descriptionEn: "" });
|
||||
const [originalEditName, setOriginalEditName] = useState("");
|
||||
const [editPermissionIds, setEditPermissionIds] = useState<Set<number>>(new Set());
|
||||
const [initialPermissionIds, setInitialPermissionIds] = useState<Set<number>>(new Set());
|
||||
const [deleteTarget, setDeleteTarget] = useState<{ id: number; name: string } | null>(null);
|
||||
@@ -3241,6 +3244,12 @@ function RolesPanel() {
|
||||
},
|
||||
},
|
||||
);
|
||||
const { data: editingRoleUsage } = useGetRoleUsage(editingPermissionsId, {
|
||||
query: {
|
||||
queryKey: getGetRoleUsageQueryKey(editingPermissionsId),
|
||||
enabled: editingId != null && !editingIsSystem,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editingId == null) return;
|
||||
@@ -3309,14 +3318,23 @@ function RolesPanel() {
|
||||
descriptionAr: role.descriptionAr ?? "",
|
||||
descriptionEn: role.descriptionEn ?? "",
|
||||
});
|
||||
setOriginalEditName(role.name);
|
||||
};
|
||||
|
||||
const closeEditDialog = () => {
|
||||
setEditingId(null);
|
||||
setEditPermissionIds(new Set());
|
||||
setInitialPermissionIds(new Set());
|
||||
setOriginalEditName("");
|
||||
};
|
||||
|
||||
const editNameTrimmed = editForm.name.trim();
|
||||
const isRenaming =
|
||||
!editingIsSystem &&
|
||||
originalEditName.length > 0 &&
|
||||
editNameTrimmed.length > 0 &&
|
||||
editNameTrimmed !== originalEditName;
|
||||
|
||||
const permissionsChanged = (() => {
|
||||
if (editPermissionIds.size !== initialPermissionIds.size) return true;
|
||||
for (const id of editPermissionIds) {
|
||||
@@ -3585,6 +3603,32 @@ function RolesPanel() {
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{editingIsSystem ? t("admin.roles.editSystemNameHint") : t("admin.roles.nameHint")}
|
||||
</p>
|
||||
{isRenaming && (
|
||||
<div
|
||||
className="mt-2 rounded-xl border border-amber-300 bg-amber-50 p-3 text-xs text-amber-900 space-y-1"
|
||||
data-testid="role-rename-warning"
|
||||
role="alert"
|
||||
>
|
||||
<p className="font-semibold">
|
||||
{t("admin.roles.renameWarningTitle", {
|
||||
from: originalEditName,
|
||||
to: editNameTrimmed,
|
||||
})}
|
||||
</p>
|
||||
<p>{t("admin.roles.renameWarningBody")}</p>
|
||||
{editingRoleUsage && (
|
||||
<p
|
||||
className="text-amber-800"
|
||||
data-testid="role-rename-usage"
|
||||
>
|
||||
{t("admin.roles.renameUsage", {
|
||||
users: editingRoleUsage.userCount,
|
||||
groups: editingRoleUsage.groupCount,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("admin.roles.descriptionAr")}</Label>
|
||||
|
||||
Reference in New Issue
Block a user