@asteby/metacore-runtime-react 18.15.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 +11 -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 +78 -50
- package/package.json +1 -1
- package/src/__tests__/permissions-manager.test.tsx +215 -91
- package/src/index.ts +6 -0
- package/src/permissions-manager.tsx +166 -130
|
@@ -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,103 @@ describe('helpers puros', () => {
|
|
|
109
184
|
})
|
|
110
185
|
})
|
|
111
186
|
|
|
112
|
-
describe('PermissionsManager', () => {
|
|
113
|
-
it('renderiza catálogo
|
|
187
|
+
describe('PermissionsManager (lista plana, shape nuevo)', () => {
|
|
188
|
+
it('renderiza catálogo, auto-selecciona rol y primer módulo, contador N/M', async () => {
|
|
114
189
|
const props = makeProps()
|
|
115
190
|
render(<PermissionsManager {...props} />)
|
|
116
191
|
|
|
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)
|
|
192
|
+
// Primer módulo = "Usuarios" (grupo sin título, va primero).
|
|
193
|
+
expect(await screen.findAllByText('Usuarios')).toBeTruthy()
|
|
123
194
|
expect(props.loadRolePermissions).toHaveBeenCalledWith('r1')
|
|
124
195
|
|
|
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()
|
|
201
|
+
|
|
125
202
|
// Generales presentes con descripción.
|
|
126
203
|
expect(screen.getByText('Permisos Generales')).toBeTruthy()
|
|
127
204
|
expect(screen.getByText('Trabajar fuera de horario')).toBeTruthy()
|
|
128
205
|
})
|
|
129
206
|
|
|
130
|
-
it('
|
|
207
|
+
it('CERO acordeones: el header de grupo es un heading, no un botón colapsable', async () => {
|
|
131
208
|
const props = makeProps()
|
|
132
209
|
render(<PermissionsManager {...props} />)
|
|
133
|
-
await screen.
|
|
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).
|
|
217
|
+
expect(screen.queryByRole('button', { name: 'Punto de venta' })).toBeNull()
|
|
218
|
+
})
|
|
134
219
|
|
|
135
|
-
|
|
136
|
-
|
|
220
|
+
it('click directo en una fila selecciona el módulo y muestra su grid', async () => {
|
|
221
|
+
const props = makeProps()
|
|
222
|
+
render(<PermissionsManager {...props} />)
|
|
223
|
+
await screen.findAllByText('Usuarios')
|
|
137
224
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
225
|
+
// Selecciono "Pedidos POS" → su grid aparece a la derecha.
|
|
226
|
+
fireEvent.click(screen.getByRole('button', { name: /Pedidos POS/ }))
|
|
227
|
+
expect(await screen.findByText('Pagar')).toBeTruthy()
|
|
141
228
|
|
|
229
|
+
// Selecciono el screen "Terminal" → acción "Acceder".
|
|
230
|
+
fireEvent.click(screen.getByRole('button', { name: /Terminal/ }))
|
|
231
|
+
expect(await screen.findByText('Acceder')).toBeTruthy()
|
|
232
|
+
await waitFor(() => expect(screen.queryByText('Pagar')).toBeNull())
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('marcar el screen "Acceder" produce capability screen.<navKey>.access', async () => {
|
|
236
|
+
const props = makeProps()
|
|
237
|
+
render(<PermissionsManager {...props} />)
|
|
238
|
+
await screen.findAllByText('Usuarios')
|
|
239
|
+
|
|
240
|
+
fireEvent.click(screen.getByRole('button', { name: /Terminal/ }))
|
|
241
|
+
await screen.findByText('Acceder')
|
|
242
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Acceder/ }))
|
|
243
|
+
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
142
244
|
await waitFor(() =>
|
|
143
245
|
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
144
|
-
'general.work_after_hours',
|
|
145
246
|
'pos_orders.index',
|
|
146
|
-
'
|
|
247
|
+
'screen.pos_terminal.access',
|
|
147
248
|
]),
|
|
148
249
|
)
|
|
149
|
-
// Tras guardar, baseline = draft → dirty desaparece.
|
|
150
|
-
await waitFor(() => expect(screen.queryByText('Cambios sin guardar')).toBeNull())
|
|
151
250
|
})
|
|
152
251
|
|
|
153
|
-
it('
|
|
252
|
+
it('marcar una acción + un general y guardar llama sync con el set completo', async () => {
|
|
154
253
|
const props = makeProps()
|
|
155
254
|
render(<PermissionsManager {...props} />)
|
|
255
|
+
await screen.findAllByText('Usuarios')
|
|
256
|
+
|
|
257
|
+
fireEvent.click(screen.getByRole('button', { name: /Pedidos POS/ }))
|
|
156
258
|
await screen.findByText('Pagar')
|
|
259
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Pagar/ }))
|
|
260
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Trabajar fuera de horario/ }))
|
|
157
261
|
|
|
158
|
-
|
|
262
|
+
expect(screen.getByText('Cambios sin guardar')).toBeTruthy()
|
|
159
263
|
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
160
|
-
|
|
264
|
+
|
|
265
|
+
await waitFor(() =>
|
|
266
|
+
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
267
|
+
'general.work_after_hours',
|
|
268
|
+
'pos_orders.index',
|
|
269
|
+
'pos_orders.pagar',
|
|
270
|
+
]),
|
|
271
|
+
)
|
|
272
|
+
await waitFor(() => expect(screen.queryByText('Cambios sin guardar')).toBeNull())
|
|
161
273
|
})
|
|
162
274
|
|
|
163
275
|
it('marcar todo / limpiar operan sobre el módulo activo', async () => {
|
|
164
276
|
const props = makeProps()
|
|
165
277
|
render(<PermissionsManager {...props} />)
|
|
278
|
+
await screen.findAllByText('Usuarios')
|
|
279
|
+
fireEvent.click(screen.getByRole('button', { name: /Pedidos POS/ }))
|
|
166
280
|
await screen.findByText('Pagar')
|
|
167
281
|
|
|
168
282
|
fireEvent.click(screen.getByRole('button', { name: /Marcar todo/ }))
|
|
169
|
-
// 3/3 aparece en el panel y en el badge del árbol.
|
|
170
283
|
expect(screen.getAllByText('3/3').length).toBeGreaterThan(0)
|
|
171
|
-
|
|
172
284
|
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
173
285
|
await waitFor(() =>
|
|
174
286
|
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
@@ -185,74 +297,86 @@ describe('PermissionsManager', () => {
|
|
|
185
297
|
it('guardar deshabilitado sin cambios', async () => {
|
|
186
298
|
const props = makeProps()
|
|
187
299
|
render(<PermissionsManager {...props} />)
|
|
188
|
-
await screen.
|
|
300
|
+
await screen.findAllByText('Usuarios')
|
|
189
301
|
const save = screen.getByRole('button', { name: /Guardar permisos/ }) as HTMLButtonElement
|
|
190
302
|
expect(save.disabled).toBe(true)
|
|
191
303
|
})
|
|
192
304
|
|
|
193
|
-
it('
|
|
305
|
+
it('la búsqueda filtra las filas de la lista plana', async () => {
|
|
194
306
|
const props = makeProps()
|
|
195
307
|
render(<PermissionsManager {...props} />)
|
|
196
|
-
await screen.
|
|
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
|
-
})
|
|
201
|
-
|
|
202
|
-
it('renderiza el árbol agrupado y permite seleccionar un módulo de otro grupo', async () => {
|
|
203
|
-
const props = makeProps()
|
|
204
|
-
render(<PermissionsManager {...props} />)
|
|
205
|
-
await screen.findByText('Pagar')
|
|
308
|
+
await screen.findAllByText('Usuarios')
|
|
206
309
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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)
|
|
310
|
+
fireEvent.change(screen.getByLabelText('Buscar módulo'), {
|
|
311
|
+
target: { value: 'terminal' },
|
|
312
|
+
})
|
|
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()
|
|
217
317
|
})
|
|
218
318
|
|
|
219
|
-
it('
|
|
319
|
+
it('oculta Nuevo rol / Editar / Eliminar cuando no hay mutators de rol', async () => {
|
|
220
320
|
const props = makeProps()
|
|
221
321
|
render(<PermissionsManager {...props} />)
|
|
222
|
-
await screen.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
})
|
|
227
|
-
// Solo el grupo con match permanece.
|
|
228
|
-
expect(screen.getByText('Sesiones POS')).toBeTruthy()
|
|
229
|
-
expect(screen.queryByText('Usuarios')).toBeNull()
|
|
322
|
+
await screen.findAllByText('Usuarios')
|
|
323
|
+
expect(screen.queryByRole('button', { name: /Nuevo rol/ })).toBeNull()
|
|
324
|
+
expect(screen.queryByRole('button', { name: 'Editar rol' })).toBeNull()
|
|
325
|
+
expect(screen.queryByRole('button', { name: 'Eliminar rol' })).toBeNull()
|
|
230
326
|
})
|
|
231
327
|
|
|
232
328
|
it('selector de rol limpio: edit/delete inline, sin chip removible', async () => {
|
|
233
|
-
const props = makeProps({
|
|
329
|
+
const props = makeProps(grouped, {
|
|
234
330
|
updateRole: vi.fn(async () => {}),
|
|
235
331
|
deleteRole: vi.fn(async () => {}),
|
|
236
332
|
})
|
|
237
333
|
render(<PermissionsManager {...props} />)
|
|
238
|
-
await screen.
|
|
239
|
-
// No existe el botón de quitar rol del chip antiguo.
|
|
334
|
+
await screen.findAllByText('Usuarios')
|
|
240
335
|
expect(screen.queryByRole('button', { name: 'Quitar rol seleccionado' })).toBeNull()
|
|
241
|
-
// Iconos inline presentes.
|
|
242
336
|
expect(screen.getByRole('button', { name: 'Editar rol' })).toBeTruthy()
|
|
243
337
|
expect(screen.getByRole('button', { name: 'Eliminar rol' })).toBeTruthy()
|
|
244
338
|
})
|
|
245
339
|
|
|
246
340
|
it('muestra los CRUD de rol cuando los mutators existen', async () => {
|
|
247
|
-
const props = makeProps({
|
|
341
|
+
const props = makeProps(grouped, {
|
|
248
342
|
createRole: vi.fn(async () => {}),
|
|
249
343
|
updateRole: vi.fn(async () => {}),
|
|
250
344
|
deleteRole: vi.fn(async () => {}),
|
|
251
345
|
})
|
|
252
346
|
render(<PermissionsManager {...props} />)
|
|
253
|
-
await screen.
|
|
347
|
+
await screen.findAllByText('Usuarios')
|
|
254
348
|
expect(screen.getByRole('button', { name: /Nuevo rol/ })).toBeTruthy()
|
|
255
349
|
expect(screen.getByRole('button', { name: 'Editar rol' })).toBeTruthy()
|
|
256
350
|
expect(screen.getByRole('button', { name: 'Eliminar rol' })).toBeTruthy()
|
|
257
351
|
})
|
|
258
352
|
})
|
|
353
|
+
|
|
354
|
+
describe('PermissionsManager (retrocompat shape viejo {modules})', () => {
|
|
355
|
+
it('renderiza el shape flat legacy sin romper, agrupado por addon', async () => {
|
|
356
|
+
const props = makeProps(legacy)
|
|
357
|
+
render(<PermissionsManager {...props} />)
|
|
358
|
+
|
|
359
|
+
// Auto-selección del primer módulo legacy (pos_orders → grupo "Punto de venta").
|
|
360
|
+
expect(await screen.findByText('Pagar')).toBeTruthy()
|
|
361
|
+
// Header gris derivado del addon.
|
|
362
|
+
expect(screen.getByText('Punto de venta')).toBeTruthy()
|
|
363
|
+
// El grupo Sistema (users sin addon) también.
|
|
364
|
+
expect(screen.getByText('Sistema')).toBeTruthy()
|
|
365
|
+
expect(screen.getByRole('button', { name: /Usuarios/ })).toBeTruthy()
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('legacy: click + guardar produce capabilities correctas', async () => {
|
|
369
|
+
const props = makeProps(legacy)
|
|
370
|
+
render(<PermissionsManager {...props} />)
|
|
371
|
+
await screen.findByText('Pagar')
|
|
372
|
+
|
|
373
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Pagar/ }))
|
|
374
|
+
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
375
|
+
await waitFor(() =>
|
|
376
|
+
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
377
|
+
'pos_orders.index',
|
|
378
|
+
'pos_orders.pagar',
|
|
379
|
+
]),
|
|
380
|
+
)
|
|
381
|
+
})
|
|
382
|
+
})
|
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,
|