@asteby/metacore-runtime-react 18.15.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 +17 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/permissions-manager.d.ts +54 -17
- package/dist/permissions-manager.d.ts.map +1 -1
- package/dist/permissions-manager.js +88 -51
- package/package.json +3 -3
- package/src/__tests__/permissions-manager.test.tsx +228 -91
- package/src/index.ts +6 -0
- package/src/permissions-manager.tsx +218 -157
|
@@ -10,13 +10,66 @@ import {
|
|
|
10
10
|
moduleCapabilities,
|
|
11
11
|
grantedCountForModule,
|
|
12
12
|
capabilitySetsEqual,
|
|
13
|
-
|
|
13
|
+
normalizeCatalogGroups,
|
|
14
|
+
flattenGroups,
|
|
14
15
|
filterModuleGroups,
|
|
16
|
+
defaultActionIcon,
|
|
15
17
|
type PermissionsCatalog,
|
|
18
|
+
type GroupedPermissionsCatalog,
|
|
19
|
+
type FlatPermissionsCatalog,
|
|
16
20
|
type RoleDef,
|
|
17
21
|
} from '../permissions-manager'
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
// New (preferred) shape: pre-grouped flat list, mirrors the host sidebar.
|
|
24
|
+
const grouped: GroupedPermissionsCatalog = {
|
|
25
|
+
groups: [
|
|
26
|
+
{
|
|
27
|
+
title: '', // core/infra — no header
|
|
28
|
+
modules: [
|
|
29
|
+
{
|
|
30
|
+
key: 'users',
|
|
31
|
+
label: 'Usuarios',
|
|
32
|
+
icon: 'Users',
|
|
33
|
+
kind: 'model',
|
|
34
|
+
actions: [{ key: 'index', label: 'Listar', icon: 'List', kind: 'crud' }],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: 'Punto de venta',
|
|
40
|
+
modules: [
|
|
41
|
+
{
|
|
42
|
+
key: 'pos_orders',
|
|
43
|
+
label: 'Pedidos POS',
|
|
44
|
+
icon: 'ShoppingCart',
|
|
45
|
+
kind: 'model',
|
|
46
|
+
actions: [
|
|
47
|
+
{ key: 'index', label: 'Listar', icon: 'List', kind: 'crud' },
|
|
48
|
+
{ key: 'create', label: 'Crear', icon: 'Plus', kind: 'crud' },
|
|
49
|
+
{ key: 'pagar', label: 'Pagar', icon: 'CreditCard', kind: 'custom' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: 'screen.pos_terminal',
|
|
54
|
+
label: 'Terminal',
|
|
55
|
+
icon: 'Monitor',
|
|
56
|
+
kind: 'screen',
|
|
57
|
+
actions: [{ key: 'access', label: 'Acceder', icon: 'Eye', kind: 'screen' }],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
general: [
|
|
63
|
+
{
|
|
64
|
+
key: 'general.work_after_hours',
|
|
65
|
+
label: 'Trabajar fuera de horario',
|
|
66
|
+
description: 'Permite operar fuera del horario configurado.',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Legacy flat shape (retrocompat): no `groups`, modules without `kind`.
|
|
72
|
+
const legacy: FlatPermissionsCatalog = {
|
|
20
73
|
modules: [
|
|
21
74
|
{
|
|
22
75
|
key: 'pos_orders',
|
|
@@ -26,18 +79,9 @@ const catalog: PermissionsCatalog = {
|
|
|
26
79
|
addon_label: 'Punto de venta',
|
|
27
80
|
actions: [
|
|
28
81
|
{ key: 'index', label: 'Listar', icon: 'List', kind: 'crud' },
|
|
29
|
-
{ key: 'create', label: 'Crear', icon: 'Plus', kind: 'crud' },
|
|
30
82
|
{ key: 'pagar', label: 'Pagar', icon: 'CreditCard', kind: 'custom' },
|
|
31
83
|
],
|
|
32
84
|
},
|
|
33
|
-
{
|
|
34
|
-
key: 'pos_sessions',
|
|
35
|
-
label: 'Sesiones POS',
|
|
36
|
-
icon: 'Clock',
|
|
37
|
-
addon_key: 'pos',
|
|
38
|
-
addon_label: 'Punto de venta',
|
|
39
|
-
actions: [{ key: 'index', label: 'Listar', icon: 'List', kind: 'crud' }],
|
|
40
|
-
},
|
|
41
85
|
{
|
|
42
86
|
key: 'users',
|
|
43
87
|
label: 'Usuarios',
|
|
@@ -45,18 +89,15 @@ const catalog: PermissionsCatalog = {
|
|
|
45
89
|
actions: [{ key: 'index', label: 'Listar', icon: 'List', kind: 'crud' }],
|
|
46
90
|
},
|
|
47
91
|
],
|
|
48
|
-
general: [
|
|
49
|
-
{
|
|
50
|
-
key: 'general.work_after_hours',
|
|
51
|
-
label: 'Trabajar fuera de horario',
|
|
52
|
-
description: 'Permite operar fuera del horario configurado.',
|
|
53
|
-
},
|
|
54
|
-
],
|
|
92
|
+
general: [],
|
|
55
93
|
}
|
|
56
94
|
|
|
57
95
|
const roles: RoleDef[] = [{ id: 'r1', name: 'cashier', label: 'Cajero', color: '#22c55e' }]
|
|
58
96
|
|
|
59
|
-
function makeProps(
|
|
97
|
+
function makeProps(
|
|
98
|
+
catalog: PermissionsCatalog = grouped,
|
|
99
|
+
overrides: Partial<Parameters<typeof PermissionsManager>[0]> = {},
|
|
100
|
+
) {
|
|
60
101
|
return {
|
|
61
102
|
loadModules: vi.fn(async () => catalog),
|
|
62
103
|
loadRoles: vi.fn(async () => roles),
|
|
@@ -71,8 +112,14 @@ describe('helpers puros', () => {
|
|
|
71
112
|
expect(moduleActionCapability('Pos_Orders', 'pagar')).toBe('pos_orders.pagar')
|
|
72
113
|
})
|
|
73
114
|
|
|
115
|
+
it('screen capability = screen.<navKey>.access', () => {
|
|
116
|
+
expect(moduleActionCapability('screen.pos_terminal', 'access')).toBe(
|
|
117
|
+
'screen.pos_terminal.access',
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
74
121
|
it('moduleCapabilities y grantedCountForModule', () => {
|
|
75
|
-
const mod =
|
|
122
|
+
const mod = grouped.groups[1].modules[0] // pos_orders
|
|
76
123
|
expect(moduleCapabilities(mod)).toEqual([
|
|
77
124
|
'pos_orders.index',
|
|
78
125
|
'pos_orders.create',
|
|
@@ -86,19 +133,47 @@ describe('helpers puros', () => {
|
|
|
86
133
|
expect(capabilitySetsEqual(new Set(['a']), new Set(['a', 'b']))).toBe(false)
|
|
87
134
|
})
|
|
88
135
|
|
|
89
|
-
it('
|
|
90
|
-
|
|
91
|
-
expect(
|
|
92
|
-
expect(
|
|
93
|
-
expect(groups[1].modules.map((m) => m.key)).toEqual(['users'])
|
|
136
|
+
it('defaultActionIcon mapea access→Eye y screens', () => {
|
|
137
|
+
expect(defaultActionIcon('access')).toBe('Eye')
|
|
138
|
+
expect(defaultActionIcon('algo', 'screen')).toBe('Eye')
|
|
139
|
+
expect(defaultActionIcon('index')).toBe('List')
|
|
94
140
|
})
|
|
95
141
|
|
|
96
|
-
|
|
97
|
-
|
|
142
|
+
describe('normalizeCatalogGroups', () => {
|
|
143
|
+
it('pasa el shape nuevo {groups} tal cual, default kind:model', () => {
|
|
144
|
+
const out = normalizeCatalogGroups(grouped)
|
|
145
|
+
expect(out.map((g) => g.title)).toEqual(['', 'Punto de venta'])
|
|
146
|
+
expect(out[1].modules.map((m) => m.key)).toEqual(['pos_orders', 'screen.pos_terminal'])
|
|
147
|
+
// El screen conserva su kind; el modelo conserva model.
|
|
148
|
+
expect(out[1].modules[0].kind).toBe('model')
|
|
149
|
+
expect(out[1].modules[1].kind).toBe('screen')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('retrocompat: envuelve el shape viejo {modules} y agrupa por addon, kind:model', () => {
|
|
153
|
+
const out = normalizeCatalogGroups(legacy)
|
|
154
|
+
// Agrupa por addon_label / "Sistema" para los sin addon.
|
|
155
|
+
expect(out.map((g) => g.title)).toEqual(['Punto de venta', 'Sistema'])
|
|
156
|
+
expect(out[0].modules.map((m) => m.key)).toEqual(['pos_orders'])
|
|
157
|
+
expect(out[1].modules.map((m) => m.key)).toEqual(['users'])
|
|
158
|
+
// Todos los módulos legacy quedan como model.
|
|
159
|
+
expect(flattenGroups(out).every((m) => m.kind === 'model')).toBe(true)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('flattenGroups recorre los grupos en orden', () => {
|
|
164
|
+
expect(flattenGroups(grouped.groups).map((m) => m.key)).toEqual([
|
|
165
|
+
'users',
|
|
166
|
+
'pos_orders',
|
|
167
|
+
'screen.pos_terminal',
|
|
168
|
+
])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('filterModuleGroups busca por módulo (accent/case-insensitive) o por título de grupo', () => {
|
|
172
|
+
const groups = grouped.groups
|
|
98
173
|
// Por nombre de módulo.
|
|
99
|
-
const
|
|
100
|
-
expect(
|
|
101
|
-
expect(
|
|
174
|
+
const byTerminal = filterModuleGroups(groups, 'terminal')
|
|
175
|
+
expect(byTerminal).toHaveLength(1)
|
|
176
|
+
expect(byTerminal[0].modules.map((m) => m.key)).toEqual(['screen.pos_terminal'])
|
|
102
177
|
// Por nombre de grupo trae todos sus módulos.
|
|
103
178
|
const byGroup = filterModuleGroups(groups, 'venta')
|
|
104
179
|
expect(byGroup[0].modules).toHaveLength(2)
|
|
@@ -109,66 +184,112 @@ describe('helpers puros', () => {
|
|
|
109
184
|
})
|
|
110
185
|
})
|
|
111
186
|
|
|
112
|
-
describe('PermissionsManager', () => {
|
|
113
|
-
|
|
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
|
+
|
|
200
|
+
it('renderiza catálogo, auto-selecciona rol y primer módulo, contador N/M', async () => {
|
|
114
201
|
const props = makeProps()
|
|
115
202
|
render(<PermissionsManager {...props} />)
|
|
116
203
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
expect(await screen.findAllByText('Pedidos POS')).toBeTruthy()
|
|
120
|
-
expect(await screen.findByText('Pagar')).toBeTruthy()
|
|
121
|
-
// El contador N/M del panel está presente (también lo refleja el árbol).
|
|
122
|
-
expect(screen.getAllByText('1/3').length).toBeGreaterThan(0)
|
|
204
|
+
// Primer módulo = "Usuarios" (auto-seleccionado) → su nombre en el trigger.
|
|
205
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
123
206
|
expect(props.loadRolePermissions).toHaveBeenCalledWith('r1')
|
|
124
207
|
|
|
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()
|
|
213
|
+
|
|
125
214
|
// Generales presentes con descripción.
|
|
126
215
|
expect(screen.getByText('Permisos Generales')).toBeTruthy()
|
|
127
216
|
expect(screen.getByText('Trabajar fuera de horario')).toBeTruthy()
|
|
128
217
|
})
|
|
129
218
|
|
|
130
|
-
it('
|
|
219
|
+
it('CERO acordeones: el módulo es un combobox, no un folder colapsable', async () => {
|
|
131
220
|
const props = makeProps()
|
|
132
221
|
render(<PermissionsManager {...props} />)
|
|
133
|
-
await
|
|
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()
|
|
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')
|
|
229
|
+
})
|
|
134
230
|
|
|
135
|
-
|
|
136
|
-
|
|
231
|
+
it('seleccionar un módulo en el combobox muestra su grid', async () => {
|
|
232
|
+
const props = makeProps()
|
|
233
|
+
render(<PermissionsManager {...props} />)
|
|
234
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
137
235
|
|
|
138
|
-
|
|
139
|
-
expect(screen.
|
|
140
|
-
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
236
|
+
await selectModule(/Pedidos POS/)
|
|
237
|
+
expect(await screen.findByText('Pagar')).toBeTruthy()
|
|
141
238
|
|
|
239
|
+
await selectModule(/Terminal/)
|
|
240
|
+
expect(await screen.findByText('Acceder')).toBeTruthy()
|
|
241
|
+
await waitFor(() => expect(screen.queryByText('Pagar')).toBeNull())
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('marcar el screen "Acceder" produce capability screen.<navKey>.access', async () => {
|
|
245
|
+
const props = makeProps()
|
|
246
|
+
render(<PermissionsManager {...props} />)
|
|
247
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
248
|
+
|
|
249
|
+
await selectModule(/Terminal/)
|
|
250
|
+
await screen.findByText('Acceder')
|
|
251
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Acceder/ }))
|
|
252
|
+
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
142
253
|
await waitFor(() =>
|
|
143
254
|
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
144
|
-
'general.work_after_hours',
|
|
145
255
|
'pos_orders.index',
|
|
146
|
-
'
|
|
256
|
+
'screen.pos_terminal.access',
|
|
147
257
|
]),
|
|
148
258
|
)
|
|
149
|
-
// Tras guardar, baseline = draft → dirty desaparece.
|
|
150
|
-
await waitFor(() => expect(screen.queryByText('Cambios sin guardar')).toBeNull())
|
|
151
259
|
})
|
|
152
260
|
|
|
153
|
-
it('
|
|
261
|
+
it('marcar una acción + un general y guardar llama sync con el set completo', async () => {
|
|
154
262
|
const props = makeProps()
|
|
155
263
|
render(<PermissionsManager {...props} />)
|
|
264
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
265
|
+
|
|
266
|
+
await selectModule(/Pedidos POS/)
|
|
156
267
|
await screen.findByText('Pagar')
|
|
268
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Pagar/ }))
|
|
269
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Trabajar fuera de horario/ }))
|
|
157
270
|
|
|
158
|
-
|
|
271
|
+
expect(screen.getByText('Cambios sin guardar')).toBeTruthy()
|
|
159
272
|
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
160
|
-
|
|
273
|
+
|
|
274
|
+
await waitFor(() =>
|
|
275
|
+
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
276
|
+
'general.work_after_hours',
|
|
277
|
+
'pos_orders.index',
|
|
278
|
+
'pos_orders.pagar',
|
|
279
|
+
]),
|
|
280
|
+
)
|
|
281
|
+
await waitFor(() => expect(screen.queryByText('Cambios sin guardar')).toBeNull())
|
|
161
282
|
})
|
|
162
283
|
|
|
163
284
|
it('marcar todo / limpiar operan sobre el módulo activo', async () => {
|
|
164
285
|
const props = makeProps()
|
|
165
286
|
render(<PermissionsManager {...props} />)
|
|
287
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
288
|
+
await selectModule(/Pedidos POS/)
|
|
166
289
|
await screen.findByText('Pagar')
|
|
167
290
|
|
|
168
291
|
fireEvent.click(screen.getByRole('button', { name: /Marcar todo/ }))
|
|
169
|
-
// 3/3 aparece en el panel y en el badge del árbol.
|
|
170
292
|
expect(screen.getAllByText('3/3').length).toBeGreaterThan(0)
|
|
171
|
-
|
|
172
293
|
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
173
294
|
await waitFor(() =>
|
|
174
295
|
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
@@ -185,74 +306,90 @@ describe('PermissionsManager', () => {
|
|
|
185
306
|
it('guardar deshabilitado sin cambios', async () => {
|
|
186
307
|
const props = makeProps()
|
|
187
308
|
render(<PermissionsManager {...props} />)
|
|
188
|
-
await
|
|
309
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
189
310
|
const save = screen.getByRole('button', { name: /Guardar permisos/ }) as HTMLButtonElement
|
|
190
311
|
expect(save.disabled).toBe(true)
|
|
191
312
|
})
|
|
192
313
|
|
|
193
|
-
it('
|
|
314
|
+
it('el combobox de módulo filtra por búsqueda', async () => {
|
|
194
315
|
const props = makeProps()
|
|
195
316
|
render(<PermissionsManager {...props} />)
|
|
196
|
-
await
|
|
197
|
-
expect(screen.queryByRole('button', { name: /Nuevo rol/ })).toBeNull()
|
|
198
|
-
expect(screen.queryByRole('button', { name: 'Editar rol' })).toBeNull()
|
|
199
|
-
expect(screen.queryByRole('button', { name: 'Eliminar rol' })).toBeNull()
|
|
200
|
-
})
|
|
317
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
201
318
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
expect(screen.
|
|
210
|
-
|
|
211
|
-
// Click en "Usuarios" (grupo Sistema) cambia el grid de acciones.
|
|
212
|
-
fireEvent.click(screen.getByRole('button', { name: /Usuarios/ }))
|
|
213
|
-
// El grid ahora muestra solo la acción de users; "Pagar" (de pos_orders) ya no.
|
|
214
|
-
await waitFor(() => expect(screen.queryByText('Pagar')).toBeNull())
|
|
215
|
-
// "Usuarios" titula el panel derecho además del árbol.
|
|
216
|
-
expect(screen.getAllByText('Usuarios').length).toBeGreaterThan(0)
|
|
319
|
+
fireEvent.click(moduleTrigger())
|
|
320
|
+
fireEvent.change(await screen.findByPlaceholderText('Buscar módulo…'), {
|
|
321
|
+
target: { value: 'terminal' },
|
|
322
|
+
})
|
|
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()
|
|
217
328
|
})
|
|
218
329
|
|
|
219
|
-
it('
|
|
330
|
+
it('oculta Nuevo rol / Editar / Eliminar cuando no hay mutators de rol', async () => {
|
|
220
331
|
const props = makeProps()
|
|
221
332
|
render(<PermissionsManager {...props} />)
|
|
222
|
-
await
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
})
|
|
227
|
-
// Solo el grupo con match permanece.
|
|
228
|
-
expect(screen.getByText('Sesiones POS')).toBeTruthy()
|
|
229
|
-
expect(screen.queryByText('Usuarios')).toBeNull()
|
|
333
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
334
|
+
expect(screen.queryByRole('button', { name: /Nuevo rol/ })).toBeNull()
|
|
335
|
+
expect(screen.queryByRole('button', { name: 'Editar rol' })).toBeNull()
|
|
336
|
+
expect(screen.queryByRole('button', { name: 'Eliminar rol' })).toBeNull()
|
|
230
337
|
})
|
|
231
338
|
|
|
232
339
|
it('selector de rol limpio: edit/delete inline, sin chip removible', async () => {
|
|
233
|
-
const props = makeProps({
|
|
340
|
+
const props = makeProps(grouped, {
|
|
234
341
|
updateRole: vi.fn(async () => {}),
|
|
235
342
|
deleteRole: vi.fn(async () => {}),
|
|
236
343
|
})
|
|
237
344
|
render(<PermissionsManager {...props} />)
|
|
238
|
-
await
|
|
239
|
-
// No existe el botón de quitar rol del chip antiguo.
|
|
345
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
240
346
|
expect(screen.queryByRole('button', { name: 'Quitar rol seleccionado' })).toBeNull()
|
|
241
|
-
// Iconos inline presentes.
|
|
242
347
|
expect(screen.getByRole('button', { name: 'Editar rol' })).toBeTruthy()
|
|
243
348
|
expect(screen.getByRole('button', { name: 'Eliminar rol' })).toBeTruthy()
|
|
244
349
|
})
|
|
245
350
|
|
|
246
351
|
it('muestra los CRUD de rol cuando los mutators existen', async () => {
|
|
247
|
-
const props = makeProps({
|
|
352
|
+
const props = makeProps(grouped, {
|
|
248
353
|
createRole: vi.fn(async () => {}),
|
|
249
354
|
updateRole: vi.fn(async () => {}),
|
|
250
355
|
deleteRole: vi.fn(async () => {}),
|
|
251
356
|
})
|
|
252
357
|
render(<PermissionsManager {...props} />)
|
|
253
|
-
await
|
|
358
|
+
await waitFor(() => expect(moduleTrigger().textContent).toMatch(/Usuarios/))
|
|
254
359
|
expect(screen.getByRole('button', { name: /Nuevo rol/ })).toBeTruthy()
|
|
255
360
|
expect(screen.getByRole('button', { name: 'Editar rol' })).toBeTruthy()
|
|
256
361
|
expect(screen.getByRole('button', { name: 'Eliminar rol' })).toBeTruthy()
|
|
257
362
|
})
|
|
258
363
|
})
|
|
364
|
+
|
|
365
|
+
describe('PermissionsManager (retrocompat shape viejo {modules})', () => {
|
|
366
|
+
it('renderiza el shape flat legacy sin romper, agrupado por addon', async () => {
|
|
367
|
+
const props = makeProps(legacy)
|
|
368
|
+
render(<PermissionsManager {...props} />)
|
|
369
|
+
|
|
370
|
+
// Auto-selección del primer módulo legacy (pos_orders → grupo "Punto de venta").
|
|
371
|
+
expect(await screen.findByText('Pagar')).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()
|
|
376
|
+
// El grupo Sistema (users sin addon) también.
|
|
377
|
+
expect(screen.getByText('Sistema')).toBeTruthy()
|
|
378
|
+
expect(screen.getByRole('option', { name: /Usuarios/ })).toBeTruthy()
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('legacy: click + guardar produce capabilities correctas', async () => {
|
|
382
|
+
const props = makeProps(legacy)
|
|
383
|
+
render(<PermissionsManager {...props} />)
|
|
384
|
+
await screen.findByText('Pagar')
|
|
385
|
+
|
|
386
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Pagar/ }))
|
|
387
|
+
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
388
|
+
await waitFor(() =>
|
|
389
|
+
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
390
|
+
'pos_orders.index',
|
|
391
|
+
'pos_orders.pagar',
|
|
392
|
+
]),
|
|
393
|
+
)
|
|
394
|
+
})
|
|
395
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -46,8 +46,14 @@ export {
|
|
|
46
46
|
grantedCountForModule,
|
|
47
47
|
capabilitySetsEqual,
|
|
48
48
|
defaultActionIcon,
|
|
49
|
+
normalizeCatalogGroups,
|
|
50
|
+
flattenGroups,
|
|
51
|
+
filterModuleGroups,
|
|
49
52
|
type PermissionsManagerProps,
|
|
50
53
|
type PermissionsCatalog,
|
|
54
|
+
type GroupedPermissionsCatalog,
|
|
55
|
+
type FlatPermissionsCatalog,
|
|
56
|
+
type ModuleGroup,
|
|
51
57
|
type PermissionModuleDef,
|
|
52
58
|
type PermissionActionDef,
|
|
53
59
|
type GeneralPermissionDef,
|