@asteby/metacore-runtime-react 18.16.0 → 18.16.1
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,11 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.16.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c53d68f: PermissionsManager: the module picker is now a grouped combobox (same Popover + Command pattern as the role selector) instead of an always-visible flat list. The long list felt heavy in the left column; the combobox is compact, opens to the grouped+searchable modules (GENERAL, CLIENTES, PUNTO DE VENTA…), and shows the selected module with its icon. Selecting an option reveals its action grid on the right. Granted-count badges appear per option.
|
|
8
|
+
|
|
3
9
|
## 18.16.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions-manager.d.ts","sourceRoot":"","sources":["../src/permissions-manager.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"permissions-manager.d.ts","sourceRoot":"","sources":["../src/permissions-manager.tsx"],"names":[],"mappings":"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"}
|
|
@@ -25,7 +25,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
25
25
|
// state is tracked against the loaded baseline and surfaced next to the
|
|
26
26
|
// save button.
|
|
27
27
|
import * as React from 'react';
|
|
28
|
-
import { Check, ChevronsUpDown, CheckCheck, Eraser, Pencil, Plus, Save,
|
|
28
|
+
import { Check, ChevronsUpDown, CheckCheck, Eraser, Pencil, Plus, Save, Shield, Trash2, } from 'lucide-react';
|
|
29
29
|
import { toast } from 'sonner';
|
|
30
30
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
31
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';
|
|
@@ -197,7 +197,7 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
197
197
|
const [loadingPerms, setLoadingPerms] = React.useState(false);
|
|
198
198
|
const [saving, setSaving] = React.useState(false);
|
|
199
199
|
const [roleOpen, setRoleOpen] = React.useState(false);
|
|
200
|
-
const [
|
|
200
|
+
const [moduleOpen, setModuleOpen] = React.useState(false);
|
|
201
201
|
// Pending role switch while there are unsaved changes.
|
|
202
202
|
const [pendingRoleId, setPendingRoleId] = React.useState(null);
|
|
203
203
|
const [roleDialog, setRoleDialog] = React.useState({ open: false, mode: 'create', label: '', color: ROLE_COLORS[5] });
|
|
@@ -264,8 +264,6 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
264
264
|
const activeRole = React.useMemo(() => roles?.find((r) => r.id === activeRoleId) ?? null, [roles, activeRoleId]);
|
|
265
265
|
const activeModule = React.useMemo(() => allModules.find((m) => m.key === activeModuleKey) ?? null, [allModules, activeModuleKey]);
|
|
266
266
|
const dirty = baseline !== null && draft !== null && !capabilitySetsEqual(baseline, draft);
|
|
267
|
-
// Flat module list, optionally filtered by the search.
|
|
268
|
-
const visibleGroups = React.useMemo(() => filterModuleGroups(groups ?? [], moduleQuery), [groups, moduleQuery]);
|
|
269
267
|
// ---- capability edits ---------------------------------------------------
|
|
270
268
|
const toggleCapability = React.useCallback((cap) => {
|
|
271
269
|
setDraft((prev) => {
|
|
@@ -430,9 +428,20 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
|
|
|
430
428
|
setRoleOpen(false);
|
|
431
429
|
}, children: [_jsx("span", { className: "mr-2 h-2 w-2 shrink-0 rounded-full", style: {
|
|
432
430
|
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\
|
|
434
|
-
|
|
435
|
-
|
|
431
|
+
}, "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\u00F3dulo" }), _jsx(CardDescription, { children: "Elige el m\u00F3dulo cuyas acciones quieres configurar." })] }), _jsx(CardContent, { children: _jsxs(Popover, { open: moduleOpen, onOpenChange: setModuleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": moduleOpen, className: "w-full justify-between font-normal", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [activeModule && (_jsx(DynamicIcon, { name: activeModule.icon ||
|
|
432
|
+
(activeModule.kind === 'screen'
|
|
433
|
+
? 'Eye'
|
|
434
|
+
: 'Square'), className: "h-4 w-4 shrink-0 opacity-70" })), _jsx("span", { className: "truncate", children: activeModule
|
|
435
|
+
? activeModule.label
|
|
436
|
+
: 'Seleccionar módulo…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "w-[var(--radix-popover-trigger-width)] min-w-[280px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Buscar m\u00F3dulo\u2026" }), _jsxs(CommandList, { className: "max-h-[360px]", children: [_jsx(CommandEmpty, { children: "Sin m\u00F3dulos." }), (groups ?? []).map((group, gi) => (_jsx(CommandGroup, { heading: group.title || undefined, children: group.modules.map((mod) => (_jsxs(CommandItem, { value: `${group.title} ${mod.label} ${mod.key}`, onSelect: () => {
|
|
437
|
+
setActiveModuleKey(mod.key);
|
|
438
|
+
setModuleOpen(false);
|
|
439
|
+
}, children: [_jsx(DynamicIcon, { name: mod.icon ||
|
|
440
|
+
(mod.kind === 'screen'
|
|
441
|
+
? 'Eye'
|
|
442
|
+
: 'Square'), className: "mr-2 h-4 w-4 shrink-0 opacity-70" }), _jsx("span", { className: "truncate", children: mod.label }), draft &&
|
|
443
|
+
grantedCountForModule(draft, mod) >
|
|
444
|
+
0 && (_jsxs(Badge, { variant: "secondary", className: "ml-auto shrink-0 tabular-nums", children: [grantedCountForModule(draft, mod), "/", mod.actions.length] })), mod.key === activeModuleKey && (_jsx(Check, { className: "ml-2 h-4 w-4 shrink-0" }))] }, 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
445
|
(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
446
|
? `${activeModuleGroupTitle || 'Sistema'} · configura las acciones permitidas`
|
|
438
447
|
: '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) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "18.16.
|
|
3
|
+
"version": "18.16.1",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"sonner": ">=1.7",
|
|
35
35
|
"zustand": ">=5",
|
|
36
36
|
"@asteby/metacore-sdk": "^3.2.0",
|
|
37
|
-
"@asteby/metacore-ui": "^2.5.
|
|
37
|
+
"@asteby/metacore-ui": "^2.5.2"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@tanstack/react-router": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"vitest": "^4.0.0",
|
|
65
65
|
"zustand": "^5.0.0",
|
|
66
66
|
"@asteby/metacore-sdk": "3.2.0",
|
|
67
|
-
"@asteby/metacore-ui": "2.5.
|
|
67
|
+
"@asteby/metacore-ui": "2.5.2"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"build": "tsc -p tsconfig.json",
|
|
@@ -184,50 +184,59 @@ describe('helpers puros', () => {
|
|
|
184
184
|
})
|
|
185
185
|
})
|
|
186
186
|
|
|
187
|
-
describe('PermissionsManager (
|
|
187
|
+
describe('PermissionsManager (módulo como combobox agrupado)', () => {
|
|
188
|
+
// The module picker is a grouped combobox (same pattern as the role
|
|
189
|
+
// selector): open it, then click the module's option.
|
|
190
|
+
const moduleTrigger = () => {
|
|
191
|
+
// Two role="combobox" triggers: [0] role selector, [1] module selector.
|
|
192
|
+
const triggers = screen.getAllByRole('combobox')
|
|
193
|
+
return triggers[triggers.length - 1]
|
|
194
|
+
}
|
|
195
|
+
const selectModule = async (name: RegExp) => {
|
|
196
|
+
fireEvent.click(moduleTrigger())
|
|
197
|
+
fireEvent.click(await screen.findByRole('option', { name }))
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
it('renderiza catálogo, auto-selecciona rol y primer módulo, contador N/M', async () => {
|
|
189
201
|
const props = makeProps()
|
|
190
202
|
render(<PermissionsManager {...props} />)
|
|
191
203
|
|
|
192
|
-
// Primer módulo = "Usuarios" (
|
|
193
|
-
|
|
204
|
+
// Primer módulo = "Usuarios" (auto-seleccionado) → su nombre en el trigger.
|
|
205
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
194
206
|
expect(props.loadRolePermissions).toHaveBeenCalledWith('r1')
|
|
195
207
|
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
expect(screen.getByRole('
|
|
200
|
-
expect(screen.getByRole('
|
|
208
|
+
// Al abrir el combobox: grupos (CommandGroup heading) + opciones.
|
|
209
|
+
fireEvent.click(moduleTrigger())
|
|
210
|
+
expect(await screen.findByText('Punto de venta')).toBeTruthy()
|
|
211
|
+
expect(screen.getByRole('option', { name: /Pedidos POS/ })).toBeTruthy()
|
|
212
|
+
expect(screen.getByRole('option', { name: /Terminal/ })).toBeTruthy()
|
|
201
213
|
|
|
202
214
|
// Generales presentes con descripción.
|
|
203
215
|
expect(screen.getByText('Permisos Generales')).toBeTruthy()
|
|
204
216
|
expect(screen.getByText('Trabajar fuera de horario')).toBeTruthy()
|
|
205
217
|
})
|
|
206
218
|
|
|
207
|
-
it('CERO acordeones: el
|
|
219
|
+
it('CERO acordeones: el módulo es un combobox, no un folder colapsable', async () => {
|
|
208
220
|
const props = makeProps()
|
|
209
221
|
render(<PermissionsManager {...props} />)
|
|
210
|
-
await
|
|
211
|
-
// El header
|
|
212
|
-
|
|
213
|
-
expect(
|
|
214
|
-
expect(header.getAttribute('role')).toBe('heading')
|
|
215
|
-
// No existe ningún botón cuyo accesible name sea el título del grupo
|
|
216
|
-
// (lo que delataría un CollapsibleTrigger).
|
|
222
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
223
|
+
// El header de grupo "Punto de venta" vive DENTRO del popover (no en la
|
|
224
|
+
// columna), así que no existe hasta abrir el combobox — nada de folders.
|
|
225
|
+
expect(screen.queryByText('Punto de venta')).toBeNull()
|
|
217
226
|
expect(screen.queryByRole('button', { name: 'Punto de venta' })).toBeNull()
|
|
227
|
+
// El trigger del módulo es un combobox accesible.
|
|
228
|
+
expect(moduleTrigger().getAttribute('role')).toBe('combobox')
|
|
218
229
|
})
|
|
219
230
|
|
|
220
|
-
it('
|
|
231
|
+
it('seleccionar un módulo en el combobox muestra su grid', async () => {
|
|
221
232
|
const props = makeProps()
|
|
222
233
|
render(<PermissionsManager {...props} />)
|
|
223
|
-
await
|
|
234
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
224
235
|
|
|
225
|
-
|
|
226
|
-
fireEvent.click(screen.getByRole('button', { name: /Pedidos POS/ }))
|
|
236
|
+
await selectModule(/Pedidos POS/)
|
|
227
237
|
expect(await screen.findByText('Pagar')).toBeTruthy()
|
|
228
238
|
|
|
229
|
-
|
|
230
|
-
fireEvent.click(screen.getByRole('button', { name: /Terminal/ }))
|
|
239
|
+
await selectModule(/Terminal/)
|
|
231
240
|
expect(await screen.findByText('Acceder')).toBeTruthy()
|
|
232
241
|
await waitFor(() => expect(screen.queryByText('Pagar')).toBeNull())
|
|
233
242
|
})
|
|
@@ -235,9 +244,9 @@ describe('PermissionsManager (lista plana, shape nuevo)', () => {
|
|
|
235
244
|
it('marcar el screen "Acceder" produce capability screen.<navKey>.access', async () => {
|
|
236
245
|
const props = makeProps()
|
|
237
246
|
render(<PermissionsManager {...props} />)
|
|
238
|
-
await
|
|
247
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
239
248
|
|
|
240
|
-
|
|
249
|
+
await selectModule(/Terminal/)
|
|
241
250
|
await screen.findByText('Acceder')
|
|
242
251
|
fireEvent.click(screen.getByRole('checkbox', { name: /Acceder/ }))
|
|
243
252
|
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
@@ -252,9 +261,9 @@ describe('PermissionsManager (lista plana, shape nuevo)', () => {
|
|
|
252
261
|
it('marcar una acción + un general y guardar llama sync con el set completo', async () => {
|
|
253
262
|
const props = makeProps()
|
|
254
263
|
render(<PermissionsManager {...props} />)
|
|
255
|
-
await
|
|
264
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
256
265
|
|
|
257
|
-
|
|
266
|
+
await selectModule(/Pedidos POS/)
|
|
258
267
|
await screen.findByText('Pagar')
|
|
259
268
|
fireEvent.click(screen.getByRole('checkbox', { name: /Pagar/ }))
|
|
260
269
|
fireEvent.click(screen.getByRole('checkbox', { name: /Trabajar fuera de horario/ }))
|
|
@@ -275,8 +284,8 @@ describe('PermissionsManager (lista plana, shape nuevo)', () => {
|
|
|
275
284
|
it('marcar todo / limpiar operan sobre el módulo activo', async () => {
|
|
276
285
|
const props = makeProps()
|
|
277
286
|
render(<PermissionsManager {...props} />)
|
|
278
|
-
await
|
|
279
|
-
|
|
287
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
288
|
+
await selectModule(/Pedidos POS/)
|
|
280
289
|
await screen.findByText('Pagar')
|
|
281
290
|
|
|
282
291
|
fireEvent.click(screen.getByRole('button', { name: /Marcar todo/ }))
|
|
@@ -297,29 +306,31 @@ describe('PermissionsManager (lista plana, shape nuevo)', () => {
|
|
|
297
306
|
it('guardar deshabilitado sin cambios', async () => {
|
|
298
307
|
const props = makeProps()
|
|
299
308
|
render(<PermissionsManager {...props} />)
|
|
300
|
-
await
|
|
309
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
301
310
|
const save = screen.getByRole('button', { name: /Guardar permisos/ }) as HTMLButtonElement
|
|
302
311
|
expect(save.disabled).toBe(true)
|
|
303
312
|
})
|
|
304
313
|
|
|
305
|
-
it('
|
|
314
|
+
it('el combobox de módulo filtra por búsqueda', async () => {
|
|
306
315
|
const props = makeProps()
|
|
307
316
|
render(<PermissionsManager {...props} />)
|
|
308
|
-
await
|
|
317
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
309
318
|
|
|
310
|
-
fireEvent.
|
|
319
|
+
fireEvent.click(moduleTrigger())
|
|
320
|
+
fireEvent.change(await screen.findByPlaceholderText('Buscar módulo…'), {
|
|
311
321
|
target: { value: 'terminal' },
|
|
312
322
|
})
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
expect(screen.queryByRole('
|
|
323
|
+
await waitFor(() =>
|
|
324
|
+
expect(screen.getByRole('option', { name: /Terminal/ })).toBeTruthy(),
|
|
325
|
+
)
|
|
326
|
+
expect(screen.queryByRole('option', { name: /Pedidos POS/ })).toBeNull()
|
|
327
|
+
expect(screen.queryByRole('option', { name: /Usuarios/ })).toBeNull()
|
|
317
328
|
})
|
|
318
329
|
|
|
319
330
|
it('oculta Nuevo rol / Editar / Eliminar cuando no hay mutators de rol', async () => {
|
|
320
331
|
const props = makeProps()
|
|
321
332
|
render(<PermissionsManager {...props} />)
|
|
322
|
-
await
|
|
333
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
323
334
|
expect(screen.queryByRole('button', { name: /Nuevo rol/ })).toBeNull()
|
|
324
335
|
expect(screen.queryByRole('button', { name: 'Editar rol' })).toBeNull()
|
|
325
336
|
expect(screen.queryByRole('button', { name: 'Eliminar rol' })).toBeNull()
|
|
@@ -331,7 +342,7 @@ describe('PermissionsManager (lista plana, shape nuevo)', () => {
|
|
|
331
342
|
deleteRole: vi.fn(async () => {}),
|
|
332
343
|
})
|
|
333
344
|
render(<PermissionsManager {...props} />)
|
|
334
|
-
await
|
|
345
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
335
346
|
expect(screen.queryByRole('button', { name: 'Quitar rol seleccionado' })).toBeNull()
|
|
336
347
|
expect(screen.getByRole('button', { name: 'Editar rol' })).toBeTruthy()
|
|
337
348
|
expect(screen.getByRole('button', { name: 'Eliminar rol' })).toBeTruthy()
|
|
@@ -344,7 +355,7 @@ describe('PermissionsManager (lista plana, shape nuevo)', () => {
|
|
|
344
355
|
deleteRole: vi.fn(async () => {}),
|
|
345
356
|
})
|
|
346
357
|
render(<PermissionsManager {...props} />)
|
|
347
|
-
await
|
|
358
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
348
359
|
expect(screen.getByRole('button', { name: /Nuevo rol/ })).toBeTruthy()
|
|
349
360
|
expect(screen.getByRole('button', { name: 'Editar rol' })).toBeTruthy()
|
|
350
361
|
expect(screen.getByRole('button', { name: 'Eliminar rol' })).toBeTruthy()
|
|
@@ -358,11 +369,13 @@ describe('PermissionsManager (retrocompat shape viejo {modules})', () => {
|
|
|
358
369
|
|
|
359
370
|
// Auto-selección del primer módulo legacy (pos_orders → grupo "Punto de venta").
|
|
360
371
|
expect(await screen.findByText('Pagar')).toBeTruthy()
|
|
361
|
-
//
|
|
362
|
-
|
|
372
|
+
// Los grupos derivados del addon viven dentro del combobox de módulo.
|
|
373
|
+
const triggers = screen.getAllByRole('combobox')
|
|
374
|
+
fireEvent.click(triggers[triggers.length - 1])
|
|
375
|
+
expect(await screen.findByText('Punto de venta')).toBeTruthy()
|
|
363
376
|
// El grupo Sistema (users sin addon) también.
|
|
364
377
|
expect(screen.getByText('Sistema')).toBeTruthy()
|
|
365
|
-
expect(screen.getByRole('
|
|
378
|
+
expect(screen.getByRole('option', { name: /Usuarios/ })).toBeTruthy()
|
|
366
379
|
})
|
|
367
380
|
|
|
368
381
|
it('legacy: click + guardar produce capabilities correctas', async () => {
|
|
@@ -463,7 +463,7 @@ export function PermissionsManager({
|
|
|
463
463
|
const [saving, setSaving] = React.useState(false)
|
|
464
464
|
|
|
465
465
|
const [roleOpen, setRoleOpen] = React.useState(false)
|
|
466
|
-
const [
|
|
466
|
+
const [moduleOpen, setModuleOpen] = React.useState(false)
|
|
467
467
|
|
|
468
468
|
// Pending role switch while there are unsaved changes.
|
|
469
469
|
const [pendingRoleId, setPendingRoleId] = React.useState<string | null>(null)
|
|
@@ -547,12 +547,6 @@ export function PermissionsManager({
|
|
|
547
547
|
|
|
548
548
|
const dirty = baseline !== null && draft !== null && !capabilitySetsEqual(baseline, draft)
|
|
549
549
|
|
|
550
|
-
// Flat module list, optionally filtered by the search.
|
|
551
|
-
const visibleGroups = React.useMemo(
|
|
552
|
-
() => filterModuleGroups(groups ?? [], moduleQuery),
|
|
553
|
-
[groups, moduleQuery],
|
|
554
|
-
)
|
|
555
|
-
|
|
556
550
|
// ---- capability edits ---------------------------------------------------
|
|
557
551
|
const toggleCapability = React.useCallback((cap: string) => {
|
|
558
552
|
setDraft((prev) => {
|
|
@@ -887,73 +881,104 @@ export function PermissionsManager({
|
|
|
887
881
|
</CardContent>
|
|
888
882
|
</Card>
|
|
889
883
|
|
|
890
|
-
{/* Card:
|
|
884
|
+
{/* Card: Módulo — a grouped combobox, same pattern as the role
|
|
885
|
+
selector above (compact; the long flat list felt heavy). */}
|
|
891
886
|
<Card>
|
|
892
887
|
<CardHeader>
|
|
893
|
-
<CardTitle className="text-base">
|
|
888
|
+
<CardTitle className="text-base">Módulo</CardTitle>
|
|
894
889
|
<CardDescription>
|
|
895
890
|
Elige el módulo cuyas acciones quieres configurar.
|
|
896
891
|
</CardDescription>
|
|
897
892
|
</CardHeader>
|
|
898
|
-
<CardContent
|
|
899
|
-
<
|
|
900
|
-
<
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
className="-mx-1 flex max-h-[460px] flex-col gap-0.5 overflow-y-auto px-1"
|
|
916
|
-
>
|
|
917
|
-
{visibleGroups.length === 0 ? (
|
|
918
|
-
<p className="px-2 py-6 text-center text-sm text-muted-foreground">
|
|
919
|
-
Sin módulos.
|
|
920
|
-
</p>
|
|
921
|
-
) : (
|
|
922
|
-
visibleGroups.map((group, gi) => (
|
|
923
|
-
<div
|
|
924
|
-
key={group.title || `__untitled_${gi}`}
|
|
925
|
-
className="flex flex-col gap-0.5"
|
|
926
|
-
>
|
|
927
|
-
{group.title && (
|
|
928
|
-
<div
|
|
929
|
-
role="heading"
|
|
930
|
-
aria-level={3}
|
|
931
|
-
className={cn(
|
|
932
|
-
'px-2 pb-1 pt-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground',
|
|
933
|
-
gi === 0 && 'pt-1',
|
|
934
|
-
)}
|
|
935
|
-
>
|
|
936
|
-
{group.title}
|
|
937
|
-
</div>
|
|
938
|
-
)}
|
|
939
|
-
{group.modules.map((mod) => (
|
|
940
|
-
<ModuleRow
|
|
941
|
-
key={mod.key}
|
|
942
|
-
module={mod}
|
|
943
|
-
active={mod.key === activeModuleKey}
|
|
944
|
-
granted={
|
|
945
|
-
draft
|
|
946
|
-
? grantedCountForModule(draft, mod)
|
|
947
|
-
: 0
|
|
893
|
+
<CardContent>
|
|
894
|
+
<Popover open={moduleOpen} onOpenChange={setModuleOpen}>
|
|
895
|
+
<PopoverTrigger asChild>
|
|
896
|
+
<Button
|
|
897
|
+
variant="outline"
|
|
898
|
+
role="combobox"
|
|
899
|
+
aria-expanded={moduleOpen}
|
|
900
|
+
className="w-full justify-between font-normal"
|
|
901
|
+
>
|
|
902
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
903
|
+
{activeModule && (
|
|
904
|
+
<DynamicIcon
|
|
905
|
+
name={
|
|
906
|
+
activeModule.icon ||
|
|
907
|
+
(activeModule.kind === 'screen'
|
|
908
|
+
? 'Eye'
|
|
909
|
+
: 'Square')
|
|
948
910
|
}
|
|
949
|
-
|
|
950
|
-
onSelect={() => setActiveModuleKey(mod.key)}
|
|
911
|
+
className="h-4 w-4 shrink-0 opacity-70"
|
|
951
912
|
/>
|
|
913
|
+
)}
|
|
914
|
+
<span className="truncate">
|
|
915
|
+
{activeModule
|
|
916
|
+
? activeModule.label
|
|
917
|
+
: 'Seleccionar módulo…'}
|
|
918
|
+
</span>
|
|
919
|
+
</span>
|
|
920
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
921
|
+
</Button>
|
|
922
|
+
</PopoverTrigger>
|
|
923
|
+
<PopoverContent
|
|
924
|
+
className="w-[var(--radix-popover-trigger-width)] min-w-[280px] p-0"
|
|
925
|
+
align="start"
|
|
926
|
+
>
|
|
927
|
+
<Command>
|
|
928
|
+
<CommandInput placeholder="Buscar módulo…" />
|
|
929
|
+
<CommandList className="max-h-[360px]">
|
|
930
|
+
<CommandEmpty>Sin módulos.</CommandEmpty>
|
|
931
|
+
{(groups ?? []).map((group, gi) => (
|
|
932
|
+
<CommandGroup
|
|
933
|
+
key={group.title || `__untitled_${gi}`}
|
|
934
|
+
heading={group.title || undefined}
|
|
935
|
+
>
|
|
936
|
+
{group.modules.map((mod) => (
|
|
937
|
+
<CommandItem
|
|
938
|
+
key={mod.key}
|
|
939
|
+
value={`${group.title} ${mod.label} ${mod.key}`}
|
|
940
|
+
onSelect={() => {
|
|
941
|
+
setActiveModuleKey(mod.key)
|
|
942
|
+
setModuleOpen(false)
|
|
943
|
+
}}
|
|
944
|
+
>
|
|
945
|
+
<DynamicIcon
|
|
946
|
+
name={
|
|
947
|
+
mod.icon ||
|
|
948
|
+
(mod.kind === 'screen'
|
|
949
|
+
? 'Eye'
|
|
950
|
+
: 'Square')
|
|
951
|
+
}
|
|
952
|
+
className="mr-2 h-4 w-4 shrink-0 opacity-70"
|
|
953
|
+
/>
|
|
954
|
+
<span className="truncate">
|
|
955
|
+
{mod.label}
|
|
956
|
+
</span>
|
|
957
|
+
{draft &&
|
|
958
|
+
grantedCountForModule(draft, mod) >
|
|
959
|
+
0 && (
|
|
960
|
+
<Badge
|
|
961
|
+
variant="secondary"
|
|
962
|
+
className="ml-auto shrink-0 tabular-nums"
|
|
963
|
+
>
|
|
964
|
+
{grantedCountForModule(
|
|
965
|
+
draft,
|
|
966
|
+
mod,
|
|
967
|
+
)}
|
|
968
|
+
/{mod.actions.length}
|
|
969
|
+
</Badge>
|
|
970
|
+
)}
|
|
971
|
+
{mod.key === activeModuleKey && (
|
|
972
|
+
<Check className="ml-2 h-4 w-4 shrink-0" />
|
|
973
|
+
)}
|
|
974
|
+
</CommandItem>
|
|
975
|
+
))}
|
|
976
|
+
</CommandGroup>
|
|
952
977
|
))}
|
|
953
|
-
</
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
</
|
|
978
|
+
</CommandList>
|
|
979
|
+
</Command>
|
|
980
|
+
</PopoverContent>
|
|
981
|
+
</Popover>
|
|
957
982
|
</CardContent>
|
|
958
983
|
</Card>
|
|
959
984
|
</div>
|