Skip to content

Org Roles Form

Install this component from the Scintillar registry.

Roles & Permissions
Define roles and assign permissions to control access across your organization.
OwnerBuilt-in5 permissions

Full access to everything

Admin4 permissions

Manage resources and members

Member2 permissions

Basic access

Installation

npx shadcn@latest add @scintillar/org-roles-form

Source

"use client"

import { useState } from "react"
import { Plus, Trash2, Shield, ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { FormSection } from "@/registry/new-york/blocks/form-section/form-section"

/** A permission that can be granted to a role. */
export interface Permission {
  /** Unique permission ID. */
  id: string
  /** Display label. */
  label: string
  /** Description of what this permission allows. */
  description?: string
  /** Permission category/group. */
  category: string
}

/** A role with its assigned permissions. */
export interface Role {
  /** Unique role ID. */
  id: string
  /** Display name. */
  name: string
  /** Role description. */
  description?: string
  /** Whether this is a built-in role that cannot be deleted. */
  builtIn?: boolean
  /** IDs of permissions granted to this role. */
  permissions: string[]
}

export interface OrgRolesFormProps {
  /** Available roles. */
  roles: Role[]
  /** Available permissions. */
  permissions: Permission[]
  /** Called when roles are updated. */
  onRolesChange?: (roles: Role[]) => void
  /** Called when a new role is created. */
  onCreateRole?: () => void
  /** Called when a role is deleted. */
  onDeleteRole?: (roleId: string) => void
  /** Additional CSS classes. */
  className?: string
}

/** Organization role and permission management form with an RBAC permission matrix. */
export function OrgRolesForm({
  roles,
  permissions,
  onRolesChange,
  onCreateRole,
  onDeleteRole,
  className,
}: OrgRolesFormProps) {
  const [expandedRole, setExpandedRole] = useState<string | null>(null)

  // Group permissions by category
  const categories = Array.from(new Set(permissions.map((p) => p.category)))
  const permsByCategory = categories.map((cat) => ({
    category: cat,
    permissions: permissions.filter((p) => p.category === cat),
  }))

  function togglePermission(roleId: string, permId: string) {
    const updated = roles.map((r) => {
      if (r.id !== roleId) return r
      const has = r.permissions.includes(permId)
      return {
        ...r,
        permissions: has
          ? r.permissions.filter((p) => p !== permId)
          : [...r.permissions, permId],
      }
    })
    onRolesChange?.(updated)
  }

  return (
    <FormSection
      title="Roles & Permissions"
      description="Define roles and assign permissions to control access across your organization."
      className={className}
      footer={
        <Button onClick={onCreateRole}>
          <Plus className="size-4 mr-1.5" />
          Create role
        </Button>
      }
    >
      <div className="space-y-4">
        {/* Roles list */}
        {roles.map((role) => (
          <div key={role.id} className="rounded-lg border">
            {/* Role header */}
            <div
              className="flex items-center gap-3 p-3 cursor-pointer hover:bg-muted/30 transition-colors"
              onClick={() => setExpandedRole(expandedRole === role.id ? null : role.id)}
            >
              <Shield className="size-4 text-muted-foreground shrink-0" />
              <div className="flex-1 min-w-0">
                <div className="flex items-center gap-2">
                  <span className="text-sm font-medium">{role.name}</span>
                  {role.builtIn && <Badge variant="secondary" className="text-[10px]">Built-in</Badge>}
                  <Badge variant="outline" className="text-[10px]">
                    {role.permissions.length} permission{role.permissions.length !== 1 ? "s" : ""}
                  </Badge>
                </div>
                {role.description && (
                  <p className="text-xs text-muted-foreground mt-0.5">{role.description}</p>
                )}
              </div>
              <div className="flex items-center gap-1">
                {!role.builtIn && onDeleteRole && (
                  <Button
                    variant="ghost"
                    size="icon-xs"
                    onClick={(e) => { e.stopPropagation(); onDeleteRole(role.id) }}
                    aria-label={`Delete ${role.name}`}
                  >
                    <Trash2 className="size-3.5 text-muted-foreground" />
                  </Button>
                )}
                <ChevronDown className={cn(
                  "size-4 text-muted-foreground transition-transform",
                  expandedRole === role.id && "rotate-180"
                )} />
              </div>
            </div>

            {/* Permission matrix */}
            {expandedRole === role.id && (
              <div className="border-t">
                {permsByCategory.map((group) => (
                  <div key={group.category}>
                    <div className="px-3 py-2 bg-muted/20">
                      <span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
                        {group.category}
                      </span>
                    </div>
                    {group.permissions.map((perm) => (
                      <div
                        key={perm.id}
                        className="flex items-center gap-3 px-3 py-2 border-t border-border/50 hover:bg-muted/10"
                      >
                        <Checkbox
                          id={`${role.id}-${perm.id}`}
                          checked={role.permissions.includes(perm.id)}
                          onCheckedChange={() => togglePermission(role.id, perm.id)}
                          disabled={role.builtIn}
                        />
                        <div className="flex-1 min-w-0">
                          <Label
                            htmlFor={`${role.id}-${perm.id}`}
                            className="text-sm cursor-pointer"
                          >
                            {perm.label}
                          </Label>
                          {perm.description && (
                            <p className="text-xs text-muted-foreground">{perm.description}</p>
                          )}
                        </div>
                      </div>
                    ))}
                  </div>
                ))}
              </div>
            )}
          </div>
        ))}

        {roles.length === 0 && (
          <p className="text-sm text-muted-foreground text-center py-6">
            No roles defined yet. Create one to get started.
          </p>
        )}
      </div>
    </FormSection>
  )
}