{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "openlmis-role-assignments",
  "title": "Role Assignments",
  "description": "One role type's assignments for a user: search, sortable columns that fold into one on a narrow card, a role's rights in a popover, unsaved and ignored badges, removal and Add Role, plus the role types and helpers the user, roles and profile pages share.",
  "dependencies": [
    "@tanstack/react-table@^9",
    "lucide-react@^1",
    "@base-ui/react@^1"
  ],
  "registryDependencies": [
    "button",
    "dropdown-menu",
    "skeleton",
    "https://registry.soldevelo.com/r/openlmis-data-table.json",
    "https://registry.soldevelo.com/r/openlmis-label-popover.json",
    "https://registry.soldevelo.com/r/openlmis-search-input.json",
    "https://registry.soldevelo.com/r/openlmis-status-badge.json"
  ],
  "files": [
    {
      "path": "registry/blocks/openlmis/role-assignments/role-assignments.ts",
      "content": "/** What a right is about; a role's rights all share one type, which is the role's type. */\nexport type RightType =\n  | \"SUPERVISION\"\n  | \"ORDER_FULFILLMENT\"\n  | \"REPORTS\"\n  | \"GENERAL_ADMIN\"\n\nexport type Right = {\n  name: string\n  type: RightType\n}\n\nexport type Role = {\n  id: string\n  name: string\n  description?: string | null\n  rights: Right[]\n}\n\nexport type Program = {\n  id: string\n  name: string\n}\n\n/** The node's facility comes as a reference only; its name is in the facilities lookup. */\nexport type SupervisoryNode = {\n  id: string\n  name: string\n  facility?: { id: string } | null\n}\n\nexport type Facility = {\n  id: string\n  code: string\n  name: string\n}\n\n/** A role granted to a user; which ids are set depends on the role's type. */\nexport type RoleAssignment = {\n  roleId: string\n  programId?: string | null\n  supervisoryNodeId?: string | null\n  warehouseId?: string | null\n}\n\n/** The tabs of the roles page, one per role type, in the order OpenLMIS shows them. */\nexport const ROLE_TABS = [\n  { id: \"supervision\", type: \"SUPERVISION\", label: \"Supervision\" },\n  { id: \"fulfillment\", type: \"ORDER_FULFILLMENT\", label: \"Fulfillment\" },\n  { id: \"reports\", type: \"REPORTS\", label: \"Reports\" },\n  { id: \"administration\", type: \"GENERAL_ADMIN\", label: \"Administration\" },\n] as const satisfies readonly { id: string; type: RightType; label: string }[]\n\nexport type RoleTab = (typeof ROLE_TABS)[number]\n\n/** A supervision role with no supervisory node applies at the user's home facility. */\nexport function isHomeFacilityRole(assignment: RoleAssignment) {\n  return Boolean(assignment.programId) && !assignment.supervisoryNodeId\n}\n\n/** Equal for two assignments that grant the same thing; the server keeps only one of them. */\nexport function assignmentKey(assignment: RoleAssignment) {\n  const { roleId, programId, supervisoryNodeId, warehouseId } = assignment\n  return [\n    roleId,\n    programId ?? \"\",\n    supervisoryNodeId ?? \"\",\n    warehouseId ?? \"\",\n  ].join(\"|\")\n}\n\n/** Only the ids the API reads, with empty ones left out. */\nexport function toSavedAssignment({\n  roleId,\n  programId,\n  supervisoryNodeId,\n  warehouseId,\n}: RoleAssignment): RoleAssignment {\n  return {\n    roleId,\n    ...(programId && { programId }),\n    ...(supervisoryNodeId && { supervisoryNodeId }),\n    ...(warehouseId && { warehouseId }),\n  }\n}\n\nexport const roleTypeOf = (role: Role | undefined): RightType | undefined =>\n  role?.rights[0]?.type\n\n/** The type an assignment belongs to: its role's, or what its ids imply if the role is gone. */\nexport function assignmentType(\n  assignment: RoleAssignment,\n  roles: ReadonlyMap<string, Role>\n): RightType {\n  const type = roleTypeOf(roles.get(assignment.roleId))\n  if (type) return type\n  if (assignment.programId) return \"SUPERVISION\"\n  if (assignment.warehouseId) return \"ORDER_FULFILLMENT\"\n  return \"GENERAL_ADMIN\"\n}\n\n/** Adds the assignments not held yet, keeping the current order, and counts both kinds. */\nexport function mergeAssignments(\n  current: readonly RoleAssignment[],\n  incoming: readonly RoleAssignment[]\n) {\n  const keys = new Set(current.map(assignmentKey))\n  const added: RoleAssignment[] = []\n  for (const assignment of incoming) {\n    const key = assignmentKey(assignment)\n    if (keys.has(key)) continue\n    keys.add(key)\n    added.push(toSavedAssignment(assignment))\n  }\n  return {\n    assignments: [...current, ...added],\n    added: added.length,\n    skipped: incoming.length - added.length,\n  }\n}\n\n/** How many assignments were added and removed since `saved`; zero when nothing changed. */\nexport function countChanges(\n  saved: readonly RoleAssignment[],\n  draft: readonly RoleAssignment[]\n) {\n  const savedKeys = new Set(saved.map(assignmentKey))\n  const draftKeys = new Set(draft.map(assignmentKey))\n  const added = [...draftKeys].filter((key) => !savedKeys.has(key)).length\n  const removed = [...savedKeys].filter((key) => !draftKeys.has(key)).length\n  return added + removed\n}\n\nexport function countByType(\n  assignments: readonly RoleAssignment[],\n  roles: ReadonlyMap<string, Role>\n) {\n  const counts: Record<RightType, number> = {\n    SUPERVISION: 0,\n    ORDER_FULFILLMENT: 0,\n    REPORTS: 0,\n    GENERAL_ADMIN: 0,\n  }\n  for (const assignment of assignments)\n    counts[assignmentType(assignment, roles)] += 1\n  return counts\n}\n\nexport type RoleLookups = {\n  roles: ReadonlyMap<string, Role>\n  programs: ReadonlyMap<string, Program>\n  /** Missing while the nodes still load. */\n  nodes?: ReadonlyMap<string, SupervisoryNode> | undefined\n  /** Missing while the facilities still load. */\n  facilities?: ReadonlyMap<string, Facility> | undefined\n}\n\nexport const byId = <T extends { id: string }>(items: readonly T[]) =>\n  new Map(items.map((item) => [item.id, item]))\n\n/** One row of a roles tab; a name is undefined when its record is unknown or still loading. */\nexport type RoleRow = {\n  id: string\n  assignment: RoleAssignment\n  role: string | undefined\n  /** The role's description, shown with its rights. */\n  description: string | undefined\n  /** What the role grants, as labels. */\n  rights: string[]\n  program: string | undefined\n  /** The supervisory node, or for a home facility role, the home facility. */\n  node: string | undefined\n  nodeFacility: string | undefined\n  facility: string | undefined\n  isHomeFacility: boolean\n  /** A home facility role for a user with no home facility grants nothing. */\n  isIgnored: boolean\n  isUnsaved: boolean\n}\n\ntype RowContext = {\n  lookups: RoleLookups\n  savedKeys: ReadonlySet<string>\n  homeFacilityId: string | null | undefined\n}\n\nfunction toRoleRow(\n  assignment: RoleAssignment,\n  { lookups, savedKeys, homeFacilityId }: RowContext\n): RoleRow {\n  const key = assignmentKey(assignment)\n  const role = lookups.roles.get(assignment.roleId)\n  const isHomeFacility = isHomeFacilityRole(assignment)\n  const node = assignment.supervisoryNodeId\n    ? lookups.nodes?.get(assignment.supervisoryNodeId)\n    : undefined\n  const facilityName = (id: string | null | undefined) =>\n    id ? lookups.facilities?.get(id)?.name : undefined\n\n  return {\n    id: key,\n    assignment,\n    role: role?.name,\n    description: role?.description ?? undefined,\n    rights: (role?.rights ?? []).map((right) => rightLabel(right.name)),\n    program: assignment.programId\n      ? lookups.programs.get(assignment.programId)?.name\n      : undefined,\n    node: node?.name,\n    nodeFacility: isHomeFacility\n      ? facilityName(homeFacilityId)\n      : facilityName(node?.facility?.id),\n    facility: facilityName(assignment.warehouseId),\n    isHomeFacility,\n    isIgnored: isHomeFacility && !homeFacilityId,\n    isUnsaved: !savedKeys.has(key),\n  }\n}\n\n/** The rows of one tab, by role, then program, then node. */\nexport function toRoleRows(\n  assignments: readonly RoleAssignment[],\n  type: RightType,\n  context: RowContext\n): RoleRow[] {\n  const rows = assignments\n    .filter(\n      (assignment) => assignmentType(assignment, context.lookups.roles) === type\n    )\n    .map((assignment) => toRoleRow(assignment, context))\n  // oxlint-disable-next-line unicorn/no-array-sort -- sorts a fresh array; toSorted needs the ES2023 lib\n  return rows.sort(compareRows(\"role\", false))\n}\n\nexport const ROLE_SORT_FIELDS = [\"role\", \"program\", \"node\", \"facility\"] as const\n\nexport type RoleSortField = (typeof ROLE_SORT_FIELDS)[number]\n\n// Ties fall through to the next field, so equal roles still list in a steady order.\nconst SORT_ORDER: Record<RoleSortField, RoleSortField[]> = {\n  role: [\"role\", \"program\", \"node\", \"facility\"],\n  program: [\"program\", \"role\", \"node\"],\n  node: [\"node\", \"role\", \"program\"],\n  facility: [\"facility\", \"role\"],\n}\n\n// A fixed locale, so a server render and the browser sort the rows the same way.\nconst collator = new Intl.Collator(\"en-US\", {\n  sensitivity: \"base\",\n  numeric: true,\n})\n\nexport function compareRows(field: RoleSortField, desc: boolean) {\n  return (a: RoleRow, b: RoleRow) => {\n    for (const key of SORT_ORDER[field]) {\n      const order = collator.compare(a[key] ?? \"\", b[key] ?? \"\")\n      if (order !== 0) return key === field && desc ? -order : order\n    }\n    return 0\n  }\n}\n\n/** Lower case without accents, so \"deposito\" finds \"Depósito\". */\nconst fold = (text: string) =>\n  text.normalize(\"NFD\").replace(/\\p{M}/gu, \"\").toLocaleLowerCase()\n\n/** Rows where any shown name contains `query`, ignoring case and accents. */\nexport function filterRows(rows: readonly RoleRow[], query: string) {\n  const term = fold(query.trim())\n  if (!term) return [...rows]\n  const matches = (value: string | undefined) =>\n    value !== undefined && fold(value).includes(term)\n  return rows.filter((row) =>\n    [row.role, row.program, row.node, row.nodeFacility, row.facility].some(\n      matches\n    )\n  )\n}\n\n// Kept as written rather than capitalized like a word.\nconst ACRONYMS: Record<string, string> = {\n  BUQ: \"BUQ\",\n  CCE: \"CCE\",\n  DHIS2: \"DHIS2\",\n  MOH: \"MOH\",\n  PODS: \"PODs\",\n  PORALG: \"PORALG\",\n}\n\n/** A right's code as words, e.g. `PODS_MANAGE` as \"PODs Manage\"; swap in your own labels. */\nexport function rightLabel(name: string) {\n  return name\n    .split(\"_\")\n    .filter(Boolean)\n    .map(\n      (word) => ACRONYMS[word] ?? word.charAt(0) + word.slice(1).toLowerCase()\n    )\n    .join(\" \")\n}",
      "type": "registry:lib",
      "target": "components/blocks/openlmis/role-assignments/role-assignments.ts"
    },
    {
      "path": "registry/blocks/openlmis/role-assignments/role-assignments-table.tsx",
      "content": "\"use client\"\n\nimport {\n  createColumnHelper,\n  functionalUpdate,\n  type PaginationState,\n  type SortingState,\n  useTable,\n} from \"@tanstack/react-table\"\nimport {\n  EllipsisIcon,\n  PlusIcon,\n  SearchXIcon,\n  ShieldIcon,\n  Trash2Icon,\n} from \"lucide-react\"\nimport { useMemo, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  DataTable,\n  DataTableColumnHeader,\n  DataTableEmpty,\n  DataTableError,\n  type DataTableFeatures,\n  DataTablePagination,\n  DataTableSkeleton,\n  dataTableFeatures,\n} from \"@/components/blocks/openlmis/data-table/data-table\"\nimport { useContainerSize } from \"@/components/blocks/openlmis/data-table/responsive-columns\"\nimport { SearchInput } from \"@/components/openlmis/search-input/search-input\"\nimport { StatusBadge } from \"@/components/openlmis/status-badge\"\n\nimport {\n  compareRows,\n  filterRows,\n  type RoleRow,\n  type RoleSortField,\n  type RoleTab,\n} from \"@/components/blocks/openlmis/role-assignments/role-assignments\"\nimport { RoleRightsPopover } from \"@/components/blocks/openlmis/role-assignments/role-rights-popover\"\n\n/** Whether the node and facility names have arrived; they load after the rows, and may fail. */\nexport type LookupStatus = {\n  nodes: \"pending\" | \"ready\" | \"failed\"\n  facilities: \"pending\" | \"ready\" | \"failed\"\n}\n\ntype NameStatus = LookupStatus[\"nodes\"]\n\nconst EMPTY_TEXT: Record<RoleTab[\"id\"], string> = {\n  supervision:\n    \"Supervision roles let this user work on a program's requisitions, at a supervisory node or at their home facility.\",\n  fulfillment: \"Fulfillment roles let this user handle orders at a facility.\",\n  reports: \"Reports roles let this user see reports.\",\n  administration: \"Administration roles let this user manage the system.\",\n}\n\nconst columnHelper = createColumnHelper<DataTableFeatures, RoleRow>()\n\n/** A name; a placeholder while it loads, a dash if its lookup failed, \"Unknown\" if it is gone. */\nfunction Name({\n  value,\n  status = \"ready\",\n}: {\n  value: string | undefined\n  status?: NameStatus\n}) {\n  if (value !== undefined) return <span className=\"truncate\">{value}</span>\n  if (status === \"pending\") return <Skeleton className=\"h-4 w-2/3\" />\n  if (status === \"failed\")\n    return <span className=\"text-muted-foreground\">-</span>\n  return <span className=\"text-muted-foreground\">Unknown</span>\n}\n\n/** The node with its facility beneath, or Home Facility with the user's home facility. */\nfunction NodeCell({ row, status }: { row: RoleRow; status: LookupStatus }) {\n  const facilityPending =\n    status.facilities === \"pending\" ||\n    (!row.isHomeFacility && status.nodes === \"pending\")\n  return (\n    <span className=\"flex min-w-0 flex-col\">\n      {row.isHomeFacility ? (\n        <span className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1\">\n          <span className=\"truncate\">Home Facility</span>\n          {row.isIgnored && <StatusBadge tone=\"warning\">Ignored</StatusBadge>}\n        </span>\n      ) : (\n        <Name status={status.nodes} value={row.node} />\n      )}\n      {row.isIgnored ? null : row.nodeFacility !== undefined ? (\n        <span className=\"truncate text-xs text-muted-foreground\">\n          {row.nodeFacility}\n        </span>\n      ) : (\n        facilityPending && <Skeleton className=\"mt-1 h-3 w-1/2\" />\n      )}\n    </span>\n  )\n}\n\ntype ColumnOptions = {\n  tab: RoleTab\n  /** Too narrow for a column each, so the role cell carries the rest on lines of its own. */\n  compact: boolean\n  status: LookupStatus\n  /** Left out where the roles are only shown, e.g. on the user's own profile. */\n  onRemove?: ((row: RoleRow) => void) | undefined\n}\n\nfunction RoleCell({ row, options }: { row: RoleRow; options: ColumnOptions }) {\n  const { tab, compact, status } = options\n  return (\n    <span className=\"flex min-w-0 flex-col gap-1\">\n      <span className=\"flex min-w-0 items-center gap-2\">\n        {row.role === undefined ? (\n          <Name value={undefined} />\n        ) : (\n          <RoleRightsPopover\n            description={row.description}\n            name={row.role}\n            rights={row.rights}\n          />\n        )}\n        {row.isUnsaved && <StatusBadge tone=\"info\">Unsaved</StatusBadge>}\n      </span>\n      {compact && tab.type === \"SUPERVISION\" && (\n        <span className=\"flex min-w-0 flex-col text-xs text-muted-foreground\">\n          <Name value={row.program} />\n          <NodeCell row={row} status={status} />\n        </span>\n      )}\n      {compact && tab.type === \"ORDER_FULFILLMENT\" && (\n        <span className=\"text-xs text-muted-foreground\">\n          <Name status={status.facilities} value={row.facility} />\n        </span>\n      )}\n    </span>\n  )\n}\n\nfunction createColumns(options: ColumnOptions) {\n  const { tab, compact, status, onRemove } = options\n  const role = columnHelper.accessor(\"role\", {\n    header: ({ column }) => (\n      <DataTableColumnHeader column={column} title=\"Role\" />\n    ),\n    cell: ({ row }) => <RoleCell options={options} row={row.original} />,\n  })\n  const withActions = onRemove\n    ? [\n        columnHelper.display({\n          id: \"actions\",\n          header: () => <span className=\"sr-only\">Actions</span>,\n          meta: { className: \"w-16\" },\n          cell: ({ row }) => (\n            <RoleActions\n              onRemove={() => onRemove(row.original)}\n              role={row.original.role ?? \"Unknown\"}\n            />\n          ),\n        }),\n      ]\n    : []\n\n  if (compact) return columnHelper.columns([role, ...withActions])\n\n  if (tab.type === \"SUPERVISION\") {\n    return columnHelper.columns([\n      columnHelper.accessor(\"program\", {\n        header: ({ column }) => (\n          <DataTableColumnHeader column={column} title=\"Program\" />\n        ),\n        meta: { className: \"w-1/5\" },\n        cell: ({ getValue }) => <Name value={getValue()} />,\n      }),\n      columnHelper.accessor(\"node\", {\n        header: ({ column }) => (\n          <DataTableColumnHeader column={column} title=\"Supervisory Node\" />\n        ),\n        meta: { className: \"w-2/5\" },\n        cell: ({ row }) => <NodeCell row={row.original} status={status} />,\n      }),\n      role,\n      ...withActions,\n    ])\n  }\n  if (tab.type === \"ORDER_FULFILLMENT\") {\n    return columnHelper.columns([\n      columnHelper.accessor(\"facility\", {\n        header: ({ column }) => (\n          <DataTableColumnHeader column={column} title=\"Supplying Facility\" />\n        ),\n        meta: { className: \"w-1/2\" },\n        cell: ({ getValue }) => (\n          <Name status={status.facilities} value={getValue()} />\n        ),\n      }),\n      role,\n      ...withActions,\n    ])\n  }\n  return columnHelper.columns([role, ...withActions])\n}\n\nfunction RoleActions({\n  role,\n  onRemove,\n}: {\n  role: string\n  onRemove: () => void\n}) {\n  return (\n    <div className=\"flex justify-end\">\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          render={\n            <Button\n              aria-label={`Actions for ${role}`}\n              size=\"icon-sm\"\n              variant=\"ghost\"\n            />\n          }\n        >\n          <EllipsisIcon />\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"end\" className=\"w-auto\">\n          <DropdownMenuItem onClick={onRemove} variant=\"destructive\">\n            <Trash2Icon />\n            Remove\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  )\n}\n\ntype RoleAssignmentsTableProps = Omit<ColumnOptions, \"compact\"> & {\n  /** Every row of the tab, unfiltered; a skeleton shows until it is set. Key the table by tab to reset it. */\n  rows: RoleRow[] | undefined\n  /** The rows could not be loaded; shows an error with Try Again. */\n  failed?: boolean\n  onRetry?: () => void\n  /** Left out where the roles are only shown; hides Add Role. */\n  onAdd?: () => void\n}\n\nconst INITIAL_SORTING: SortingState = [{ id: \"role\", desc: false }]\nconst INITIAL_PAGINATION: PaginationState = { pageIndex: 0, pageSize: 10 }\n\n/** One tab's roles: a search and Add Role above a table that filters, sorts and pages them in the browser. */\nexport function RoleAssignmentsTable({\n  rows,\n  failed = false,\n  onRetry,\n  onAdd,\n  ...options\n}: RoleAssignmentsTableProps) {\n  const [query, setQuery] = useState(\"\")\n  const [sorting, setSorting] = useState(INITIAL_SORTING)\n  const [pagination, setPagination] = useState(INITIAL_PAGINATION)\n  const [measure, size] = useContainerSize<HTMLDivElement>()\n  // Below `xl` there is no room for a column per name.\n  const compact =\n    size === \"none\" || size === \"sm\" || size === \"md\" || size === \"lg\"\n\n  const { tab, status, onRemove } = options\n  const columns = useMemo(\n    () => createColumns({ tab, compact, status, onRemove }),\n    [tab, compact, status, onRemove]\n  )\n\n  const [sort = INITIAL_SORTING[0]] = sorting\n  const matching = useMemo(() => {\n    const filtered = filterRows(rows ?? [], query)\n    // oxlint-disable-next-line unicorn/no-array-sort -- sorts the fresh filtered array\n    return filtered.sort(\n      compareRows((sort?.id ?? \"role\") as RoleSortField, sort?.desc ?? false)\n    )\n  }, [rows, query, sort])\n  // A removal can leave the page past the end, so it is held to the last page that exists.\n  const lastPage = Math.max(\n    0,\n    Math.ceil(matching.length / pagination.pageSize) - 1\n  )\n  // Memoized: the table compares controlled state by reference and would reset it every render.\n  const page = useMemo(\n    () => ({\n      ...pagination,\n      pageIndex: Math.min(pagination.pageIndex, lastPage),\n    }),\n    [pagination, lastPage]\n  )\n  const data = useMemo(\n    () =>\n      matching.slice(\n        page.pageIndex * page.pageSize,\n        (page.pageIndex + 1) * page.pageSize\n      ),\n    [matching, page.pageIndex, page.pageSize]\n  )\n\n  const table = useTable({\n    features: dataTableFeatures,\n    columns,\n    data,\n    getRowId: (row) => row.id,\n    rowCount: matching.length,\n    manualPagination: true,\n    manualSorting: true,\n    enableSortingRemoval: false,\n    state: { sorting, pagination: page },\n    onSortingChange: (updater) => {\n      setSorting(functionalUpdate(updater, sorting))\n      setPagination((current) => ({ ...current, pageIndex: 0 }))\n    },\n    onPaginationChange: (updater) =>\n      setPagination(functionalUpdate(updater, page)),\n  })\n\n  const clearSearch = () => {\n    setQuery(\"\")\n    setPagination((current) => ({ ...current, pageIndex: 0 }))\n  }\n\n  return (\n    <div className=\"@container/roles flex flex-col gap-4\" ref={measure}>\n      <div className=\"flex flex-wrap items-center gap-2\">\n        <div className=\"min-w-0 flex-1 @xl/roles:w-72 @xl/roles:flex-none\">\n          <SearchInput\n            label=\"Search roles, programs, nodes and facilities\"\n            onValueChange={(value) => {\n              setQuery(value)\n              setPagination((current) => ({ ...current, pageIndex: 0 }))\n            }}\n            placeholder=\"Search roles...\"\n            value={query}\n          />\n        </div>\n        {onAdd && (\n          <Button className=\"ms-auto\" onClick={onAdd}>\n            <PlusIcon data-icon=\"inline-start\" />\n            Add Role\n          </Button>\n        )}\n      </div>\n      {failed && onRetry ? (\n        <DataTableError\n          description=\"Check your connection and try again.\"\n          onRetry={onRetry}\n          title=\"Could Not Load Roles\"\n        />\n      ) : rows === undefined ? (\n        <DataTableSkeleton rowCount={5} table={table} />\n      ) : (\n        <DataTable\n          empty={\n            rows.length === 0 ? (\n              <DataTableEmpty\n                action={\n                  onAdd && (\n                    <Button onClick={onAdd} variant=\"outline\">\n                      <PlusIcon data-icon=\"inline-start\" />\n                      Add Role\n                    </Button>\n                  )\n                }\n                description={\n                  onAdd\n                    ? EMPTY_TEXT[tab.id]\n                    : \"Ask an administrator if you need a role of this type.\"\n                }\n                icon={<ShieldIcon />}\n                title=\"No Roles Yet\"\n              />\n            ) : (\n              <DataTableEmpty\n                action={\n                  <Button onClick={clearSearch} variant=\"destructive\">\n                    Clear Search\n                  </Button>\n                }\n                description=\"Try a different search.\"\n                icon={<SearchXIcon />}\n                title=\"No Matching Roles\"\n              />\n            )\n          }\n          footer={matching.length > 0 && <DataTablePagination table={table} />}\n          table={table}\n        />\n      )}\n    </div>\n  )\n}",
      "type": "registry:component",
      "target": "components/blocks/openlmis/role-assignments/role-assignments-table.tsx"
    },
    {
      "path": "registry/blocks/openlmis/role-assignments/role-rights-popover.tsx",
      "content": "import { CheckIcon } from \"lucide-react\"\n\nimport { LabelPopover } from \"@/components/openlmis/label-popover\"\n\ntype RoleRightsPopoverProps = {\n  /** The role's name, shown beside the button and as the popover's title. */\n  name: string\n  /** The rights the role grants, already labelled; they are sorted for you. */\n  rights: readonly string[]\n  /** Shown instead of the rights count when the role has one. */\n  description?: string | null\n}\n\n/** The role's name with a button beside it that shows what the role lets its holder do. */\nexport function RoleRightsPopover({\n  name,\n  rights,\n  description,\n}: RoleRightsPopoverProps) {\n  return (\n    <LabelPopover\n      ariaLabel={`${name} Rights`}\n      description={description || rightsCount(rights.length)}\n      label={name}\n      title={`${name} Rights`}\n    >\n      {rights.length > 0 && <RightsList rights={rights} />}\n    </LabelPopover>\n  )\n}\n\n/** Rendered only while the popover is open, so a table of roles does no work for closed ones. */\nfunction RightsList({ rights }: { rights: readonly string[] }) {\n  // oxlint-disable-next-line unicorn/no-array-sort -- sorts a fresh copy; toSorted needs the ES2023 lib\n  const sorted = [...rights].sort()\n\n  return (\n    <ul className=\"flex max-h-64 flex-col gap-1.5 overflow-y-auto\">\n      {sorted.map((right) => (\n        <li className=\"flex items-center gap-2\" key={right}>\n          <CheckIcon\n            aria-hidden=\"true\"\n            className=\"size-4 shrink-0 text-success\"\n          />\n          {right}\n        </li>\n      ))}\n    </ul>\n  )\n}\n\nfunction rightsCount(count: number) {\n  if (count === 0) return \"This role grants no rights.\"\n  return count === 1\n    ? \"The 1 right this role grants.\"\n    : `The ${count} rights this role grants.`\n}",
      "type": "registry:component",
      "target": "components/blocks/openlmis/role-assignments/role-rights-popover.tsx"
    }
  ],
  "cssVars": {
    "light": {
      "destructive": "oklch(0.577 0.245 27.325)",
      "destructive-foreground": "oklch(0.985 0 0)",
      "success": "oklch(0.596 0.145 163.225)",
      "success-foreground": "oklch(0.985 0 0)",
      "warning": "oklch(0.666 0.179 58.318)",
      "warning-foreground": "oklch(0.985 0 0)",
      "info": "oklch(0.55 0.13 237)",
      "info-foreground": "oklch(0.985 0 0)"
    },
    "dark": {
      "destructive": "oklch(0.704 0.191 22.216)",
      "destructive-foreground": "oklch(0.21 0.04 25)",
      "success": "oklch(0.696 0.17 162.48)",
      "success-foreground": "oklch(0.21 0.04 163)",
      "warning": "oklch(0.769 0.188 70.08)",
      "warning-foreground": "oklch(0.22 0.04 60)",
      "info": "oklch(0.7 0.14 237)",
      "info-foreground": "oklch(0.2 0.04 240)"
    }
  },
  "meta": {
    "project": "openlmis",
    "height": "354px"
  },
  "docs": "Requires a `base-*` style in components.json (the `shadcn init` default). Legacy `new-york`/`radix-*` styles install Radix primitives, which reject the `render` prop this item uses.",
  "categories": [
    "administration",
    "data-table"
  ],
  "type": "registry:block"
}
