@asteby/metacore-runtime-react 18.14.0 → 18.15.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
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cbcedd9: PermissionsManager: rediseño de UX para que la elección de módulo refleje el sidebar.
|
|
8
|
+
- El selector de módulo plano se reemplaza por un **árbol jerárquico** agrupado por `addon_label` (acordeón colapsable, ícono por módulo, badge de acciones otorgadas N/M, búsqueda que filtra el árbol). Los módulos sin addon caen en el grupo "Sistema".
|
|
9
|
+
- El selector de **rol** queda limpio: combobox con acciones de **editar** y **eliminar** inline (íconos lápiz/basurero a la derecha), sin el chip removible separado.
|
|
10
|
+
- Estados claros del panel de acciones: "elige un rol" / "elige un módulo" / skeleton de carga; el grid se habilita en cuanto hay rol + módulo. El panel titula con el módulo activo y su addon.
|
|
11
|
+
- Nuevo campo opcional `icon` en `PermissionModuleDef` (lucide) para mostrar el ícono del módulo en el árbol y el panel.
|
|
12
|
+
- Helpers exportados nuevos: `groupModules`, `filterModuleGroups` (+ tipo `ModuleGroup`).
|
|
13
|
+
|
|
14
|
+
Sin cambios en la firma de props de `PermissionsManager` (solo render interno; `PermissionModuleDef.icon` es aditivo/opcional).
|
|
15
|
+
|
|
3
16
|
## 18.14.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
|
@@ -14,9 +14,11 @@ export interface PermissionModuleDef {
|
|
|
14
14
|
key: string;
|
|
15
15
|
/** Localized module label ("Pedidos POS"). */
|
|
16
16
|
label: string;
|
|
17
|
+
/** Module icon (lucide name) — mirrors the sidebar entry. */
|
|
18
|
+
icon?: string;
|
|
17
19
|
/** Owning addon key (`pos`). */
|
|
18
20
|
addon_key?: string;
|
|
19
|
-
/** Localized addon label ("Punto de venta") — used to group the
|
|
21
|
+
/** Localized addon label ("Punto de venta") — used to group the tree. */
|
|
20
22
|
addon_label?: string;
|
|
21
23
|
actions: PermissionActionDef[];
|
|
22
24
|
}
|
|
@@ -53,7 +55,7 @@ export interface PermissionsManagerProps {
|
|
|
53
55
|
loadRolePermissions: (roleId: string) => Promise<string[]>;
|
|
54
56
|
/** Persists the FULL granted capability set of a role. */
|
|
55
57
|
syncRolePermissions: (roleId: string, capabilities: string[]) => Promise<void>;
|
|
56
|
-
/** Optional role CRUD — omitting one hides its
|
|
58
|
+
/** Optional role CRUD — omitting one hides its control. */
|
|
57
59
|
createRole?: (input: RoleInput) => Promise<RoleDef | void>;
|
|
58
60
|
updateRole?: (roleId: string, input: RoleInput) => Promise<RoleDef | void>;
|
|
59
61
|
deleteRole?: (roleId: string) => Promise<void>;
|
|
@@ -70,5 +72,13 @@ export declare function grantedCountForModule(granted: ReadonlySet<string>, modu
|
|
|
70
72
|
export declare function capabilitySetsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean;
|
|
71
73
|
/** Default lucide icon when the manifest action doesn't declare one. */
|
|
72
74
|
export declare function defaultActionIcon(actionKey: string, kind?: string): string;
|
|
75
|
+
/** [groupLabel, modules] buckets in stable insertion order. */
|
|
76
|
+
export interface ModuleGroup {
|
|
77
|
+
label: string;
|
|
78
|
+
modules: PermissionModuleDef[];
|
|
79
|
+
}
|
|
80
|
+
export declare function groupModules(modules: PermissionModuleDef[]): ModuleGroup[];
|
|
81
|
+
/** Filter the grouped tree by a folded query against module + group labels. */
|
|
82
|
+
export declare function filterModuleGroups(groups: ModuleGroup[], query: string): ModuleGroup[];
|
|
73
83
|
export declare function PermissionsManager({ loadModules, loadRoles, loadRolePermissions, syncRolePermissions, createRole, updateRole, deleteRole, title, className, }: PermissionsManagerProps): React.JSX.Element;
|
|
74
84
|
//# 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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"}
|
|
@@ -6,24 +6,27 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
// The capability universe (modules × actions + general flags) is derived from
|
|
7
7
|
// the installed manifests server-side; this component only renders it.
|
|
8
8
|
//
|
|
9
|
-
// Layout (
|
|
9
|
+
// Layout (mirrors the app sidebar so admins recognise what they grant):
|
|
10
10
|
// header — title + "Nuevo rol" (primary) + "Guardar permisos" (green).
|
|
11
|
-
// left — Card "Rol":
|
|
12
|
-
//
|
|
13
|
-
// — Card "
|
|
14
|
-
//
|
|
11
|
+
// left — Card "Rol": clean role combobox with inline Editar/Eliminar
|
|
12
|
+
// icons (no removable chip) + "Permisos Generales" flags.
|
|
13
|
+
// — Card "Módulos": a searchable, accordion *tree* grouped by
|
|
14
|
+
// `addon_label` (modules without one → "Sistema"). Each group
|
|
15
|
+
// lists its modules with their icon + a granted-count badge.
|
|
16
|
+
// Clicking a module selects it and reveals its action grid.
|
|
15
17
|
// right — Card "Acciones permitidas": granted counter N/M, mark-all /
|
|
16
|
-
// clear
|
|
18
|
+
// clear, checkbox grid (icon + label per action). Clear empty
|
|
19
|
+
// states for "pick a role" / "pick a module" / loading.
|
|
17
20
|
//
|
|
18
21
|
// Saving calls `syncRolePermissions(roleId, capabilities)` with the FULL
|
|
19
22
|
// granted set of the active role (baseline + the edits made here). Dirty
|
|
20
23
|
// state is tracked against the loaded baseline and surfaced next to the
|
|
21
24
|
// save button.
|
|
22
25
|
import * as React from 'react';
|
|
23
|
-
import { Check, ChevronsUpDown, CheckCheck, Eraser, Pencil, Plus, Save, Shield, Trash2,
|
|
26
|
+
import { Check, ChevronRight, ChevronsUpDown, CheckCheck, Eraser, Folder, Pencil, Plus, Save, Search, Shield, Trash2, } from 'lucide-react';
|
|
24
27
|
import { toast } from 'sonner';
|
|
25
28
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
26
|
-
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';
|
|
29
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Separator, Skeleton, } from '@asteby/metacore-ui/primitives';
|
|
27
30
|
import { DynamicIcon } from './dynamic-icon';
|
|
28
31
|
// ---------------------------------------------------------------------------
|
|
29
32
|
// Pure helpers (exported for hosts/tests)
|
|
@@ -67,11 +70,20 @@ export function defaultActionIcon(actionKey, kind) {
|
|
|
67
70
|
return kind === 'crud' ? 'List' : 'Zap';
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
/** Group label fallback when a module has no addon ("Sistema" = core/infra). */
|
|
74
|
+
const SYSTEM_GROUP = 'Sistema';
|
|
75
|
+
function moduleGroupLabel(mod) {
|
|
76
|
+
return mod.addon_label || mod.addon_key || SYSTEM_GROUP;
|
|
77
|
+
}
|
|
78
|
+
/** Accent-insensitive, lowercase fold for tree search. */
|
|
79
|
+
function fold(s) {
|
|
80
|
+
return s
|
|
72
81
|
.normalize('NFD')
|
|
73
|
-
.replace(/[
|
|
74
|
-
.toLowerCase()
|
|
82
|
+
.replace(/[̀-ͯ]/g, '')
|
|
83
|
+
.toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
function slugify(label) {
|
|
86
|
+
return fold(label)
|
|
75
87
|
.trim()
|
|
76
88
|
.replace(/[^a-z0-9]+/g, '_')
|
|
77
89
|
.replace(/^_+|_+$/g, '');
|
|
@@ -87,6 +99,35 @@ const ROLE_COLORS = [
|
|
|
87
99
|
'#ec4899',
|
|
88
100
|
'#6b7280',
|
|
89
101
|
];
|
|
102
|
+
export function groupModules(modules) {
|
|
103
|
+
const order = [];
|
|
104
|
+
const byGroup = new Map();
|
|
105
|
+
for (const mod of modules) {
|
|
106
|
+
const g = moduleGroupLabel(mod);
|
|
107
|
+
if (!byGroup.has(g)) {
|
|
108
|
+
byGroup.set(g, []);
|
|
109
|
+
order.push(g);
|
|
110
|
+
}
|
|
111
|
+
byGroup.get(g).push(mod);
|
|
112
|
+
}
|
|
113
|
+
return order.map((label) => ({ label, modules: byGroup.get(label) }));
|
|
114
|
+
}
|
|
115
|
+
/** Filter the grouped tree by a folded query against module + group labels. */
|
|
116
|
+
export function filterModuleGroups(groups, query) {
|
|
117
|
+
const q = fold(query).trim();
|
|
118
|
+
if (!q)
|
|
119
|
+
return groups;
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const g of groups) {
|
|
122
|
+
const groupMatches = fold(g.label).includes(q);
|
|
123
|
+
const mods = groupMatches
|
|
124
|
+
? g.modules
|
|
125
|
+
: g.modules.filter((m) => fold(m.label).includes(q) || fold(m.key).includes(q));
|
|
126
|
+
if (mods.length)
|
|
127
|
+
out.push({ label: g.label, modules: mods });
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
90
131
|
// ---------------------------------------------------------------------------
|
|
91
132
|
// Internal sub-components
|
|
92
133
|
// ---------------------------------------------------------------------------
|
|
@@ -101,9 +142,11 @@ function CapabilityCheck({ checked, disabled, onToggle, icon, label, description
|
|
|
101
142
|
}
|
|
102
143
|
}, 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 }))] })] }));
|
|
103
144
|
}
|
|
104
|
-
/**
|
|
105
|
-
function
|
|
106
|
-
return (_jsxs(
|
|
145
|
+
/** One module row inside the tree. */
|
|
146
|
+
function ModuleTreeItem({ module, active, granted, total, onSelect, }) {
|
|
147
|
+
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
|
+
? '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] }))] }));
|
|
107
150
|
}
|
|
108
151
|
// ---------------------------------------------------------------------------
|
|
109
152
|
// Component
|
|
@@ -120,7 +163,9 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
120
163
|
const [loadingPerms, setLoadingPerms] = React.useState(false);
|
|
121
164
|
const [saving, setSaving] = React.useState(false);
|
|
122
165
|
const [roleOpen, setRoleOpen] = React.useState(false);
|
|
123
|
-
const [
|
|
166
|
+
const [moduleQuery, setModuleQuery] = React.useState('');
|
|
167
|
+
// Groups the user explicitly collapsed (default: every group open).
|
|
168
|
+
const [collapsedGroups, setCollapsedGroups] = React.useState(new Set());
|
|
124
169
|
// Pending role switch while there are unsaved changes.
|
|
125
170
|
const [pendingRoleId, setPendingRoleId] = React.useState(null);
|
|
126
171
|
const [roleDialog, setRoleDialog] = React.useState({ open: false, mode: 'create', label: '', color: ROLE_COLORS[5] });
|
|
@@ -184,17 +229,10 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
184
229
|
const activeRole = React.useMemo(() => roles?.find((r) => r.id === activeRoleId) ?? null, [roles, activeRoleId]);
|
|
185
230
|
const activeModule = React.useMemo(() => catalog?.modules.find((m) => m.key === activeModuleKey) ?? null, [catalog, activeModuleKey]);
|
|
186
231
|
const dirty = baseline !== null && draft !== null && !capabilitySetsEqual(baseline, draft);
|
|
187
|
-
//
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const group = mod.addon_label || mod.addon_key || 'Otros';
|
|
192
|
-
const list = groups.get(group) ?? [];
|
|
193
|
-
list.push(mod);
|
|
194
|
-
groups.set(group, list);
|
|
195
|
-
}
|
|
196
|
-
return Array.from(groups.entries());
|
|
197
|
-
}, [catalog]);
|
|
232
|
+
// Module tree: grouped by addon label, optionally filtered by the search.
|
|
233
|
+
const allGroups = React.useMemo(() => groupModules(catalog?.modules ?? []), [catalog]);
|
|
234
|
+
const visibleGroups = React.useMemo(() => filterModuleGroups(allGroups, moduleQuery), [allGroups, moduleQuery]);
|
|
235
|
+
const searching = moduleQuery.trim().length > 0;
|
|
198
236
|
// ---- capability edits ---------------------------------------------------
|
|
199
237
|
const toggleCapability = React.useCallback((cap) => {
|
|
200
238
|
setDraft((prev) => {
|
|
@@ -250,6 +288,14 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
250
288
|
else
|
|
251
289
|
setActiveRoleId(roleId);
|
|
252
290
|
};
|
|
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
|
+
});
|
|
253
299
|
// ---- role CRUD -----------------------------------------------------------
|
|
254
300
|
const refreshRoles = async (selectId) => {
|
|
255
301
|
const rs = await loadRoles();
|
|
@@ -318,6 +364,16 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
318
364
|
setDeleting(false);
|
|
319
365
|
}
|
|
320
366
|
};
|
|
367
|
+
const openEditRole = () => {
|
|
368
|
+
if (!activeRole)
|
|
369
|
+
return;
|
|
370
|
+
setRoleDialog({
|
|
371
|
+
open: true,
|
|
372
|
+
mode: 'edit',
|
|
373
|
+
label: activeRole.label || activeRole.name,
|
|
374
|
+
color: activeRole.color || ROLE_COLORS[5],
|
|
375
|
+
});
|
|
376
|
+
};
|
|
321
377
|
// ---- derived for the right panel ----------------------------------------
|
|
322
378
|
const moduleGranted = activeModule && draft ? grantedCountForModule(draft, activeModule) : 0;
|
|
323
379
|
const moduleTotal = activeModule?.actions.length ?? 0;
|
|
@@ -327,20 +383,31 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
327
383
|
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." })] }));
|
|
328
384
|
}
|
|
329
385
|
if (loading) {
|
|
330
|
-
return (_jsxs("div", { className: cn('flex flex-col gap-4', className), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Skeleton, { className: "h-8 w-56" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Skeleton, { className: "h-9 w-28" }), _jsx(Skeleton, { className: "h-9 w-40" })] })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-[340px_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(Skeleton, { className: "h-
|
|
386
|
+
return (_jsxs("div", { className: cn('flex flex-col gap-4', className), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Skeleton, { className: "h-8 w-56" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Skeleton, { className: "h-9 w-28" }), _jsx(Skeleton, { className: "h-9 w-40" })] })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-[340px_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(Skeleton, { className: "h-40 w-full" }), _jsx(Skeleton, { className: "h-80 w-full" })] }), _jsx(Skeleton, { className: "h-96 w-full" })] })] }));
|
|
331
387
|
}
|
|
332
|
-
return (_jsxs("div", { className: cn('flex flex-col gap-4', className), children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-bold tracking-tight", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Define qu\u00E9 puede hacer cada rol en cada m\u00F3dulo." })] }), _jsxs("div", { className: "flex items-center gap-2", children: [dirty && (_jsx(Badge, { variant: "outline", className: "border-amber-500/50 text-amber-600", children: "Cambios sin guardar" })), createRole && (_jsxs(Button, { onClick: () => setRoleDialog({
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
388
|
+
return (_jsxs("div", { className: cn('flex flex-col gap-4', className), children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-bold tracking-tight", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Define qu\u00E9 puede hacer cada rol en cada m\u00F3dulo." })] }), _jsxs("div", { className: "flex items-center gap-2", children: [dirty && (_jsx(Badge, { variant: "outline", className: "border-amber-500/50 text-amber-600", children: "Cambios sin guardar" })), createRole && (_jsxs(Button, { onClick: () => setRoleDialog({
|
|
389
|
+
open: true,
|
|
390
|
+
mode: 'create',
|
|
391
|
+
label: '',
|
|
392
|
+
color: ROLE_COLORS[5],
|
|
393
|
+
}), children: [_jsx(Plus, { className: "mr-1.5 h-4 w-4" }), " Nuevo rol"] })), _jsxs(Button, { onClick: handleSave, disabled: !dirty || saving || !activeRole, className: "bg-emerald-600 text-white hover:bg-emerald-700", children: [_jsx(Save, { className: "mr-1.5 h-4 w-4" }), saving ? 'Guardando…' : 'Guardar permisos'] })] })] }), _jsxs("div", { className: "grid items-start gap-4 lg:grid-cols-[340px_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "Rol" }), _jsx(CardDescription, { children: "Selecciona el rol a configurar." })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Popover, { open: roleOpen, onOpenChange: setRoleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": roleOpen, className: "min-w-0 flex-1 justify-between font-normal", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [activeRole && (_jsx("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full", style: {
|
|
394
|
+
background: activeRole.color || '#6b7280',
|
|
395
|
+
}, "aria-hidden": "true" })), _jsx("span", { className: "truncate", children: activeRole
|
|
396
|
+
? activeRole.label || activeRole.name
|
|
397
|
+
: 'Seleccionar rol…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "w-[280px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Buscar rol\u2026" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "Sin resultados." }), _jsx(CommandGroup, { children: (roles ?? []).map((role) => (_jsxs(CommandItem, { value: `${role.label || ''} ${role.name}`, onSelect: () => {
|
|
398
|
+
requestRoleSwitch(role.id);
|
|
399
|
+
setRoleOpen(false);
|
|
400
|
+
}, children: [_jsx("span", { className: "mr-2 h-2 w-2 shrink-0 rounded-full", style: {
|
|
401
|
+
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" }) }))] }), (catalog?.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: catalog.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: "tree", "aria-label": "M\u00F3dulos", className: "-mx-1 max-h-[460px] 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) => {
|
|
403
|
+
// While searching, force every matching group open.
|
|
404
|
+
const open = searching || !collapsedGroups.has(group.label);
|
|
405
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: () => !searching && toggleGroup(group.label), children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs font-semibold uppercase tracking-wide text-muted-foreground transition-colors hover:bg-muted/40", children: [_jsx(ChevronRight, { className: cn('h-3.5 w-3.5 shrink-0 transition-transform', open && 'rotate-90') }), _jsx(Folder, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "min-w-0 flex-1 truncate normal-case", children: group.label }), _jsx("span", { className: "shrink-0 text-[10px] tabular-nums opacity-70", children: group.modules.length })] }) }), _jsx(CollapsibleContent, { children: _jsx("div", { className: "ml-3 flex flex-col gap-0.5 border-l border-border/60 pl-1.5", children: group.modules.map((mod) => (_jsx(ModuleTreeItem, { module: mod, active: mod.key === activeModuleKey, granted: draft
|
|
406
|
+
? grantedCountForModule(draft, mod)
|
|
407
|
+
: 0, total: mod.actions.length, onSelect: () => setActiveModuleKey(mod.key) }, mod.key))) }) })] }, group.label));
|
|
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) => {
|
|
344
411
|
const cap = moduleActionCapability(activeModule.key, action.key);
|
|
345
412
|
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));
|
|
346
413
|
}) })) })] })] }), _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: () => {
|
|
@@ -348,7 +415,11 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
348
415
|
setPendingRoleId(null);
|
|
349
416
|
}, children: "Descartar y cambiar" })] })] }) }), _jsx(Dialog, { open: roleDialog.open, onOpenChange: (open) => setRoleDialog((d) => ({ ...d, open })), children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: roleDialog.mode === 'create' ? 'Nuevo rol' : 'Editar rol' }) }), _jsxs("div", { className: "flex flex-col gap-4 py-2", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "pm-role-name", children: "Nombre del rol" }), _jsx(Input, { id: "pm-role-name", value: roleDialog.label, placeholder: "Ej. Cajero", onChange: (e) => setRoleDialog((d) => ({ ...d, label: e.target.value })) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Color" }), _jsx("div", { className: "flex flex-wrap gap-2", children: ROLE_COLORS.map((c) => (_jsx("button", { type: "button", "aria-label": `Color ${c}`, onClick: () => setRoleDialog((d) => ({ ...d, color: c })), className: cn('h-7 w-7 rounded-full border-2 transition-transform', roleDialog.color === c
|
|
350
417
|
? 'scale-110 border-foreground'
|
|
351
|
-
: 'border-transparent hover:scale-105'), style: { background: c } }, c))) })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setRoleDialog((d) => ({ ...d, open: false })), disabled: roleSaving, children: "Cancelar" }), _jsx(Button, { onClick: handleRoleSubmit, disabled: roleSaving || !roleDialog.label.trim(), children: roleSaving
|
|
418
|
+
: 'border-transparent hover:scale-105'), style: { background: c } }, c))) })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setRoleDialog((d) => ({ ...d, open: false })), disabled: roleSaving, children: "Cancelar" }), _jsx(Button, { onClick: handleRoleSubmit, disabled: roleSaving || !roleDialog.label.trim(), children: roleSaving
|
|
419
|
+
? 'Guardando…'
|
|
420
|
+
: roleDialog.mode === 'create'
|
|
421
|
+
? 'Crear rol'
|
|
422
|
+
: 'Guardar' })] })] }) }), _jsx(AlertDialog, { open: deleteOpen, onOpenChange: (open) => !deleting && setDeleteOpen(open), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "\u00BFEliminar el rol?" }), _jsxs(AlertDialogDescription, { children: ["Se eliminar\u00E1 el rol", ' ', _jsx("strong", { children: activeRole ? activeRole.label || activeRole.name : '' }), " y sus asignaciones de permisos. Esta acci\u00F3n no se puede deshacer."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: deleting, children: "Cancelar" }), _jsx(AlertDialogAction, { className: "bg-red-600 hover:bg-red-700", disabled: deleting, onClick: (e) => {
|
|
352
423
|
e.preventDefault();
|
|
353
424
|
handleDeleteRole();
|
|
354
425
|
}, children: deleting ? 'Eliminando…' : 'Eliminar' })] })] }) })] }));
|
package/package.json
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
moduleCapabilities,
|
|
11
11
|
grantedCountForModule,
|
|
12
12
|
capabilitySetsEqual,
|
|
13
|
+
groupModules,
|
|
14
|
+
filterModuleGroups,
|
|
13
15
|
type PermissionsCatalog,
|
|
14
16
|
type RoleDef,
|
|
15
17
|
} from '../permissions-manager'
|
|
@@ -19,6 +21,7 @@ const catalog: PermissionsCatalog = {
|
|
|
19
21
|
{
|
|
20
22
|
key: 'pos_orders',
|
|
21
23
|
label: 'Pedidos POS',
|
|
24
|
+
icon: 'ShoppingCart',
|
|
22
25
|
addon_key: 'pos',
|
|
23
26
|
addon_label: 'Punto de venta',
|
|
24
27
|
actions: [
|
|
@@ -27,6 +30,20 @@ const catalog: PermissionsCatalog = {
|
|
|
27
30
|
{ key: 'pagar', label: 'Pagar', icon: 'CreditCard', kind: 'custom' },
|
|
28
31
|
],
|
|
29
32
|
},
|
|
33
|
+
{
|
|
34
|
+
key: 'pos_sessions',
|
|
35
|
+
label: 'Sesiones POS',
|
|
36
|
+
icon: 'Clock',
|
|
37
|
+
addon_key: 'pos',
|
|
38
|
+
addon_label: 'Punto de venta',
|
|
39
|
+
actions: [{ key: 'index', label: 'Listar', icon: 'List', kind: 'crud' }],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: 'users',
|
|
43
|
+
label: 'Usuarios',
|
|
44
|
+
icon: 'Users',
|
|
45
|
+
actions: [{ key: 'index', label: 'Listar', icon: 'List', kind: 'crud' }],
|
|
46
|
+
},
|
|
30
47
|
],
|
|
31
48
|
general: [
|
|
32
49
|
{
|
|
@@ -68,6 +85,28 @@ describe('helpers puros', () => {
|
|
|
68
85
|
expect(capabilitySetsEqual(new Set(['a', 'b']), new Set(['b', 'a']))).toBe(true)
|
|
69
86
|
expect(capabilitySetsEqual(new Set(['a']), new Set(['a', 'b']))).toBe(false)
|
|
70
87
|
})
|
|
88
|
+
|
|
89
|
+
it('groupModules agrupa por addon_label y manda los sin addon a "Sistema"', () => {
|
|
90
|
+
const groups = groupModules(catalog.modules)
|
|
91
|
+
expect(groups.map((g) => g.label)).toEqual(['Punto de venta', 'Sistema'])
|
|
92
|
+
expect(groups[0].modules.map((m) => m.key)).toEqual(['pos_orders', 'pos_sessions'])
|
|
93
|
+
expect(groups[1].modules.map((m) => m.key)).toEqual(['users'])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('filterModuleGroups busca por módulo (accent/case-insensitive) o por grupo', () => {
|
|
97
|
+
const groups = groupModules(catalog.modules)
|
|
98
|
+
// Por nombre de módulo.
|
|
99
|
+
const bySession = filterModuleGroups(groups, 'sesiones')
|
|
100
|
+
expect(bySession).toHaveLength(1)
|
|
101
|
+
expect(bySession[0].modules.map((m) => m.key)).toEqual(['pos_sessions'])
|
|
102
|
+
// Por nombre de grupo trae todos sus módulos.
|
|
103
|
+
const byGroup = filterModuleGroups(groups, 'venta')
|
|
104
|
+
expect(byGroup[0].modules).toHaveLength(2)
|
|
105
|
+
// Query vacía = pasa todo.
|
|
106
|
+
expect(filterModuleGroups(groups, ' ')).toEqual(groups)
|
|
107
|
+
// Sin match = vacío.
|
|
108
|
+
expect(filterModuleGroups(groups, 'zzz')).toEqual([])
|
|
109
|
+
})
|
|
71
110
|
})
|
|
72
111
|
|
|
73
112
|
describe('PermissionsManager', () => {
|
|
@@ -76,9 +115,11 @@ describe('PermissionsManager', () => {
|
|
|
76
115
|
render(<PermissionsManager {...props} />)
|
|
77
116
|
|
|
78
117
|
// Auto-selección: primer rol + primer módulo, grid con las acciones.
|
|
79
|
-
|
|
118
|
+
// El panel derecho titula con el módulo activo ("Pedidos POS").
|
|
119
|
+
expect(await screen.findAllByText('Pedidos POS')).toBeTruthy()
|
|
80
120
|
expect(await screen.findByText('Pagar')).toBeTruthy()
|
|
81
|
-
|
|
121
|
+
// El contador N/M del panel está presente (también lo refleja el árbol).
|
|
122
|
+
expect(screen.getAllByText('1/3').length).toBeGreaterThan(0)
|
|
82
123
|
expect(props.loadRolePermissions).toHaveBeenCalledWith('r1')
|
|
83
124
|
|
|
84
125
|
// Generales presentes con descripción.
|
|
@@ -125,7 +166,8 @@ describe('PermissionsManager', () => {
|
|
|
125
166
|
await screen.findByText('Pagar')
|
|
126
167
|
|
|
127
168
|
fireEvent.click(screen.getByRole('button', { name: /Marcar todo/ }))
|
|
128
|
-
|
|
169
|
+
// 3/3 aparece en el panel y en el badge del árbol.
|
|
170
|
+
expect(screen.getAllByText('3/3').length).toBeGreaterThan(0)
|
|
129
171
|
|
|
130
172
|
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
131
173
|
await waitFor(() =>
|
|
@@ -157,6 +199,50 @@ describe('PermissionsManager', () => {
|
|
|
157
199
|
expect(screen.queryByRole('button', { name: 'Eliminar rol' })).toBeNull()
|
|
158
200
|
})
|
|
159
201
|
|
|
202
|
+
it('renderiza el árbol agrupado y permite seleccionar un módulo de otro grupo', async () => {
|
|
203
|
+
const props = makeProps()
|
|
204
|
+
render(<PermissionsManager {...props} />)
|
|
205
|
+
await screen.findByText('Pagar')
|
|
206
|
+
|
|
207
|
+
// Grupos visibles (encabezados del árbol).
|
|
208
|
+
expect(screen.getByText('Punto de venta')).toBeTruthy()
|
|
209
|
+
expect(screen.getByText('Sistema')).toBeTruthy()
|
|
210
|
+
|
|
211
|
+
// Click en "Usuarios" (grupo Sistema) cambia el grid de acciones.
|
|
212
|
+
fireEvent.click(screen.getByRole('button', { name: /Usuarios/ }))
|
|
213
|
+
// El grid ahora muestra solo la acción de users; "Pagar" (de pos_orders) ya no.
|
|
214
|
+
await waitFor(() => expect(screen.queryByText('Pagar')).toBeNull())
|
|
215
|
+
// "Usuarios" titula el panel derecho además del árbol.
|
|
216
|
+
expect(screen.getAllByText('Usuarios').length).toBeGreaterThan(0)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('la búsqueda filtra el árbol de módulos', async () => {
|
|
220
|
+
const props = makeProps()
|
|
221
|
+
render(<PermissionsManager {...props} />)
|
|
222
|
+
await screen.findByText('Pagar')
|
|
223
|
+
|
|
224
|
+
fireEvent.change(screen.getByLabelText('Buscar módulo'), {
|
|
225
|
+
target: { value: 'sesiones' },
|
|
226
|
+
})
|
|
227
|
+
// Solo el grupo con match permanece.
|
|
228
|
+
expect(screen.getByText('Sesiones POS')).toBeTruthy()
|
|
229
|
+
expect(screen.queryByText('Usuarios')).toBeNull()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('selector de rol limpio: edit/delete inline, sin chip removible', async () => {
|
|
233
|
+
const props = makeProps({
|
|
234
|
+
updateRole: vi.fn(async () => {}),
|
|
235
|
+
deleteRole: vi.fn(async () => {}),
|
|
236
|
+
})
|
|
237
|
+
render(<PermissionsManager {...props} />)
|
|
238
|
+
await screen.findByText('Pagar')
|
|
239
|
+
// No existe el botón de quitar rol del chip antiguo.
|
|
240
|
+
expect(screen.queryByRole('button', { name: 'Quitar rol seleccionado' })).toBeNull()
|
|
241
|
+
// Iconos inline presentes.
|
|
242
|
+
expect(screen.getByRole('button', { name: 'Editar rol' })).toBeTruthy()
|
|
243
|
+
expect(screen.getByRole('button', { name: 'Eliminar rol' })).toBeTruthy()
|
|
244
|
+
})
|
|
245
|
+
|
|
160
246
|
it('muestra los CRUD de rol cuando los mutators existen', async () => {
|
|
161
247
|
const props = makeProps({
|
|
162
248
|
createRole: vi.fn(async () => {}),
|