@asteby/metacore-runtime-react 18.14.0 → 18.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/permissions-manager.d.ts +58 -11
- package/dist/permissions-manager.d.ts.map +1 -1
- package/dist/permissions-manager.js +146 -47
- package/package.json +1 -1
- package/src/__tests__/permissions-manager.test.tsx +245 -35
- package/src/index.ts +6 -0
- package/src/permissions-manager.tsx +417 -222
|
@@ -10,36 +10,94 @@ import {
|
|
|
10
10
|
moduleCapabilities,
|
|
11
11
|
grantedCountForModule,
|
|
12
12
|
capabilitySetsEqual,
|
|
13
|
+
normalizeCatalogGroups,
|
|
14
|
+
flattenGroups,
|
|
15
|
+
filterModuleGroups,
|
|
16
|
+
defaultActionIcon,
|
|
13
17
|
type PermissionsCatalog,
|
|
18
|
+
type GroupedPermissionsCatalog,
|
|
19
|
+
type FlatPermissionsCatalog,
|
|
14
20
|
type RoleDef,
|
|
15
21
|
} from '../permissions-manager'
|
|
16
22
|
|
|
17
|
-
|
|
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 = {
|
|
18
73
|
modules: [
|
|
19
74
|
{
|
|
20
75
|
key: 'pos_orders',
|
|
21
76
|
label: 'Pedidos POS',
|
|
77
|
+
icon: 'ShoppingCart',
|
|
22
78
|
addon_key: 'pos',
|
|
23
79
|
addon_label: 'Punto de venta',
|
|
24
80
|
actions: [
|
|
25
81
|
{ key: 'index', label: 'Listar', icon: 'List', kind: 'crud' },
|
|
26
|
-
{ key: 'create', label: 'Crear', icon: 'Plus', kind: 'crud' },
|
|
27
82
|
{ key: 'pagar', label: 'Pagar', icon: 'CreditCard', kind: 'custom' },
|
|
28
83
|
],
|
|
29
84
|
},
|
|
30
|
-
],
|
|
31
|
-
general: [
|
|
32
85
|
{
|
|
33
|
-
key: '
|
|
34
|
-
label: '
|
|
35
|
-
|
|
86
|
+
key: 'users',
|
|
87
|
+
label: 'Usuarios',
|
|
88
|
+
icon: 'Users',
|
|
89
|
+
actions: [{ key: 'index', label: 'Listar', icon: 'List', kind: 'crud' }],
|
|
36
90
|
},
|
|
37
91
|
],
|
|
92
|
+
general: [],
|
|
38
93
|
}
|
|
39
94
|
|
|
40
95
|
const roles: RoleDef[] = [{ id: 'r1', name: 'cashier', label: 'Cajero', color: '#22c55e' }]
|
|
41
96
|
|
|
42
|
-
function makeProps(
|
|
97
|
+
function makeProps(
|
|
98
|
+
catalog: PermissionsCatalog = grouped,
|
|
99
|
+
overrides: Partial<Parameters<typeof PermissionsManager>[0]> = {},
|
|
100
|
+
) {
|
|
43
101
|
return {
|
|
44
102
|
loadModules: vi.fn(async () => catalog),
|
|
45
103
|
loadRoles: vi.fn(async () => roles),
|
|
@@ -54,8 +112,14 @@ describe('helpers puros', () => {
|
|
|
54
112
|
expect(moduleActionCapability('Pos_Orders', 'pagar')).toBe('pos_orders.pagar')
|
|
55
113
|
})
|
|
56
114
|
|
|
115
|
+
it('screen capability = screen.<navKey>.access', () => {
|
|
116
|
+
expect(moduleActionCapability('screen.pos_terminal', 'access')).toBe(
|
|
117
|
+
'screen.pos_terminal.access',
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
57
121
|
it('moduleCapabilities y grantedCountForModule', () => {
|
|
58
|
-
const mod =
|
|
122
|
+
const mod = grouped.groups[1].modules[0] // pos_orders
|
|
59
123
|
expect(moduleCapabilities(mod)).toEqual([
|
|
60
124
|
'pos_orders.index',
|
|
61
125
|
'pos_orders.create',
|
|
@@ -68,65 +132,155 @@ describe('helpers puros', () => {
|
|
|
68
132
|
expect(capabilitySetsEqual(new Set(['a', 'b']), new Set(['b', 'a']))).toBe(true)
|
|
69
133
|
expect(capabilitySetsEqual(new Set(['a']), new Set(['a', 'b']))).toBe(false)
|
|
70
134
|
})
|
|
135
|
+
|
|
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')
|
|
140
|
+
})
|
|
141
|
+
|
|
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
|
|
173
|
+
// Por nombre de módulo.
|
|
174
|
+
const byTerminal = filterModuleGroups(groups, 'terminal')
|
|
175
|
+
expect(byTerminal).toHaveLength(1)
|
|
176
|
+
expect(byTerminal[0].modules.map((m) => m.key)).toEqual(['screen.pos_terminal'])
|
|
177
|
+
// Por nombre de grupo trae todos sus módulos.
|
|
178
|
+
const byGroup = filterModuleGroups(groups, 'venta')
|
|
179
|
+
expect(byGroup[0].modules).toHaveLength(2)
|
|
180
|
+
// Query vacía = pasa todo.
|
|
181
|
+
expect(filterModuleGroups(groups, ' ')).toEqual(groups)
|
|
182
|
+
// Sin match = vacío.
|
|
183
|
+
expect(filterModuleGroups(groups, 'zzz')).toEqual([])
|
|
184
|
+
})
|
|
71
185
|
})
|
|
72
186
|
|
|
73
|
-
describe('PermissionsManager', () => {
|
|
74
|
-
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 () => {
|
|
75
189
|
const props = makeProps()
|
|
76
190
|
render(<PermissionsManager {...props} />)
|
|
77
191
|
|
|
78
|
-
//
|
|
79
|
-
expect(await screen.
|
|
80
|
-
expect(await screen.findByText('Pagar')).toBeTruthy()
|
|
81
|
-
expect(screen.getByText('1/3')).toBeTruthy()
|
|
192
|
+
// Primer módulo = "Usuarios" (grupo sin título, va primero).
|
|
193
|
+
expect(await screen.findAllByText('Usuarios')).toBeTruthy()
|
|
82
194
|
expect(props.loadRolePermissions).toHaveBeenCalledWith('r1')
|
|
83
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
|
+
|
|
84
202
|
// Generales presentes con descripción.
|
|
85
203
|
expect(screen.getByText('Permisos Generales')).toBeTruthy()
|
|
86
204
|
expect(screen.getByText('Trabajar fuera de horario')).toBeTruthy()
|
|
87
205
|
})
|
|
88
206
|
|
|
89
|
-
it('
|
|
207
|
+
it('CERO acordeones: el header de grupo es un heading, no un botón colapsable', async () => {
|
|
90
208
|
const props = makeProps()
|
|
91
209
|
render(<PermissionsManager {...props} />)
|
|
92
|
-
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
|
+
})
|
|
93
219
|
|
|
94
|
-
|
|
95
|
-
|
|
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')
|
|
96
224
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
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()
|
|
100
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/ }))
|
|
101
244
|
await waitFor(() =>
|
|
102
245
|
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
103
|
-
'general.work_after_hours',
|
|
104
246
|
'pos_orders.index',
|
|
105
|
-
'
|
|
247
|
+
'screen.pos_terminal.access',
|
|
106
248
|
]),
|
|
107
249
|
)
|
|
108
|
-
// Tras guardar, baseline = draft → dirty desaparece.
|
|
109
|
-
await waitFor(() => expect(screen.queryByText('Cambios sin guardar')).toBeNull())
|
|
110
250
|
})
|
|
111
251
|
|
|
112
|
-
it('
|
|
252
|
+
it('marcar una acción + un general y guardar llama sync con el set completo', async () => {
|
|
113
253
|
const props = makeProps()
|
|
114
254
|
render(<PermissionsManager {...props} />)
|
|
255
|
+
await screen.findAllByText('Usuarios')
|
|
256
|
+
|
|
257
|
+
fireEvent.click(screen.getByRole('button', { name: /Pedidos POS/ }))
|
|
115
258
|
await screen.findByText('Pagar')
|
|
259
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Pagar/ }))
|
|
260
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /Trabajar fuera de horario/ }))
|
|
116
261
|
|
|
117
|
-
|
|
262
|
+
expect(screen.getByText('Cambios sin guardar')).toBeTruthy()
|
|
118
263
|
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
119
|
-
|
|
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())
|
|
120
273
|
})
|
|
121
274
|
|
|
122
275
|
it('marcar todo / limpiar operan sobre el módulo activo', async () => {
|
|
123
276
|
const props = makeProps()
|
|
124
277
|
render(<PermissionsManager {...props} />)
|
|
278
|
+
await screen.findAllByText('Usuarios')
|
|
279
|
+
fireEvent.click(screen.getByRole('button', { name: /Pedidos POS/ }))
|
|
125
280
|
await screen.findByText('Pagar')
|
|
126
281
|
|
|
127
282
|
fireEvent.click(screen.getByRole('button', { name: /Marcar todo/ }))
|
|
128
|
-
expect(screen.
|
|
129
|
-
|
|
283
|
+
expect(screen.getAllByText('3/3').length).toBeGreaterThan(0)
|
|
130
284
|
fireEvent.click(screen.getByRole('button', { name: /Guardar permisos/ }))
|
|
131
285
|
await waitFor(() =>
|
|
132
286
|
expect(props.syncRolePermissions).toHaveBeenCalledWith('r1', [
|
|
@@ -143,30 +297,86 @@ describe('PermissionsManager', () => {
|
|
|
143
297
|
it('guardar deshabilitado sin cambios', async () => {
|
|
144
298
|
const props = makeProps()
|
|
145
299
|
render(<PermissionsManager {...props} />)
|
|
146
|
-
await screen.
|
|
300
|
+
await screen.findAllByText('Usuarios')
|
|
147
301
|
const save = screen.getByRole('button', { name: /Guardar permisos/ }) as HTMLButtonElement
|
|
148
302
|
expect(save.disabled).toBe(true)
|
|
149
303
|
})
|
|
150
304
|
|
|
305
|
+
it('la búsqueda filtra las filas de la lista plana', async () => {
|
|
306
|
+
const props = makeProps()
|
|
307
|
+
render(<PermissionsManager {...props} />)
|
|
308
|
+
await screen.findAllByText('Usuarios')
|
|
309
|
+
|
|
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()
|
|
317
|
+
})
|
|
318
|
+
|
|
151
319
|
it('oculta Nuevo rol / Editar / Eliminar cuando no hay mutators de rol', async () => {
|
|
152
320
|
const props = makeProps()
|
|
153
321
|
render(<PermissionsManager {...props} />)
|
|
154
|
-
await screen.
|
|
322
|
+
await screen.findAllByText('Usuarios')
|
|
155
323
|
expect(screen.queryByRole('button', { name: /Nuevo rol/ })).toBeNull()
|
|
156
324
|
expect(screen.queryByRole('button', { name: 'Editar rol' })).toBeNull()
|
|
157
325
|
expect(screen.queryByRole('button', { name: 'Eliminar rol' })).toBeNull()
|
|
158
326
|
})
|
|
159
327
|
|
|
328
|
+
it('selector de rol limpio: edit/delete inline, sin chip removible', async () => {
|
|
329
|
+
const props = makeProps(grouped, {
|
|
330
|
+
updateRole: vi.fn(async () => {}),
|
|
331
|
+
deleteRole: vi.fn(async () => {}),
|
|
332
|
+
})
|
|
333
|
+
render(<PermissionsManager {...props} />)
|
|
334
|
+
await screen.findAllByText('Usuarios')
|
|
335
|
+
expect(screen.queryByRole('button', { name: 'Quitar rol seleccionado' })).toBeNull()
|
|
336
|
+
expect(screen.getByRole('button', { name: 'Editar rol' })).toBeTruthy()
|
|
337
|
+
expect(screen.getByRole('button', { name: 'Eliminar rol' })).toBeTruthy()
|
|
338
|
+
})
|
|
339
|
+
|
|
160
340
|
it('muestra los CRUD de rol cuando los mutators existen', async () => {
|
|
161
|
-
const props = makeProps({
|
|
341
|
+
const props = makeProps(grouped, {
|
|
162
342
|
createRole: vi.fn(async () => {}),
|
|
163
343
|
updateRole: vi.fn(async () => {}),
|
|
164
344
|
deleteRole: vi.fn(async () => {}),
|
|
165
345
|
})
|
|
166
346
|
render(<PermissionsManager {...props} />)
|
|
167
|
-
await screen.
|
|
347
|
+
await screen.findAllByText('Usuarios')
|
|
168
348
|
expect(screen.getByRole('button', { name: /Nuevo rol/ })).toBeTruthy()
|
|
169
349
|
expect(screen.getByRole('button', { name: 'Editar rol' })).toBeTruthy()
|
|
170
350
|
expect(screen.getByRole('button', { name: 'Eliminar rol' })).toBeTruthy()
|
|
171
351
|
})
|
|
172
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,
|