@asteby/metacore-runtime-react 18.15.0 → 18.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/permissions-manager.d.ts +54 -17
- package/dist/permissions-manager.d.ts.map +1 -1
- package/dist/permissions-manager.js +78 -50
- package/package.json +1 -1
- package/src/__tests__/permissions-manager.test.tsx +215 -91
- package/src/index.ts +6 -0
- package/src/permissions-manager.tsx +166 -130
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.16.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cc4851a: PermissionsManager: la elección de módulo pasa de un **árbol con acordeones/folders** a una **lista plana idéntica al sidebar**.
|
|
8
|
+
- Cada grupo se dibuja como un **header gris no colapsable** (uppercase tracking, estilo "Módulos"/"Sistema" del sidebar) seguido de sus módulos como **filas clickeables** (ícono + label + badge contador N/M). El click directo en una fila selecciona el módulo y muestra su grid de acciones a la derecha. CERO Collapsible/acordeón/folder. La búsqueda filtra las filas (accent/case-insensitive por label de módulo o título de grupo) y oculta los grupos sin coincidencias.
|
|
9
|
+
- **Nuevo shape de entrada (desacoplado)**: `loadModules()` ahora puede devolver `{ groups: ModuleGroup[]; general: GeneralPermissionDef[] }` donde `ModuleGroup = { title: string; modules: ModuleDef[] }` (`title: ''` → sin header) y cada módulo es `{ key, label, icon?, kind: 'model' | 'screen', actions: ActionDef[] }`. La capability final es `${module.key}.${action.key}`; para pantallas no-modelo el host manda `key: 'screen.<navKey>'` + una acción `{ key: 'access', label: 'Acceder', icon: 'Eye', kind: 'screen' }` → capability `screen.<navKey>.access`.
|
|
10
|
+
- **Retrocompat**: si `loadModules` devuelve el shape viejo `{ modules, general }` (flat, sin `kind`), se envuelve en grupos (agrupados por `addon_label`/`addon_key`, fallback "Sistema") y cada módulo se trata como `kind: 'model'`. Los hosts que aún mandan el shape viejo siguen funcionando.
|
|
11
|
+
- Tipos exportados nuevos: `ModuleGroup`, `GroupedPermissionsCatalog`, `FlatPermissionsCatalog` y `kind` en `PermissionModuleDef`/`PermissionActionDef`. Helpers exportados: `normalizeCatalogGroups`, `flattenGroups`, `filterModuleGroups` (firma actualizada a `ModuleGroup`). Se eliminó `groupModules` (reemplazado por `normalizeCatalogGroups`).
|
|
12
|
+
- Intacto: selector de rol limpio (edit/delete inline), permisos generales, dirty tracking + guardar (sync = set completo), guard de cambios sin guardar, i18n español, `createRole`/`updateRole`/`deleteRole` opcionales.
|
|
13
|
+
|
|
3
14
|
## 18.15.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { AddonLayoutProvider, useAddonLayout, useAddonLayoutControl, useDeclareA
|
|
|
9
9
|
export * from './slot';
|
|
10
10
|
export * from './capability-gate';
|
|
11
11
|
export { PermissionsProvider, useCan, usePermissionsActive, makeCan, capabilityForActionKey, modelCapability, gateTableMetadata, type CanFn, type PermissionsProviderProps, } from './permissions-context';
|
|
12
|
-
export { PermissionsManager, moduleActionCapability, moduleCapabilities, grantedCountForModule, capabilitySetsEqual, defaultActionIcon, type PermissionsManagerProps, type PermissionsCatalog, type PermissionModuleDef, type PermissionActionDef, type GeneralPermissionDef, type RoleDef, type RoleInput, } from './permissions-manager';
|
|
12
|
+
export { PermissionsManager, moduleActionCapability, moduleCapabilities, grantedCountForModule, capabilitySetsEqual, defaultActionIcon, normalizeCatalogGroups, flattenGroups, filterModuleGroups, type PermissionsManagerProps, type PermissionsCatalog, type GroupedPermissionsCatalog, type FlatPermissionsCatalog, type ModuleGroup, type PermissionModuleDef, type PermissionActionDef, type GeneralPermissionDef, type RoleDef, type RoleInput, } from './permissions-manager';
|
|
13
13
|
export * from './org-runtime-context';
|
|
14
14
|
export * from './org-runtime-provider';
|
|
15
15
|
export * from './navigation-builder';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { AddonLayoutProvider, useAddonLayout, useAddonLayoutControl, useDeclareA
|
|
|
14
14
|
export * from './slot';
|
|
15
15
|
export * from './capability-gate';
|
|
16
16
|
export { PermissionsProvider, useCan, usePermissionsActive, makeCan, capabilityForActionKey, modelCapability, gateTableMetadata, } from './permissions-context';
|
|
17
|
-
export { PermissionsManager, moduleActionCapability, moduleCapabilities, grantedCountForModule, capabilitySetsEqual, defaultActionIcon, } from './permissions-manager';
|
|
17
|
+
export { PermissionsManager, moduleActionCapability, moduleCapabilities, grantedCountForModule, capabilitySetsEqual, defaultActionIcon, normalizeCatalogGroups, flattenGroups, filterModuleGroups, } from './permissions-manager';
|
|
18
18
|
export * from './org-runtime-context';
|
|
19
19
|
export * from './org-runtime-provider';
|
|
20
20
|
export * from './navigation-builder';
|
|
@@ -1,37 +1,68 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
export interface PermissionActionDef {
|
|
3
|
-
/** Canonical action key (`index`, `create`, …,
|
|
3
|
+
/** Canonical action key (`index`, `create`, …, a custom `pagar`, or `access`). */
|
|
4
4
|
key: string;
|
|
5
|
-
/** Localized label ("Listar", "Pagar"). */
|
|
5
|
+
/** Localized label ("Listar", "Pagar", "Acceder"). */
|
|
6
6
|
label: string;
|
|
7
7
|
/** Lucide icon name from the manifest action (optional). */
|
|
8
8
|
icon?: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* `crud` for the derived CRUD set, `custom` for manifest actions,
|
|
11
|
+
* `screen` for the single `access` action of a non-model screen.
|
|
12
|
+
*/
|
|
13
|
+
kind?: 'crud' | 'custom' | 'screen' | string;
|
|
11
14
|
}
|
|
12
15
|
export interface PermissionModuleDef {
|
|
13
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Module key.
|
|
18
|
+
* - model: lowercase model table (`pos_orders`).
|
|
19
|
+
* - screen: `screen.<navKey>` (the host prefixes it).
|
|
20
|
+
*/
|
|
14
21
|
key: string;
|
|
15
|
-
/** Localized module label ("Pedidos POS"). */
|
|
22
|
+
/** Localized module label ("Pedidos POS", "Terminal"). */
|
|
16
23
|
label: string;
|
|
17
24
|
/** Module icon (lucide name) — mirrors the sidebar entry. */
|
|
18
25
|
icon?: string;
|
|
19
|
-
/**
|
|
26
|
+
/** Whether this entry is a data model or a non-model screen. */
|
|
27
|
+
kind?: 'model' | 'screen';
|
|
28
|
+
/** Owning addon key (`pos`) — legacy shape only, used for grouping. */
|
|
20
29
|
addon_key?: string;
|
|
21
|
-
/** Localized addon label ("Punto de venta") —
|
|
30
|
+
/** Localized addon label ("Punto de venta") — legacy shape only. */
|
|
22
31
|
addon_label?: string;
|
|
23
32
|
actions: PermissionActionDef[];
|
|
24
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* A sidebar-style group: a grey (non-collapsible) header + its modules.
|
|
36
|
+
* `title === ''` → no header (e.g. core/infra modules).
|
|
37
|
+
*/
|
|
38
|
+
export interface ModuleGroup {
|
|
39
|
+
title: string;
|
|
40
|
+
modules: PermissionModuleDef[];
|
|
41
|
+
}
|
|
25
42
|
export interface GeneralPermissionDef {
|
|
26
43
|
/** Full capability key (`general.work_after_hours`). */
|
|
27
44
|
key: string;
|
|
28
45
|
label: string;
|
|
29
46
|
description?: string;
|
|
30
47
|
}
|
|
31
|
-
|
|
48
|
+
/**
|
|
49
|
+
* What `loadModules()` may return.
|
|
50
|
+
*
|
|
51
|
+
* New (preferred) shape — pre-grouped flat list, mirrors the host sidebar:
|
|
52
|
+
* { groups: ModuleGroup[], general }
|
|
53
|
+
*
|
|
54
|
+
* Legacy shape (still accepted, wrapped into a single untitled group) —
|
|
55
|
+
* { modules: PermissionModuleDef[], general }
|
|
56
|
+
*/
|
|
57
|
+
export interface GroupedPermissionsCatalog {
|
|
58
|
+
groups: ModuleGroup[];
|
|
59
|
+
general: GeneralPermissionDef[];
|
|
60
|
+
}
|
|
61
|
+
export interface FlatPermissionsCatalog {
|
|
32
62
|
modules: PermissionModuleDef[];
|
|
33
63
|
general: GeneralPermissionDef[];
|
|
34
64
|
}
|
|
65
|
+
export type PermissionsCatalog = GroupedPermissionsCatalog | FlatPermissionsCatalog;
|
|
35
66
|
export interface RoleDef {
|
|
36
67
|
id: string;
|
|
37
68
|
/** Stable role key ("cashier"). */
|
|
@@ -47,7 +78,7 @@ export interface RoleInput {
|
|
|
47
78
|
color?: string;
|
|
48
79
|
}
|
|
49
80
|
export interface PermissionsManagerProps {
|
|
50
|
-
/** Loads the module×action universe + general flags. */
|
|
81
|
+
/** Loads the module×action universe + general flags (grouped or flat). */
|
|
51
82
|
loadModules: () => Promise<PermissionsCatalog>;
|
|
52
83
|
/** Loads every assignable role. */
|
|
53
84
|
loadRoles: () => Promise<RoleDef[]>;
|
|
@@ -72,13 +103,19 @@ export declare function grantedCountForModule(granted: ReadonlySet<string>, modu
|
|
|
72
103
|
export declare function capabilitySetsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean;
|
|
73
104
|
/** Default lucide icon when the manifest action doesn't declare one. */
|
|
74
105
|
export declare function defaultActionIcon(actionKey: string, kind?: string): string;
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Normalize whatever `loadModules` returned into the canonical grouped shape.
|
|
108
|
+
*
|
|
109
|
+
* - New shape (`{ groups }`): passed through (modules default to kind:'model').
|
|
110
|
+
* - Legacy flat shape (`{ modules }`): grouped by `addon_label`/`addon_key`
|
|
111
|
+
* (falling back to "Sistema") so old hosts keep their familiar buckets,
|
|
112
|
+
* every module defaulting to kind:'model'. The grey headers still render,
|
|
113
|
+
* just derived from the addon instead of the sidebar group.
|
|
114
|
+
*/
|
|
115
|
+
export declare function normalizeCatalogGroups(catalog: PermissionsCatalog): ModuleGroup[];
|
|
116
|
+
/** Flat list of every module across groups, in render order. */
|
|
117
|
+
export declare function flattenGroups(groups: ModuleGroup[]): PermissionModuleDef[];
|
|
118
|
+
/** Filter the grouped flat list by a folded query against module + group titles. */
|
|
82
119
|
export declare function filterModuleGroups(groups: ModuleGroup[], query: string): ModuleGroup[];
|
|
83
120
|
export declare function PermissionsManager({ loadModules, loadRoles, loadRolePermissions, syncRolePermissions, createRole, updateRole, deleteRole, title, className, }: PermissionsManagerProps): React.JSX.Element;
|
|
84
121
|
//# sourceMappingURL=permissions-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions-manager.d.ts","sourceRoot":"","sources":["../src/permissions-manager.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"permissions-manager.d.ts","sourceRoot":"","sources":["../src/permissions-manager.tsx"],"names":[],"mappings":"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"}
|
|
@@ -4,16 +4,18 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// Transport-agnostic: every read/write arrives via props (loaders/mutators),
|
|
5
5
|
// so each host wires them to its own api client (ops → /api/permissions/*).
|
|
6
6
|
// The capability universe (modules × actions + general flags) is derived from
|
|
7
|
-
// the installed manifests server-side; this
|
|
7
|
+
// the installed manifests + the real sidebar nav server/host-side; this
|
|
8
|
+
// component only renders it.
|
|
8
9
|
//
|
|
9
|
-
// Layout
|
|
10
|
+
// Layout — a *flat list* that mirrors the app sidebar (NO accordions/folders):
|
|
10
11
|
// header — title + "Nuevo rol" (primary) + "Guardar permisos" (green).
|
|
11
12
|
// left — Card "Rol": clean role combobox with inline Editar/Eliminar
|
|
12
13
|
// icons (no removable chip) + "Permisos Generales" flags.
|
|
13
|
-
// — Card "Módulos": a searchable
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// — Card "Módulos": a searchable flat list. Each group renders a
|
|
15
|
+
// non-collapsible grey header (uppercase tracking, like "Módulos"
|
|
16
|
+
// / "Sistema" in the sidebar) followed by its modules as clickable
|
|
17
|
+
// rows (icon + label + granted-count badge). Clicking a row selects
|
|
18
|
+
// that module and reveals its action grid on the right.
|
|
17
19
|
// right — Card "Acciones permitidas": granted counter N/M, mark-all /
|
|
18
20
|
// clear, checkbox grid (icon + label per action). Clear empty
|
|
19
21
|
// states for "pick a role" / "pick a module" / loading.
|
|
@@ -23,10 +25,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
23
25
|
// state is tracked against the loaded baseline and surfaced next to the
|
|
24
26
|
// save button.
|
|
25
27
|
import * as React from 'react';
|
|
26
|
-
import { Check,
|
|
28
|
+
import { Check, ChevronsUpDown, CheckCheck, Eraser, Pencil, Plus, Save, Search, Shield, Trash2, } from 'lucide-react';
|
|
27
29
|
import { toast } from 'sonner';
|
|
28
30
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
29
|
-
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox,
|
|
31
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Separator, Skeleton, } from '@asteby/metacore-ui/primitives';
|
|
30
32
|
import { DynamicIcon } from './dynamic-icon';
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
32
34
|
// Pure helpers (exported for hosts/tests)
|
|
@@ -66,16 +68,22 @@ export function defaultActionIcon(actionKey, kind) {
|
|
|
66
68
|
return 'Download';
|
|
67
69
|
case 'import':
|
|
68
70
|
return 'Upload';
|
|
71
|
+
case 'access':
|
|
72
|
+
return 'Eye';
|
|
69
73
|
default:
|
|
70
|
-
|
|
74
|
+
if (kind === 'crud')
|
|
75
|
+
return 'List';
|
|
76
|
+
if (kind === 'screen')
|
|
77
|
+
return 'Eye';
|
|
78
|
+
return 'Zap';
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
|
-
/** Group label fallback when a module has no addon ("Sistema" = core
|
|
81
|
+
/** Group label fallback when a legacy module has no addon ("Sistema" = core). */
|
|
74
82
|
const SYSTEM_GROUP = 'Sistema';
|
|
75
|
-
function
|
|
83
|
+
function legacyGroupLabel(mod) {
|
|
76
84
|
return mod.addon_label || mod.addon_key || SYSTEM_GROUP;
|
|
77
85
|
}
|
|
78
|
-
/** Accent-insensitive, lowercase fold for
|
|
86
|
+
/** Accent-insensitive, lowercase fold for search. */
|
|
79
87
|
function fold(s) {
|
|
80
88
|
return s
|
|
81
89
|
.normalize('NFD')
|
|
@@ -99,32 +107,57 @@ const ROLE_COLORS = [
|
|
|
99
107
|
'#ec4899',
|
|
100
108
|
'#6b7280',
|
|
101
109
|
];
|
|
102
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Normalize whatever `loadModules` returned into the canonical grouped shape.
|
|
112
|
+
*
|
|
113
|
+
* - New shape (`{ groups }`): passed through (modules default to kind:'model').
|
|
114
|
+
* - Legacy flat shape (`{ modules }`): grouped by `addon_label`/`addon_key`
|
|
115
|
+
* (falling back to "Sistema") so old hosts keep their familiar buckets,
|
|
116
|
+
* every module defaulting to kind:'model'. The grey headers still render,
|
|
117
|
+
* just derived from the addon instead of the sidebar group.
|
|
118
|
+
*/
|
|
119
|
+
export function normalizeCatalogGroups(catalog) {
|
|
120
|
+
const withKind = (m) => ({
|
|
121
|
+
...m,
|
|
122
|
+
kind: m.kind ?? 'model',
|
|
123
|
+
});
|
|
124
|
+
if ('groups' in catalog && Array.isArray(catalog.groups)) {
|
|
125
|
+
return catalog.groups.map((g) => ({
|
|
126
|
+
title: g.title ?? '',
|
|
127
|
+
modules: g.modules.map(withKind),
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
const modules = ('modules' in catalog && catalog.modules) || [];
|
|
103
131
|
const order = [];
|
|
104
132
|
const byGroup = new Map();
|
|
105
|
-
for (const
|
|
106
|
-
const
|
|
133
|
+
for (const raw of modules) {
|
|
134
|
+
const mod = withKind(raw);
|
|
135
|
+
const g = legacyGroupLabel(mod);
|
|
107
136
|
if (!byGroup.has(g)) {
|
|
108
137
|
byGroup.set(g, []);
|
|
109
138
|
order.push(g);
|
|
110
139
|
}
|
|
111
140
|
byGroup.get(g).push(mod);
|
|
112
141
|
}
|
|
113
|
-
return order.map((
|
|
142
|
+
return order.map((title) => ({ title, modules: byGroup.get(title) }));
|
|
143
|
+
}
|
|
144
|
+
/** Flat list of every module across groups, in render order. */
|
|
145
|
+
export function flattenGroups(groups) {
|
|
146
|
+
return groups.flatMap((g) => g.modules);
|
|
114
147
|
}
|
|
115
|
-
/** Filter the grouped
|
|
148
|
+
/** Filter the grouped flat list by a folded query against module + group titles. */
|
|
116
149
|
export function filterModuleGroups(groups, query) {
|
|
117
150
|
const q = fold(query).trim();
|
|
118
151
|
if (!q)
|
|
119
152
|
return groups;
|
|
120
153
|
const out = [];
|
|
121
154
|
for (const g of groups) {
|
|
122
|
-
const groupMatches = fold(g.
|
|
155
|
+
const groupMatches = g.title.length > 0 && fold(g.title).includes(q);
|
|
123
156
|
const mods = groupMatches
|
|
124
157
|
? g.modules
|
|
125
158
|
: g.modules.filter((m) => fold(m.label).includes(q) || fold(m.key).includes(q));
|
|
126
159
|
if (mods.length)
|
|
127
|
-
out.push({
|
|
160
|
+
out.push({ title: g.title, modules: mods });
|
|
128
161
|
}
|
|
129
162
|
return out;
|
|
130
163
|
}
|
|
@@ -142,17 +175,18 @@ function CapabilityCheck({ checked, disabled, onToggle, icon, label, description
|
|
|
142
175
|
}
|
|
143
176
|
}, className: cn('flex items-start gap-2.5 rounded-md border border-border/60 bg-card px-3 py-2.5 text-sm transition-colors', disabled ? 'opacity-50' : 'cursor-pointer hover:bg-muted/40', checked && 'border-primary/40 bg-primary/5'), children: [_jsx(Checkbox, { checked: checked, "aria-hidden": "true", tabIndex: -1, className: "pointer-events-none mt-0.5 shrink-0" }), icon && (_jsx(DynamicIcon, { name: icon, className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" })), _jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate font-medium text-foreground", children: label }), description && (_jsx("span", { className: "mt-0.5 block text-xs text-muted-foreground", children: description }))] })] }));
|
|
144
177
|
}
|
|
145
|
-
/** One module row
|
|
146
|
-
function
|
|
178
|
+
/** One clickable module row in the flat list (mirrors a sidebar item). */
|
|
179
|
+
function ModuleRow({ module, active, granted, total, onSelect, }) {
|
|
147
180
|
return (_jsxs("button", { type: "button", onClick: onSelect, "aria-current": active || undefined, className: cn('group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors', active
|
|
148
181
|
? 'bg-primary/10 font-medium text-foreground'
|
|
149
|
-
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'), children: [_jsx(DynamicIcon, { name: module.icon || 'Square', className: cn('h-4 w-4 shrink-0', active ? 'text-primary' : 'text-muted-foreground') }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: module.label }), granted > 0 && (_jsxs(Badge, { variant: granted === total ? 'default' : 'secondary', className: "h-5 shrink-0 px-1.5 text-[10px] tabular-nums", children: [granted, "/", total] }))] }));
|
|
182
|
+
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'), children: [_jsx(DynamicIcon, { name: module.icon || (module.kind === 'screen' ? 'Eye' : 'Square'), className: cn('h-4 w-4 shrink-0', active ? 'text-primary' : 'text-muted-foreground') }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: module.label }), granted > 0 && (_jsxs(Badge, { variant: granted === total ? 'default' : 'secondary', className: "h-5 shrink-0 px-1.5 text-[10px] tabular-nums", children: [granted, "/", total] }))] }));
|
|
150
183
|
}
|
|
151
184
|
// ---------------------------------------------------------------------------
|
|
152
185
|
// Component
|
|
153
186
|
// ---------------------------------------------------------------------------
|
|
154
187
|
export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions, syncRolePermissions, createRole, updateRole, deleteRole, title = 'Permisos y Roles', className, }) {
|
|
155
|
-
const [
|
|
188
|
+
const [groups, setGroups] = React.useState(null);
|
|
189
|
+
const [general, setGeneral] = React.useState(null);
|
|
156
190
|
const [roles, setRoles] = React.useState(null);
|
|
157
191
|
const [loadError, setLoadError] = React.useState(false);
|
|
158
192
|
const [activeRoleId, setActiveRoleId] = React.useState(null);
|
|
@@ -164,15 +198,14 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
164
198
|
const [saving, setSaving] = React.useState(false);
|
|
165
199
|
const [roleOpen, setRoleOpen] = React.useState(false);
|
|
166
200
|
const [moduleQuery, setModuleQuery] = React.useState('');
|
|
167
|
-
// Groups the user explicitly collapsed (default: every group open).
|
|
168
|
-
const [collapsedGroups, setCollapsedGroups] = React.useState(new Set());
|
|
169
201
|
// Pending role switch while there are unsaved changes.
|
|
170
202
|
const [pendingRoleId, setPendingRoleId] = React.useState(null);
|
|
171
203
|
const [roleDialog, setRoleDialog] = React.useState({ open: false, mode: 'create', label: '', color: ROLE_COLORS[5] });
|
|
172
204
|
const [roleSaving, setRoleSaving] = React.useState(false);
|
|
173
205
|
const [deleteOpen, setDeleteOpen] = React.useState(false);
|
|
174
206
|
const [deleting, setDeleting] = React.useState(false);
|
|
175
|
-
const loading =
|
|
207
|
+
const loading = groups === null || roles === null;
|
|
208
|
+
const allModules = React.useMemo(() => (groups ? flattenGroups(groups) : []), [groups]);
|
|
176
209
|
// ---- initial load: catalog + roles in parallel -------------------------
|
|
177
210
|
React.useEffect(() => {
|
|
178
211
|
let cancelled = false;
|
|
@@ -180,10 +213,12 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
180
213
|
.then(([cat, rs]) => {
|
|
181
214
|
if (cancelled)
|
|
182
215
|
return;
|
|
183
|
-
|
|
216
|
+
const grouped = normalizeCatalogGroups(cat);
|
|
217
|
+
setGroups(grouped);
|
|
218
|
+
setGeneral(cat.general ?? []);
|
|
184
219
|
setRoles(rs);
|
|
185
220
|
setActiveRoleId((prev) => prev ?? rs[0]?.id ?? null);
|
|
186
|
-
setActiveModuleKey((prev) => prev ??
|
|
221
|
+
setActiveModuleKey((prev) => prev ?? flattenGroups(grouped)[0]?.key ?? null);
|
|
187
222
|
})
|
|
188
223
|
.catch(() => {
|
|
189
224
|
if (!cancelled)
|
|
@@ -227,12 +262,10 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
227
262
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
228
263
|
}, [activeRoleId]);
|
|
229
264
|
const activeRole = React.useMemo(() => roles?.find((r) => r.id === activeRoleId) ?? null, [roles, activeRoleId]);
|
|
230
|
-
const activeModule = React.useMemo(() =>
|
|
265
|
+
const activeModule = React.useMemo(() => allModules.find((m) => m.key === activeModuleKey) ?? null, [allModules, activeModuleKey]);
|
|
231
266
|
const dirty = baseline !== null && draft !== null && !capabilitySetsEqual(baseline, draft);
|
|
232
|
-
//
|
|
233
|
-
const
|
|
234
|
-
const visibleGroups = React.useMemo(() => filterModuleGroups(allGroups, moduleQuery), [allGroups, moduleQuery]);
|
|
235
|
-
const searching = moduleQuery.trim().length > 0;
|
|
267
|
+
// Flat module list, optionally filtered by the search.
|
|
268
|
+
const visibleGroups = React.useMemo(() => filterModuleGroups(groups ?? [], moduleQuery), [groups, moduleQuery]);
|
|
236
269
|
// ---- capability edits ---------------------------------------------------
|
|
237
270
|
const toggleCapability = React.useCallback((cap) => {
|
|
238
271
|
setDraft((prev) => {
|
|
@@ -288,14 +321,6 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
288
321
|
else
|
|
289
322
|
setActiveRoleId(roleId);
|
|
290
323
|
};
|
|
291
|
-
const toggleGroup = (label) => setCollapsedGroups((prev) => {
|
|
292
|
-
const next = new Set(prev);
|
|
293
|
-
if (next.has(label))
|
|
294
|
-
next.delete(label);
|
|
295
|
-
else
|
|
296
|
-
next.add(label);
|
|
297
|
-
return next;
|
|
298
|
-
});
|
|
299
324
|
// ---- role CRUD -----------------------------------------------------------
|
|
300
325
|
const refreshRoles = async (selectId) => {
|
|
301
326
|
const rs = await loadRoles();
|
|
@@ -378,6 +403,12 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
378
403
|
const moduleGranted = activeModule && draft ? grantedCountForModule(draft, activeModule) : 0;
|
|
379
404
|
const moduleTotal = activeModule?.actions.length ?? 0;
|
|
380
405
|
const checksDisabled = !activeRole || !draft || loadingPerms || saving;
|
|
406
|
+
const activeModuleGroupTitle = React.useMemo(() => {
|
|
407
|
+
if (!activeModule || !groups)
|
|
408
|
+
return '';
|
|
409
|
+
const g = groups.find((grp) => grp.modules.some((m) => m.key === activeModule.key));
|
|
410
|
+
return g?.title ?? '';
|
|
411
|
+
}, [activeModule, groups]);
|
|
381
412
|
// ---- render --------------------------------------------------------------
|
|
382
413
|
if (loadError) {
|
|
383
414
|
return (_jsxs("div", { className: cn('flex flex-col items-center justify-center gap-2 py-16 text-muted-foreground', className), children: [_jsx(Shield, { className: "h-8 w-8 opacity-40" }), _jsx("p", { className: "text-sm", children: "No se pudo cargar el cat\u00E1logo de permisos." })] }));
|
|
@@ -399,15 +430,12 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
399
430
|
setRoleOpen(false);
|
|
400
431
|
}, children: [_jsx("span", { className: "mr-2 h-2 w-2 shrink-0 rounded-full", style: {
|
|
401
432
|
background: role.color || '#6b7280',
|
|
402
|
-
}, "aria-hidden": "true" }), _jsx("span", { className: "truncate", children: role.label || role.name }), role.id === activeRoleId && (_jsx(Check, { className: "ml-auto h-4 w-4" }))] }, role.id))) })] })] }) })] }), updateRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0", "aria-label": "Editar rol", disabled: !activeRole, onClick: openEditRole, children: _jsx(Pencil, { className: "h-4 w-4" }) })), deleteRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0 text-destructive hover:text-destructive", "aria-label": "Eliminar rol", disabled: !activeRole, onClick: () => setDeleteOpen(true), children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }), (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
})) })] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex flex-wrap items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [activeModule && (_jsx(DynamicIcon, { name: activeModule.icon || 'Square', className: "h-4 w-4 shrink-0 text-primary" })), _jsx("span", { className: "truncate", children: activeModule ? activeModule.label : 'Acciones permitidas' })] }), _jsx(CardDescription, { children: activeModule
|
|
409
|
-
? `${moduleGroupLabel(activeModule)} · configura las acciones permitidas`
|
|
410
|
-
: 'Configura los permisos del módulo seleccionado.' })] }), activeRole && activeModule && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Badge, { variant: "secondary", className: "tabular-nums", children: [moduleGranted, "/", moduleTotal] }), _jsxs(Button, { variant: "outline", size: "sm", className: "h-8", disabled: checksDisabled || moduleGranted === moduleTotal, onClick: () => setModuleAll(true), children: [_jsx(CheckCheck, { className: "mr-1.5 h-3.5 w-3.5" }), " Marcar todo"] }), _jsxs(Button, { variant: "outline", size: "sm", className: "h-8", disabled: checksDisabled || moduleGranted === 0, onClick: () => setModuleAll(false), children: [_jsx(Eraser, { className: "mr-1.5 h-3.5 w-3.5" }), " Limpiar"] })] }))] }) }), _jsx(CardContent, { children: !activeRole ? (_jsx(EmptyHint, { text: "Selecciona un rol para configurar sus permisos." })) : loadingPerms ? (_jsx("div", { className: "grid gap-2 sm:grid-cols-2 xl:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Skeleton, { className: "h-11 w-full" }, i))) })) : !activeModule ? (_jsx(EmptyHint, { text: "Selecciona un m\u00F3dulo del \u00E1rbol para ver sus acciones." })) : (_jsx("div", { className: "grid gap-2 sm:grid-cols-2 xl:grid-cols-3", children: activeModule.actions.map((action) => {
|
|
433
|
+
}, "aria-hidden": "true" }), _jsx("span", { className: "truncate", children: role.label || role.name }), role.id === activeRoleId && (_jsx(Check, { className: "ml-auto h-4 w-4" }))] }, role.id))) })] })] }) })] }), updateRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0", "aria-label": "Editar rol", disabled: !activeRole, onClick: openEditRole, children: _jsx(Pencil, { className: "h-4 w-4" }) })), deleteRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0 text-destructive hover:text-destructive", "aria-label": "Eliminar rol", disabled: !activeRole, onClick: () => setDeleteOpen(true), children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }), (general?.length ?? 0) > 0 && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs("div", { children: [_jsx("h3", { className: "mb-2 text-sm font-semibold", children: "Permisos Generales" }), _jsx("div", { className: "flex flex-col gap-2", children: general.map((g) => (_jsx(CapabilityCheck, { checked: draft?.has(g.key) ?? false, disabled: checksDisabled, onToggle: () => toggleCapability(g.key), label: g.label, description: g.description }, g.key))) })] })] }))] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "M\u00F3dulos" }), _jsx(CardDescription, { children: "Elige el m\u00F3dulo cuyas acciones quieres configurar." })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: moduleQuery, onChange: (e) => setModuleQuery(e.target.value), placeholder: "Buscar m\u00F3dulo\u2026", "aria-label": "Buscar m\u00F3dulo", className: "pl-8" })] }), _jsx("div", { role: "list", "aria-label": "M\u00F3dulos", className: "-mx-1 flex max-h-[460px] flex-col gap-0.5 overflow-y-auto px-1", children: visibleGroups.length === 0 ? (_jsx("p", { className: "px-2 py-6 text-center text-sm text-muted-foreground", children: "Sin m\u00F3dulos." })) : (visibleGroups.map((group, gi) => (_jsxs("div", { className: "flex flex-col gap-0.5", children: [group.title && (_jsx("div", { role: "heading", "aria-level": 3, className: cn('px-2 pb-1 pt-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground', gi === 0 && 'pt-1'), children: group.title })), group.modules.map((mod) => (_jsx(ModuleRow, { module: mod, active: mod.key === activeModuleKey, granted: draft
|
|
434
|
+
? grantedCountForModule(draft, mod)
|
|
435
|
+
: 0, total: mod.actions.length, onSelect: () => setActiveModuleKey(mod.key) }, mod.key)))] }, group.title || `__untitled_${gi}`)))) })] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex flex-wrap items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [activeModule && (_jsx(DynamicIcon, { name: activeModule.icon ||
|
|
436
|
+
(activeModule.kind === 'screen' ? 'Eye' : 'Square'), className: "h-4 w-4 shrink-0 text-primary" })), _jsx("span", { className: "truncate", children: activeModule ? activeModule.label : 'Acciones permitidas' })] }), _jsx(CardDescription, { children: activeModule
|
|
437
|
+
? `${activeModuleGroupTitle || 'Sistema'} · configura las acciones permitidas`
|
|
438
|
+
: 'Configura los permisos del módulo seleccionado.' })] }), activeRole && activeModule && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Badge, { variant: "secondary", className: "tabular-nums", children: [moduleGranted, "/", moduleTotal] }), _jsxs(Button, { variant: "outline", size: "sm", className: "h-8", disabled: checksDisabled || moduleGranted === moduleTotal, onClick: () => setModuleAll(true), children: [_jsx(CheckCheck, { className: "mr-1.5 h-3.5 w-3.5" }), " Marcar todo"] }), _jsxs(Button, { variant: "outline", size: "sm", className: "h-8", disabled: checksDisabled || moduleGranted === 0, onClick: () => setModuleAll(false), children: [_jsx(Eraser, { className: "mr-1.5 h-3.5 w-3.5" }), " Limpiar"] })] }))] }) }), _jsx(CardContent, { children: !activeRole ? (_jsx(EmptyHint, { text: "Selecciona un rol para configurar sus permisos." })) : loadingPerms ? (_jsx("div", { className: "grid gap-2 sm:grid-cols-2 xl:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Skeleton, { className: "h-11 w-full" }, i))) })) : !activeModule ? (_jsx(EmptyHint, { text: "Selecciona un m\u00F3dulo de la lista para ver sus acciones." })) : (_jsx("div", { className: "grid gap-2 sm:grid-cols-2 xl:grid-cols-3", children: activeModule.actions.map((action) => {
|
|
411
439
|
const cap = moduleActionCapability(activeModule.key, action.key);
|
|
412
440
|
return (_jsx(CapabilityCheck, { checked: draft?.has(cap) ?? false, disabled: checksDisabled, onToggle: () => toggleCapability(cap), icon: action.icon || defaultActionIcon(action.key, action.kind), label: action.label }, action.key));
|
|
413
441
|
}) })) })] })] }), _jsx(AlertDialog, { open: pendingRoleId !== null, onOpenChange: (open) => !open && setPendingRoleId(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Cambios sin guardar" }), _jsx(AlertDialogDescription, { children: "Tienes cambios sin guardar en este rol. Si cambias de rol se descartar\u00E1n." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Cancelar" }), _jsx(AlertDialogAction, { onClick: () => {
|