@asteby/metacore-runtime-react 18.14.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 +24 -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 +58 -11
- package/dist/permissions-manager.d.ts.map +1 -1
- package/dist/permissions-manager.js +146 -47
- package/package.json +1 -1
- package/src/__tests__/permissions-manager.test.tsx +245 -35
- package/src/index.ts +6 -0
- package/src/permissions-manager.tsx +417 -222
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
14
|
+
## 18.15.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- cbcedd9: PermissionsManager: rediseño de UX para que la elección de módulo refleje el sidebar.
|
|
19
|
+
- 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".
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- Nuevo campo opcional `icon` en `PermissionModuleDef` (lucide) para mostrar el ícono del módulo en el árbol y el panel.
|
|
23
|
+
- Helpers exportados nuevos: `groupModules`, `filterModuleGroups` (+ tipo `ModuleGroup`).
|
|
24
|
+
|
|
25
|
+
Sin cambios en la firma de props de `PermissionsManager` (solo render interno; `PermissionModuleDef.icon` es aditivo/opcional).
|
|
26
|
+
|
|
3
27
|
## 18.14.0
|
|
4
28
|
|
|
5
29
|
### 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,35 +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. */
|
|
25
|
+
icon?: string;
|
|
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. */
|
|
18
29
|
addon_key?: string;
|
|
19
|
-
/** Localized addon label ("Punto de venta") —
|
|
30
|
+
/** Localized addon label ("Punto de venta") — legacy shape only. */
|
|
20
31
|
addon_label?: string;
|
|
21
32
|
actions: PermissionActionDef[];
|
|
22
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
|
+
}
|
|
23
42
|
export interface GeneralPermissionDef {
|
|
24
43
|
/** Full capability key (`general.work_after_hours`). */
|
|
25
44
|
key: string;
|
|
26
45
|
label: string;
|
|
27
46
|
description?: string;
|
|
28
47
|
}
|
|
29
|
-
|
|
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 {
|
|
30
62
|
modules: PermissionModuleDef[];
|
|
31
63
|
general: GeneralPermissionDef[];
|
|
32
64
|
}
|
|
65
|
+
export type PermissionsCatalog = GroupedPermissionsCatalog | FlatPermissionsCatalog;
|
|
33
66
|
export interface RoleDef {
|
|
34
67
|
id: string;
|
|
35
68
|
/** Stable role key ("cashier"). */
|
|
@@ -45,7 +78,7 @@ export interface RoleInput {
|
|
|
45
78
|
color?: string;
|
|
46
79
|
}
|
|
47
80
|
export interface PermissionsManagerProps {
|
|
48
|
-
/** Loads the module×action universe + general flags. */
|
|
81
|
+
/** Loads the module×action universe + general flags (grouped or flat). */
|
|
49
82
|
loadModules: () => Promise<PermissionsCatalog>;
|
|
50
83
|
/** Loads every assignable role. */
|
|
51
84
|
loadRoles: () => Promise<RoleDef[]>;
|
|
@@ -53,7 +86,7 @@ export interface PermissionsManagerProps {
|
|
|
53
86
|
loadRolePermissions: (roleId: string) => Promise<string[]>;
|
|
54
87
|
/** Persists the FULL granted capability set of a role. */
|
|
55
88
|
syncRolePermissions: (roleId: string, capabilities: string[]) => Promise<void>;
|
|
56
|
-
/** Optional role CRUD — omitting one hides its
|
|
89
|
+
/** Optional role CRUD — omitting one hides its control. */
|
|
57
90
|
createRole?: (input: RoleInput) => Promise<RoleDef | void>;
|
|
58
91
|
updateRole?: (roleId: string, input: RoleInput) => Promise<RoleDef | void>;
|
|
59
92
|
deleteRole?: (roleId: string) => Promise<void>;
|
|
@@ -70,5 +103,19 @@ export declare function grantedCountForModule(granted: ReadonlySet<string>, modu
|
|
|
70
103
|
export declare function capabilitySetsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean;
|
|
71
104
|
/** Default lucide icon when the manifest action doesn't declare one. */
|
|
72
105
|
export declare function defaultActionIcon(actionKey: string, kind?: string): string;
|
|
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. */
|
|
119
|
+
export declare function filterModuleGroups(groups: ModuleGroup[], query: string): ModuleGroup[];
|
|
73
120
|
export declare function PermissionsManager({ loadModules, loadRoles, loadRolePermissions, syncRolePermissions, createRole, updateRole, deleteRole, title, className, }: PermissionsManagerProps): React.JSX.Element;
|
|
74
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,23 +4,28 @@ 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
|
-
// left — Card "Rol":
|
|
12
|
-
//
|
|
13
|
-
// — Card "
|
|
14
|
-
//
|
|
12
|
+
// left — Card "Rol": clean role combobox with inline Editar/Eliminar
|
|
13
|
+
// icons (no removable chip) + "Permisos Generales" flags.
|
|
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.
|
|
15
19
|
// right — Card "Acciones permitidas": granted counter N/M, mark-all /
|
|
16
|
-
// clear
|
|
20
|
+
// clear, checkbox grid (icon + label per action). Clear empty
|
|
21
|
+
// states for "pick a role" / "pick a module" / loading.
|
|
17
22
|
//
|
|
18
23
|
// Saving calls `syncRolePermissions(roleId, capabilities)` with the FULL
|
|
19
24
|
// granted set of the active role (baseline + the edits made here). Dirty
|
|
20
25
|
// state is tracked against the loaded baseline and surfaced next to the
|
|
21
26
|
// save button.
|
|
22
27
|
import * as React from 'react';
|
|
23
|
-
import { Check, ChevronsUpDown, CheckCheck, Eraser, Pencil, Plus, Save, Shield, Trash2,
|
|
28
|
+
import { Check, ChevronsUpDown, CheckCheck, Eraser, Pencil, Plus, Save, Search, Shield, Trash2, } from 'lucide-react';
|
|
24
29
|
import { toast } from 'sonner';
|
|
25
30
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
26
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';
|
|
@@ -63,15 +68,30 @@ export function defaultActionIcon(actionKey, kind) {
|
|
|
63
68
|
return 'Download';
|
|
64
69
|
case 'import':
|
|
65
70
|
return 'Upload';
|
|
71
|
+
case 'access':
|
|
72
|
+
return 'Eye';
|
|
66
73
|
default:
|
|
67
|
-
|
|
74
|
+
if (kind === 'crud')
|
|
75
|
+
return 'List';
|
|
76
|
+
if (kind === 'screen')
|
|
77
|
+
return 'Eye';
|
|
78
|
+
return 'Zap';
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
/** Group label fallback when a legacy module has no addon ("Sistema" = core). */
|
|
82
|
+
const SYSTEM_GROUP = 'Sistema';
|
|
83
|
+
function legacyGroupLabel(mod) {
|
|
84
|
+
return mod.addon_label || mod.addon_key || SYSTEM_GROUP;
|
|
85
|
+
}
|
|
86
|
+
/** Accent-insensitive, lowercase fold for search. */
|
|
87
|
+
function fold(s) {
|
|
88
|
+
return s
|
|
72
89
|
.normalize('NFD')
|
|
73
|
-
.replace(/[
|
|
74
|
-
.toLowerCase()
|
|
90
|
+
.replace(/[̀-ͯ]/g, '')
|
|
91
|
+
.toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
function slugify(label) {
|
|
94
|
+
return fold(label)
|
|
75
95
|
.trim()
|
|
76
96
|
.replace(/[^a-z0-9]+/g, '_')
|
|
77
97
|
.replace(/^_+|_+$/g, '');
|
|
@@ -87,6 +107,60 @@ const ROLE_COLORS = [
|
|
|
87
107
|
'#ec4899',
|
|
88
108
|
'#6b7280',
|
|
89
109
|
];
|
|
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) || [];
|
|
131
|
+
const order = [];
|
|
132
|
+
const byGroup = new Map();
|
|
133
|
+
for (const raw of modules) {
|
|
134
|
+
const mod = withKind(raw);
|
|
135
|
+
const g = legacyGroupLabel(mod);
|
|
136
|
+
if (!byGroup.has(g)) {
|
|
137
|
+
byGroup.set(g, []);
|
|
138
|
+
order.push(g);
|
|
139
|
+
}
|
|
140
|
+
byGroup.get(g).push(mod);
|
|
141
|
+
}
|
|
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);
|
|
147
|
+
}
|
|
148
|
+
/** Filter the grouped flat list by a folded query against module + group titles. */
|
|
149
|
+
export function filterModuleGroups(groups, query) {
|
|
150
|
+
const q = fold(query).trim();
|
|
151
|
+
if (!q)
|
|
152
|
+
return groups;
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const g of groups) {
|
|
155
|
+
const groupMatches = g.title.length > 0 && fold(g.title).includes(q);
|
|
156
|
+
const mods = groupMatches
|
|
157
|
+
? g.modules
|
|
158
|
+
: g.modules.filter((m) => fold(m.label).includes(q) || fold(m.key).includes(q));
|
|
159
|
+
if (mods.length)
|
|
160
|
+
out.push({ title: g.title, modules: mods });
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
90
164
|
// ---------------------------------------------------------------------------
|
|
91
165
|
// Internal sub-components
|
|
92
166
|
// ---------------------------------------------------------------------------
|
|
@@ -101,15 +175,18 @@ function CapabilityCheck({ checked, disabled, onToggle, icon, label, description
|
|
|
101
175
|
}
|
|
102
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 }))] })] }));
|
|
103
177
|
}
|
|
104
|
-
/**
|
|
105
|
-
function
|
|
106
|
-
return (_jsxs(
|
|
178
|
+
/** One clickable module row in the flat list (mirrors a sidebar item). */
|
|
179
|
+
function ModuleRow({ module, active, granted, total, onSelect, }) {
|
|
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
|
|
181
|
+
? 'bg-primary/10 font-medium text-foreground'
|
|
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] }))] }));
|
|
107
183
|
}
|
|
108
184
|
// ---------------------------------------------------------------------------
|
|
109
185
|
// Component
|
|
110
186
|
// ---------------------------------------------------------------------------
|
|
111
187
|
export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions, syncRolePermissions, createRole, updateRole, deleteRole, title = 'Permisos y Roles', className, }) {
|
|
112
|
-
const [
|
|
188
|
+
const [groups, setGroups] = React.useState(null);
|
|
189
|
+
const [general, setGeneral] = React.useState(null);
|
|
113
190
|
const [roles, setRoles] = React.useState(null);
|
|
114
191
|
const [loadError, setLoadError] = React.useState(false);
|
|
115
192
|
const [activeRoleId, setActiveRoleId] = React.useState(null);
|
|
@@ -120,14 +197,15 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
120
197
|
const [loadingPerms, setLoadingPerms] = React.useState(false);
|
|
121
198
|
const [saving, setSaving] = React.useState(false);
|
|
122
199
|
const [roleOpen, setRoleOpen] = React.useState(false);
|
|
123
|
-
const [
|
|
200
|
+
const [moduleQuery, setModuleQuery] = React.useState('');
|
|
124
201
|
// Pending role switch while there are unsaved changes.
|
|
125
202
|
const [pendingRoleId, setPendingRoleId] = React.useState(null);
|
|
126
203
|
const [roleDialog, setRoleDialog] = React.useState({ open: false, mode: 'create', label: '', color: ROLE_COLORS[5] });
|
|
127
204
|
const [roleSaving, setRoleSaving] = React.useState(false);
|
|
128
205
|
const [deleteOpen, setDeleteOpen] = React.useState(false);
|
|
129
206
|
const [deleting, setDeleting] = React.useState(false);
|
|
130
|
-
const loading =
|
|
207
|
+
const loading = groups === null || roles === null;
|
|
208
|
+
const allModules = React.useMemo(() => (groups ? flattenGroups(groups) : []), [groups]);
|
|
131
209
|
// ---- initial load: catalog + roles in parallel -------------------------
|
|
132
210
|
React.useEffect(() => {
|
|
133
211
|
let cancelled = false;
|
|
@@ -135,10 +213,12 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
135
213
|
.then(([cat, rs]) => {
|
|
136
214
|
if (cancelled)
|
|
137
215
|
return;
|
|
138
|
-
|
|
216
|
+
const grouped = normalizeCatalogGroups(cat);
|
|
217
|
+
setGroups(grouped);
|
|
218
|
+
setGeneral(cat.general ?? []);
|
|
139
219
|
setRoles(rs);
|
|
140
220
|
setActiveRoleId((prev) => prev ?? rs[0]?.id ?? null);
|
|
141
|
-
setActiveModuleKey((prev) => prev ??
|
|
221
|
+
setActiveModuleKey((prev) => prev ?? flattenGroups(grouped)[0]?.key ?? null);
|
|
142
222
|
})
|
|
143
223
|
.catch(() => {
|
|
144
224
|
if (!cancelled)
|
|
@@ -182,19 +262,10 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
182
262
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
263
|
}, [activeRoleId]);
|
|
184
264
|
const activeRole = React.useMemo(() => roles?.find((r) => r.id === activeRoleId) ?? null, [roles, activeRoleId]);
|
|
185
|
-
const activeModule = React.useMemo(() =>
|
|
265
|
+
const activeModule = React.useMemo(() => allModules.find((m) => m.key === activeModuleKey) ?? null, [allModules, activeModuleKey]);
|
|
186
266
|
const dirty = baseline !== null && draft !== null && !capabilitySetsEqual(baseline, draft);
|
|
187
|
-
//
|
|
188
|
-
const
|
|
189
|
-
const groups = new Map();
|
|
190
|
-
for (const mod of catalog?.modules ?? []) {
|
|
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]);
|
|
267
|
+
// Flat module list, optionally filtered by the search.
|
|
268
|
+
const visibleGroups = React.useMemo(() => filterModuleGroups(groups ?? [], moduleQuery), [groups, moduleQuery]);
|
|
198
269
|
// ---- capability edits ---------------------------------------------------
|
|
199
270
|
const toggleCapability = React.useCallback((cap) => {
|
|
200
271
|
setDraft((prev) => {
|
|
@@ -318,29 +389,53 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
318
389
|
setDeleting(false);
|
|
319
390
|
}
|
|
320
391
|
};
|
|
392
|
+
const openEditRole = () => {
|
|
393
|
+
if (!activeRole)
|
|
394
|
+
return;
|
|
395
|
+
setRoleDialog({
|
|
396
|
+
open: true,
|
|
397
|
+
mode: 'edit',
|
|
398
|
+
label: activeRole.label || activeRole.name,
|
|
399
|
+
color: activeRole.color || ROLE_COLORS[5],
|
|
400
|
+
});
|
|
401
|
+
};
|
|
321
402
|
// ---- derived for the right panel ----------------------------------------
|
|
322
403
|
const moduleGranted = activeModule && draft ? grantedCountForModule(draft, activeModule) : 0;
|
|
323
404
|
const moduleTotal = activeModule?.actions.length ?? 0;
|
|
324
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]);
|
|
325
412
|
// ---- render --------------------------------------------------------------
|
|
326
413
|
if (loadError) {
|
|
327
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." })] }));
|
|
328
415
|
}
|
|
329
416
|
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-
|
|
417
|
+
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
418
|
}
|
|
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
|
-
|
|
419
|
+
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({
|
|
420
|
+
open: true,
|
|
421
|
+
mode: 'create',
|
|
422
|
+
label: '',
|
|
423
|
+
color: ROLE_COLORS[5],
|
|
424
|
+
}), 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: {
|
|
425
|
+
background: activeRole.color || '#6b7280',
|
|
426
|
+
}, "aria-hidden": "true" })), _jsx("span", { className: "truncate", children: activeRole
|
|
427
|
+
? activeRole.label || activeRole.name
|
|
428
|
+
: '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: () => {
|
|
429
|
+
requestRoleSwitch(role.id);
|
|
430
|
+
setRoleOpen(false);
|
|
431
|
+
}, children: [_jsx("span", { className: "mr-2 h-2 w-2 shrink-0 rounded-full", style: {
|
|
432
|
+
background: role.color || '#6b7280',
|
|
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) => {
|
|
344
439
|
const cap = moduleActionCapability(activeModule.key, action.key);
|
|
345
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));
|
|
346
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: () => {
|
|
@@ -348,7 +443,11 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
348
443
|
setPendingRoleId(null);
|
|
349
444
|
}, 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
445
|
? '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
|
|
446
|
+
: '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
|
|
447
|
+
? 'Guardando…'
|
|
448
|
+
: roleDialog.mode === 'create'
|
|
449
|
+
? 'Crear rol'
|
|
450
|
+
: '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
451
|
e.preventDefault();
|
|
353
452
|
handleDeleteRole();
|
|
354
453
|
}, children: deleting ? 'Eliminando…' : 'Eliminar' })] })] }) })] }));
|