@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, Search, Shield, Trash2, } from 'lucide-react';
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 [moduleQuery, setModuleQuery] = React.useState('');
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\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 ||
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.0",
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.1"
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.1"
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 (lista plana, shape nuevo)', () => {
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" (grupo sin título, va primero).
193
- expect(await screen.findAllByText('Usuarios')).toBeTruthy()
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
- // Headers de grupo grises (no colapsables): el del grupo con título.
197
- expect(screen.getByText('Punto de venta')).toBeTruthy()
198
- // Filas de módulo de la lista plana.
199
- expect(screen.getByRole('button', { name: /Pedidos POS/ })).toBeTruthy()
200
- expect(screen.getByRole('button', { name: /Terminal/ })).toBeTruthy()
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 header de grupo es un heading, no un botón colapsable', async () => {
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 screen.findAllByText('Usuarios')
211
- // El header gris "Punto de venta" es un heading, NO un button (sin folder/acordeón).
212
- const header = screen.getByText('Punto de venta')
213
- expect(header.closest('button')).toBeNull()
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('click directo en una fila selecciona el módulo y muestra su grid', async () => {
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 screen.findAllByText('Usuarios')
234
+ await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
224
235
 
225
- // Selecciono "Pedidos POS" → su grid aparece a la derecha.
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
- // Selecciono el screen "Terminal" → acción "Acceder".
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 screen.findAllByText('Usuarios')
247
+ await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
239
248
 
240
- fireEvent.click(screen.getByRole('button', { name: /Terminal/ }))
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 screen.findAllByText('Usuarios')
264
+ await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
256
265
 
257
- fireEvent.click(screen.getByRole('button', { name: /Pedidos POS/ }))
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 screen.findAllByText('Usuarios')
279
- fireEvent.click(screen.getByRole('button', { name: /Pedidos POS/ }))
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 screen.findAllByText('Usuarios')
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('la búsqueda filtra las filas de la lista plana', async () => {
314
+ it('el combobox de módulo filtra por búsqueda', async () => {
306
315
  const props = makeProps()
307
316
  render(<PermissionsManager {...props} />)
308
- await screen.findAllByText('Usuarios')
317
+ await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
309
318
 
310
- fireEvent.change(screen.getByLabelText('Buscar módulo'), {
319
+ fireEvent.click(moduleTrigger())
320
+ fireEvent.change(await screen.findByPlaceholderText('Buscar módulo…'), {
311
321
  target: { value: 'terminal' },
312
322
  })
313
- // Solo el módulo con match permanece como fila; otros se ocultan.
314
- expect(screen.getByRole('button', { name: /Terminal/ })).toBeTruthy()
315
- expect(screen.queryByRole('button', { name: /Pedidos POS/ })).toBeNull()
316
- expect(screen.queryByRole('button', { name: /Usuarios/ })).toBeNull()
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 screen.findAllByText('Usuarios')
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 screen.findAllByText('Usuarios')
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 screen.findAllByText('Usuarios')
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
- // Header gris derivado del addon.
362
- expect(screen.getByText('Punto de venta')).toBeTruthy()
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('button', { name: /Usuarios/ })).toBeTruthy()
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 [moduleQuery, setModuleQuery] = React.useState('')
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: Módulos (flat list, mirrors the sidebar no folders) */}
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">Módulos</CardTitle>
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 className="flex flex-col gap-3">
899
- <div className="relative">
900
- <Search className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
901
- <Input
902
- value={moduleQuery}
903
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
904
- setModuleQuery(e.target.value)
905
- }
906
- placeholder="Buscar módulo…"
907
- aria-label="Buscar módulo"
908
- className="pl-8"
909
- />
910
- </div>
911
-
912
- <div
913
- role="list"
914
- aria-label="Módulos"
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
- total={mod.actions.length}
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
- </div>
954
- ))
955
- )}
956
- </div>
978
+ </CommandList>
979
+ </Command>
980
+ </PopoverContent>
981
+ </Popover>
957
982
  </CardContent>
958
983
  </Card>
959
984
  </div>