feat: add Copy Role button to duplicate roles with permissions

This commit is contained in:
Jannis Braun
2026-03-21 23:30:12 +01:00
parent cb34bfc8ea
commit 57b192533d
@@ -103,6 +103,7 @@ export function RolesPanel({ spaceId }: RolesPanelProps) {
spaceId={spaceId} spaceId={spaceId}
onBack={() => setEditingRoleId(null)} onBack={() => setEditingRoleId(null)}
onDeleted={() => setEditingRoleId(null)} onDeleted={() => setEditingRoleId(null)}
onCopied={(newRoleId) => setEditingRoleId(newRoleId)}
/> />
); );
} }
@@ -175,9 +176,10 @@ interface RoleEditViewProps {
spaceId: string; spaceId: string;
onBack: () => void; onBack: () => void;
onDeleted: () => void; onDeleted: () => void;
onCopied: (newRoleId: string) => void;
} }
function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) { function RoleEditView({ role, spaceId, onBack, onDeleted, onCopied }: RoleEditViewProps) {
const loadSpaceDetail = useSpaceStore((s) => s.loadSpaceDetail); const loadSpaceDetail = useSpaceStore((s) => s.loadSpaceDetail);
const roles = useSpaceStore((s) => s.roles); const roles = useSpaceStore((s) => s.roles);
const isEveryone = role.id === spaceId; const isEveryone = role.id === spaceId;
@@ -208,7 +210,6 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) {
const hasColorChange = !isEveryone && draftColor !== role.color; const hasColorChange = !isEveryone && draftColor !== role.color;
const hasPermChange = permissionsToString(draftPermissions) !== (role.permissions ?? '0'); const hasPermChange = permissionsToString(draftPermissions) !== (role.permissions ?? '0');
const hasChanges = hasNameChange || hasColorChange || hasPermChange; const hasChanges = hasNameChange || hasColorChange || hasPermChange;
const showPill = hasChanges || !isEveryone;
const togglePermission = (bit: bigint) => { const togglePermission = (bit: bigint) => {
setDraftPermissions((prev) => (prev & bit) !== 0n ? prev & ~bit : prev | bit); setDraftPermissions((prev) => (prev & bit) !== 0n ? prev & ~bit : prev | bit);
@@ -268,6 +269,26 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) {
} }
}; };
const [copying, setCopying] = useState(false);
const handleCopy = async () => {
setCopying(true);
setSaveError('');
try {
const newRole = await api.roles.create(spaceId, {
name: `Copy of ${role.name}`,
color: role.color,
permissions: role.permissions ?? undefined,
});
await loadSpaceDetail(spaceId);
onCopied(newRole.id);
} catch (err) {
setSaveError(err instanceof Error ? err.message : 'Failed to copy role');
} finally {
setCopying(false);
}
};
return ( return (
<div className="space-y-5"> <div className="space-y-5">
{/* Back button */} {/* Back button */}
@@ -398,7 +419,6 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) {
{saveSuccess && ( {saveSuccess && (
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Role saved</div> <div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Role saved</div>
)} )}
{showPill && (
<div className="sticky bottom-0 z-10 pointer-events-none"> <div className="sticky bottom-0 z-10 pointer-events-none">
<div className="flex justify-center pt-3 pb-1"> <div className="flex justify-center pt-3 pb-1">
<div className={`glass-bubble rounded-full px-4 py-2 flex items-center gap-2 pointer-events-auto${ <div className={`glass-bubble rounded-full px-4 py-2 flex items-center gap-2 pointer-events-auto${
@@ -421,10 +441,19 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) {
</button> </button>
</> </>
)} )}
{!isEveryone && hasChanges && ( {hasChanges && (
<div className="w-px h-5 bg-white/10" /> <div className="w-px h-5 bg-white/10" />
)} )}
<button
onClick={handleCopy}
disabled={copying}
className="px-3 py-1.5 text-sm font-medium rounded-full text-txt-secondary hover:bg-interactive-hover transition-colors disabled:opacity-50"
>
{copying ? 'Copying...' : 'Copy Role'}
</button>
{!isEveryone && ( {!isEveryone && (
<>
<div className="w-px h-5 bg-white/10" />
<button <button
onClick={handleDelete} onClick={handleDelete}
disabled={deleting} disabled={deleting}
@@ -436,11 +465,11 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) {
> >
{deleting ? 'Deleting...' : confirmDelete ? 'Confirm?' : 'Delete Role'} {deleting ? 'Deleting...' : confirmDelete ? 'Confirm?' : 'Delete Role'}
</button> </button>
</>
)} )}
</div> </div>
</div> </div>
</div> </div>
)}
</div> </div>
); );
} }