From 3fe9f774a1e585bb17d1cadfaeb8814412855ae2 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:24:11 +0100 Subject: [PATCH] feat: add client-side role name uniqueness validation with 409 handling --- .../modals/spaceSettingsPanels/RolesPanel.tsx | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/web/src/components/modals/spaceSettingsPanels/RolesPanel.tsx b/packages/web/src/components/modals/spaceSettingsPanels/RolesPanel.tsx index 9b288f0f..605b385a 100644 --- a/packages/web/src/components/modals/spaceSettingsPanels/RolesPanel.tsx +++ b/packages/web/src/components/modals/spaceSettingsPanels/RolesPanel.tsx @@ -179,9 +179,21 @@ interface RoleEditViewProps { function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) { const loadSpaceDetail = useSpaceStore((s) => s.loadSpaceDetail); + const roles = useSpaceStore((s) => s.roles); const isEveryone = role.id === spaceId; const [draftName, setDraftName] = useState(role.name); + const [nameError, setNameError] = useState(''); + + const validateName = (name: string): string => { + const trimmed = name.trim(); + if (!trimmed) return 'Role name cannot be empty'; + const isDuplicate = roles.some( + (r) => r.id !== role.id && r.name.toLowerCase() === trimmed.toLowerCase() + ); + return isDuplicate ? 'A role with this name already exists' : ''; + }; + const [draftColor, setDraftColor] = useState(role.color); const [draftPermissions, setDraftPermissions] = useState( stringToPermissions(role.permissions) @@ -217,7 +229,12 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) { setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 2000); } catch (err) { - setSaveError(err instanceof Error ? err.message : 'Failed to save role'); + const msg = err instanceof Error ? err.message : 'Failed to save role'; + if (msg.includes('already exists')) { + setNameError(msg); + } else { + setSaveError(msg); + } } finally { setSaving(false); } @@ -229,6 +246,7 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) { setDraftPermissions(stringToPermissions(role.permissions)); setConfirmDelete(false); setSaveError(''); + setNameError(''); }; const handleDelete = async () => { @@ -277,9 +295,16 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) { setDraftName(e.target.value)} - className="input-standard w-full" + onChange={(e) => { + setDraftName(e.target.value); + setNameError(validateName(e.target.value)); + }} + onBlur={() => setNameError(validateName(draftName))} + className={`input-standard w-full${nameError ? ' ring-2 ring-accent-rose' : ''}`} /> + {nameError && ( +

{nameError}

+ )}