diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 87e4281ae..156cabd99 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -206,7 +206,7 @@ export default function TeamManagementPage() { ); const handleUpdateRole = useCallback( - async (roleId: number, data: { permissions?: string[] }): Promise => { + async (roleId: number, data: { name?: string; description?: string | null; permissions?: string[]; is_default?: boolean }): Promise => { const request: UpdateRoleRequest = { search_space_id: searchSpaceId, role_id: roleId, @@ -944,7 +944,7 @@ function RolesTab({ roles, groupedPermissions, loading, - onUpdateRole: _onUpdateRole, + onUpdateRole, onDeleteRole, onCreateRole, canUpdate, @@ -954,7 +954,7 @@ function RolesTab({ roles: Role[]; groupedPermissions: Record; loading: boolean; - onUpdateRole: (roleId: number, data: { permissions?: string[] }) => Promise; + onUpdateRole: (roleId: number, data: { name?: string; description?: string | null; permissions?: string[]; is_default?: boolean }) => Promise; onDeleteRole: (roleId: number) => Promise; onCreateRole: (data: CreateRoleRequest["data"]) => Promise; canUpdate: boolean; @@ -962,6 +962,7 @@ function RolesTab({ canCreate: boolean; }) { const [showCreateRole, setShowCreateRole] = useState(false); + const [editingRoleId, setEditingRoleId] = useState(null); if (loading) { return ( @@ -997,6 +998,20 @@ function RolesTab({ /> )} + {/* Edit Role Form */} + {editingRoleId !== null && (() => { + const roleToEdit = roles.find((r) => r.id === editingRoleId); + if (!roleToEdit) return null; + return ( + setEditingRoleId(null)} + /> + ); + })()} + {/* Roles Grid */}
{roles.map((role, index) => ( @@ -1055,12 +1070,13 @@ function RolesTab({ - + e.preventDefault()} + > {canUpdate && ( { - // TODO: Implement edit role dialog/modal - }} + onClick={() => setEditingRoleId(role.id)} > Edit Role @@ -2026,3 +2042,363 @@ function CreateRoleSection({ ); } + +function EditRoleSection({ + role, + groupedPermissions, + onUpdateRole, + onCancel, +}: { + role: Role; + groupedPermissions: Record; + onUpdateRole: (roleId: number, data: { name?: string; description?: string | null; permissions?: string[]; is_default?: boolean }) => Promise; + onCancel: () => void; +}) { + const [saving, setSaving] = useState(false); + const [name, setName] = useState(role.name); + const [description, setDescription] = useState(role.description || ""); + const [selectedPermissions, setSelectedPermissions] = useState(role.permissions); + const [isDefault, setIsDefault] = useState(role.is_default); + const [expandedCategories, setExpandedCategories] = useState([]); + + // Sort categories by order + const sortedCategories = useMemo(() => { + return Object.keys(groupedPermissions).sort((a, b) => { + const orderA = CATEGORY_CONFIG[a]?.order ?? 99; + const orderB = CATEGORY_CONFIG[b]?.order ?? 99; + return orderA - orderB; + }); + }, [groupedPermissions]); + + const handleSave = async () => { + if (!name.trim()) { + toast.error("Please enter a role name"); + return; + } + + setSaving(true); + try { + await onUpdateRole(role.id, { + name: name.trim(), + description: description.trim() || null, + permissions: selectedPermissions, + is_default: isDefault, + }); + toast.success("Role updated successfully"); + onCancel(); + } catch (error) { + console.error("Failed to update role:", error); + toast.error("Failed to update role"); + } finally { + setSaving(false); + } + }; + + const togglePermission = useCallback((perm: string) => { + setSelectedPermissions((prev) => + prev.includes(perm) ? prev.filter((p) => p !== perm) : [...prev, perm] + ); + }, []); + + const toggleCategory = useCallback( + (category: string) => { + const categoryPerms = groupedPermissions[category]?.map((p) => p.value) || []; + const allSelected = categoryPerms.every((p) => selectedPermissions.includes(p)); + + if (allSelected) { + setSelectedPermissions((prev) => prev.filter((p) => !categoryPerms.includes(p))); + } else { + setSelectedPermissions((prev) => [...new Set([...prev, ...categoryPerms])]); + } + }, + [groupedPermissions, selectedPermissions] + ); + + const toggleCategoryExpanded = useCallback((category: string) => { + setExpandedCategories((prev) => + prev.includes(category) ? prev.filter((c) => c !== category) : [...prev, category] + ); + }, []); + + const getCategoryStats = useCallback( + (category: string) => { + const perms = groupedPermissions[category] || []; + const selected = perms.filter((p) => selectedPermissions.includes(p.value)).length; + return { selected, total: perms.length, allSelected: selected === perms.length }; + }, + [groupedPermissions, selectedPermissions] + ); + + return ( + + + +
+
+
+ +
+
+ Edit Role + + Modify permissions for "{role.name}" + +
+
+ +
+
+ + {/* Role Details */} +
+
+ + setName(e.target.value)} + /> +
+
+ + setDescription(e.target.value)} + /> +
+
+ + {/* Default Role Checkbox */} +
+ setIsDefault(checked === true)} + /> +
+ +

+ New members without a specific role will be assigned this role +

+
+
+ + {/* Permissions Section */} +
+
+ + +
+ +
+ {sortedCategories.map((category) => { + const config = CATEGORY_CONFIG[category] || { + label: category, + icon: FileText, + description: "", + order: 99, + }; + const IconComponent = config.icon; + const stats = getCategoryStats(category); + const isExpanded = expandedCategories.includes(category); + const perms = groupedPermissions[category] || []; + + return ( +
+ {/* Category Header */} +
toggleCategoryExpanded(category)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggleCategoryExpanded(category); + } + }} + tabIndex={0} + role="button" + > +
+
0 ? "bg-primary/10" : "bg-muted" + )} + > + 0 ? "text-primary" : "text-muted-foreground" + )} + /> +
+
+
+ {config.label} + 0 ? "default" : "secondary"} + className="text-xs h-5" + > + {stats.selected}/{stats.total} + +
+

+ {config.description} +

+
+
+
+ toggleCategory(category)} + onClick={(e) => e.stopPropagation()} + aria-label={`Select all ${config.label} permissions`} + /> + + + + + +
+
+ + {/* Permissions List */} + {isExpanded && ( + +
+ {perms.map((perm) => { + const action = perm.value.split(":")[1]; + const actionConfig = ACTION_DISPLAY[action] || { + label: action, + color: "text-gray-600 bg-gray-500/10", + }; + const isSelected = selectedPermissions.includes(perm.value); + + return ( +
togglePermission(perm.value)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + togglePermission(perm.value); + } + }} + tabIndex={0} + role="checkbox" + aria-checked={isSelected} + > +
+ togglePermission(perm.value)} + onClick={(e) => e.stopPropagation()} + /> +
+
+ + {actionConfig.label} + +
+

+ {perm.description} +

+
+
+
+ ); + })} +
+
+ )} +
+ ); + })} +
+
+ + {/* Actions */} +
+ + +
+
+
+
+ ); +}