@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
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
// Transport-agnostic: every read/write arrives via props (loaders/mutators),
|
|
4
4
|
// so each host wires them to its own api client (ops → /api/permissions/*).
|
|
5
5
|
// The capability universe (modules × actions + general flags) is derived from
|
|
6
|
-
// the installed manifests server-side; this
|
|
6
|
+
// the installed manifests + the real sidebar nav server/host-side; this
|
|
7
|
+
// component only renders it.
|
|
7
8
|
//
|
|
8
|
-
// Layout
|
|
9
|
+
// Layout — a *flat list* that mirrors the app sidebar (NO accordions/folders):
|
|
9
10
|
// header — title + "Nuevo rol" (primary) + "Guardar permisos" (green).
|
|
10
11
|
// left — Card "Rol": clean role combobox with inline Editar/Eliminar
|
|
11
12
|
// icons (no removable chip) + "Permisos Generales" flags.
|
|
12
|
-
// — Card "Módulos": a searchable
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// — Card "Módulos": a searchable flat list. Each group renders a
|
|
14
|
+
// non-collapsible grey header (uppercase tracking, like "Módulos"
|
|
15
|
+
// / "Sistema" in the sidebar) followed by its modules as clickable
|
|
16
|
+
// rows (icon + label + granted-count badge). Clicking a row selects
|
|
17
|
+
// that module and reveals its action grid on the right.
|
|
16
18
|
// right — Card "Acciones permitidas": granted counter N/M, mark-all /
|
|
17
19
|
// clear, checkbox grid (icon + label per action). Clear empty
|
|
18
20
|
// states for "pick a role" / "pick a module" / loading.
|
|
@@ -24,11 +26,9 @@
|
|
|
24
26
|
import * as React from 'react'
|
|
25
27
|
import {
|
|
26
28
|
Check,
|
|
27
|
-
ChevronRight,
|
|
28
29
|
ChevronsUpDown,
|
|
29
30
|
CheckCheck,
|
|
30
31
|
Eraser,
|
|
31
|
-
Folder,
|
|
32
32
|
Pencil,
|
|
33
33
|
Plus,
|
|
34
34
|
Save,
|
|
@@ -55,9 +55,6 @@ import {
|
|
|
55
55
|
CardHeader,
|
|
56
56
|
CardTitle,
|
|
57
57
|
Checkbox,
|
|
58
|
-
Collapsible,
|
|
59
|
-
CollapsibleContent,
|
|
60
|
-
CollapsibleTrigger,
|
|
61
58
|
Command,
|
|
62
59
|
CommandEmpty,
|
|
63
60
|
CommandGroup,
|
|
@@ -80,34 +77,52 @@ import {
|
|
|
80
77
|
import { DynamicIcon } from './dynamic-icon'
|
|
81
78
|
|
|
82
79
|
// ---------------------------------------------------------------------------
|
|
83
|
-
// Types (mirror of `GET /api/permissions/modules`
|
|
80
|
+
// Types (mirror of `GET /api/permissions/modules` + the host sidebar nav)
|
|
84
81
|
// ---------------------------------------------------------------------------
|
|
85
82
|
|
|
86
83
|
export interface PermissionActionDef {
|
|
87
|
-
/** Canonical action key (`index`, `create`, …,
|
|
84
|
+
/** Canonical action key (`index`, `create`, …, a custom `pagar`, or `access`). */
|
|
88
85
|
key: string
|
|
89
|
-
/** Localized label ("Listar", "Pagar"). */
|
|
86
|
+
/** Localized label ("Listar", "Pagar", "Acceder"). */
|
|
90
87
|
label: string
|
|
91
88
|
/** Lucide icon name from the manifest action (optional). */
|
|
92
89
|
icon?: string
|
|
93
|
-
/**
|
|
94
|
-
|
|
90
|
+
/**
|
|
91
|
+
* `crud` for the derived CRUD set, `custom` for manifest actions,
|
|
92
|
+
* `screen` for the single `access` action of a non-model screen.
|
|
93
|
+
*/
|
|
94
|
+
kind?: 'crud' | 'custom' | 'screen' | string
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
export interface PermissionModuleDef {
|
|
98
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Module key.
|
|
100
|
+
* - model: lowercase model table (`pos_orders`).
|
|
101
|
+
* - screen: `screen.<navKey>` (the host prefixes it).
|
|
102
|
+
*/
|
|
99
103
|
key: string
|
|
100
|
-
/** Localized module label ("Pedidos POS"). */
|
|
104
|
+
/** Localized module label ("Pedidos POS", "Terminal"). */
|
|
101
105
|
label: string
|
|
102
106
|
/** Module icon (lucide name) — mirrors the sidebar entry. */
|
|
103
107
|
icon?: string
|
|
104
|
-
/**
|
|
108
|
+
/** Whether this entry is a data model or a non-model screen. */
|
|
109
|
+
kind?: 'model' | 'screen'
|
|
110
|
+
/** Owning addon key (`pos`) — legacy shape only, used for grouping. */
|
|
105
111
|
addon_key?: string
|
|
106
|
-
/** Localized addon label ("Punto de venta") —
|
|
112
|
+
/** Localized addon label ("Punto de venta") — legacy shape only. */
|
|
107
113
|
addon_label?: string
|
|
108
114
|
actions: PermissionActionDef[]
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
/**
|
|
118
|
+
* A sidebar-style group: a grey (non-collapsible) header + its modules.
|
|
119
|
+
* `title === ''` → no header (e.g. core/infra modules).
|
|
120
|
+
*/
|
|
121
|
+
export interface ModuleGroup {
|
|
122
|
+
title: string
|
|
123
|
+
modules: PermissionModuleDef[]
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
export interface GeneralPermissionDef {
|
|
112
127
|
/** Full capability key (`general.work_after_hours`). */
|
|
113
128
|
key: string
|
|
@@ -115,11 +130,27 @@ export interface GeneralPermissionDef {
|
|
|
115
130
|
description?: string
|
|
116
131
|
}
|
|
117
132
|
|
|
118
|
-
|
|
133
|
+
/**
|
|
134
|
+
* What `loadModules()` may return.
|
|
135
|
+
*
|
|
136
|
+
* New (preferred) shape — pre-grouped flat list, mirrors the host sidebar:
|
|
137
|
+
* { groups: ModuleGroup[], general }
|
|
138
|
+
*
|
|
139
|
+
* Legacy shape (still accepted, wrapped into a single untitled group) —
|
|
140
|
+
* { modules: PermissionModuleDef[], general }
|
|
141
|
+
*/
|
|
142
|
+
export interface GroupedPermissionsCatalog {
|
|
143
|
+
groups: ModuleGroup[]
|
|
144
|
+
general: GeneralPermissionDef[]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface FlatPermissionsCatalog {
|
|
119
148
|
modules: PermissionModuleDef[]
|
|
120
149
|
general: GeneralPermissionDef[]
|
|
121
150
|
}
|
|
122
151
|
|
|
152
|
+
export type PermissionsCatalog = GroupedPermissionsCatalog | FlatPermissionsCatalog
|
|
153
|
+
|
|
123
154
|
export interface RoleDef {
|
|
124
155
|
id: string
|
|
125
156
|
/** Stable role key ("cashier"). */
|
|
@@ -137,7 +168,7 @@ export interface RoleInput {
|
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
export interface PermissionsManagerProps {
|
|
140
|
-
/** Loads the module×action universe + general flags. */
|
|
171
|
+
/** Loads the module×action universe + general flags (grouped or flat). */
|
|
141
172
|
loadModules: () => Promise<PermissionsCatalog>
|
|
142
173
|
/** Loads every assignable role. */
|
|
143
174
|
loadRoles: () => Promise<RoleDef[]>
|
|
@@ -197,19 +228,23 @@ export function defaultActionIcon(actionKey: string, kind?: string): string {
|
|
|
197
228
|
return 'Download'
|
|
198
229
|
case 'import':
|
|
199
230
|
return 'Upload'
|
|
231
|
+
case 'access':
|
|
232
|
+
return 'Eye'
|
|
200
233
|
default:
|
|
201
|
-
|
|
234
|
+
if (kind === 'crud') return 'List'
|
|
235
|
+
if (kind === 'screen') return 'Eye'
|
|
236
|
+
return 'Zap'
|
|
202
237
|
}
|
|
203
238
|
}
|
|
204
239
|
|
|
205
|
-
/** Group label fallback when a module has no addon ("Sistema" = core
|
|
240
|
+
/** Group label fallback when a legacy module has no addon ("Sistema" = core). */
|
|
206
241
|
const SYSTEM_GROUP = 'Sistema'
|
|
207
242
|
|
|
208
|
-
function
|
|
243
|
+
function legacyGroupLabel(mod: PermissionModuleDef): string {
|
|
209
244
|
return mod.addon_label || mod.addon_key || SYSTEM_GROUP
|
|
210
245
|
}
|
|
211
246
|
|
|
212
|
-
/** Accent-insensitive, lowercase fold for
|
|
247
|
+
/** Accent-insensitive, lowercase fold for search. */
|
|
213
248
|
function fold(s: string): string {
|
|
214
249
|
return s
|
|
215
250
|
.normalize('NFD')
|
|
@@ -236,39 +271,59 @@ const ROLE_COLORS = [
|
|
|
236
271
|
'#6b7280',
|
|
237
272
|
]
|
|
238
273
|
|
|
239
|
-
/**
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
274
|
+
/**
|
|
275
|
+
* Normalize whatever `loadModules` returned into the canonical grouped shape.
|
|
276
|
+
*
|
|
277
|
+
* - New shape (`{ groups }`): passed through (modules default to kind:'model').
|
|
278
|
+
* - Legacy flat shape (`{ modules }`): grouped by `addon_label`/`addon_key`
|
|
279
|
+
* (falling back to "Sistema") so old hosts keep their familiar buckets,
|
|
280
|
+
* every module defaulting to kind:'model'. The grey headers still render,
|
|
281
|
+
* just derived from the addon instead of the sidebar group.
|
|
282
|
+
*/
|
|
283
|
+
export function normalizeCatalogGroups(catalog: PermissionsCatalog): ModuleGroup[] {
|
|
284
|
+
const withKind = (m: PermissionModuleDef): PermissionModuleDef => ({
|
|
285
|
+
...m,
|
|
286
|
+
kind: m.kind ?? 'model',
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
if ('groups' in catalog && Array.isArray(catalog.groups)) {
|
|
290
|
+
return catalog.groups.map((g) => ({
|
|
291
|
+
title: g.title ?? '',
|
|
292
|
+
modules: g.modules.map(withKind),
|
|
293
|
+
}))
|
|
294
|
+
}
|
|
244
295
|
|
|
245
|
-
|
|
296
|
+
const modules = ('modules' in catalog && catalog.modules) || []
|
|
246
297
|
const order: string[] = []
|
|
247
298
|
const byGroup = new Map<string, PermissionModuleDef[]>()
|
|
248
|
-
for (const
|
|
249
|
-
const
|
|
299
|
+
for (const raw of modules) {
|
|
300
|
+
const mod = withKind(raw)
|
|
301
|
+
const g = legacyGroupLabel(mod)
|
|
250
302
|
if (!byGroup.has(g)) {
|
|
251
303
|
byGroup.set(g, [])
|
|
252
304
|
order.push(g)
|
|
253
305
|
}
|
|
254
306
|
byGroup.get(g)!.push(mod)
|
|
255
307
|
}
|
|
256
|
-
return order.map((
|
|
308
|
+
return order.map((title) => ({ title, modules: byGroup.get(title)! }))
|
|
257
309
|
}
|
|
258
310
|
|
|
259
|
-
/**
|
|
311
|
+
/** Flat list of every module across groups, in render order. */
|
|
312
|
+
export function flattenGroups(groups: ModuleGroup[]): PermissionModuleDef[] {
|
|
313
|
+
return groups.flatMap((g) => g.modules)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Filter the grouped flat list by a folded query against module + group titles. */
|
|
260
317
|
export function filterModuleGroups(groups: ModuleGroup[], query: string): ModuleGroup[] {
|
|
261
318
|
const q = fold(query).trim()
|
|
262
319
|
if (!q) return groups
|
|
263
320
|
const out: ModuleGroup[] = []
|
|
264
321
|
for (const g of groups) {
|
|
265
|
-
const groupMatches = fold(g.
|
|
322
|
+
const groupMatches = g.title.length > 0 && fold(g.title).includes(q)
|
|
266
323
|
const mods = groupMatches
|
|
267
324
|
? g.modules
|
|
268
|
-
: g.modules.filter(
|
|
269
|
-
|
|
270
|
-
)
|
|
271
|
-
if (mods.length) out.push({ label: g.label, modules: mods })
|
|
325
|
+
: g.modules.filter((m) => fold(m.label).includes(q) || fold(m.key).includes(q))
|
|
326
|
+
if (mods.length) out.push({ title: g.title, modules: mods })
|
|
272
327
|
}
|
|
273
328
|
return out
|
|
274
329
|
}
|
|
@@ -335,8 +390,8 @@ function CapabilityCheck({
|
|
|
335
390
|
)
|
|
336
391
|
}
|
|
337
392
|
|
|
338
|
-
/** One module row
|
|
339
|
-
function
|
|
393
|
+
/** One clickable module row in the flat list (mirrors a sidebar item). */
|
|
394
|
+
function ModuleRow({
|
|
340
395
|
module,
|
|
341
396
|
active,
|
|
342
397
|
granted,
|
|
@@ -362,7 +417,7 @@ function ModuleTreeItem({
|
|
|
362
417
|
)}
|
|
363
418
|
>
|
|
364
419
|
<DynamicIcon
|
|
365
|
-
name={module.icon || 'Square'}
|
|
420
|
+
name={module.icon || (module.kind === 'screen' ? 'Eye' : 'Square')}
|
|
366
421
|
className={cn('h-4 w-4 shrink-0', active ? 'text-primary' : 'text-muted-foreground')}
|
|
367
422
|
/>
|
|
368
423
|
<span className="min-w-0 flex-1 truncate">{module.label}</span>
|
|
@@ -393,7 +448,8 @@ export function PermissionsManager({
|
|
|
393
448
|
title = 'Permisos y Roles',
|
|
394
449
|
className,
|
|
395
450
|
}: PermissionsManagerProps) {
|
|
396
|
-
const [
|
|
451
|
+
const [groups, setGroups] = React.useState<ModuleGroup[] | null>(null)
|
|
452
|
+
const [general, setGeneral] = React.useState<GeneralPermissionDef[] | null>(null)
|
|
397
453
|
const [roles, setRoles] = React.useState<RoleDef[] | null>(null)
|
|
398
454
|
const [loadError, setLoadError] = React.useState(false)
|
|
399
455
|
|
|
@@ -408,8 +464,6 @@ export function PermissionsManager({
|
|
|
408
464
|
|
|
409
465
|
const [roleOpen, setRoleOpen] = React.useState(false)
|
|
410
466
|
const [moduleQuery, setModuleQuery] = React.useState('')
|
|
411
|
-
// Groups the user explicitly collapsed (default: every group open).
|
|
412
|
-
const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set())
|
|
413
467
|
|
|
414
468
|
// Pending role switch while there are unsaved changes.
|
|
415
469
|
const [pendingRoleId, setPendingRoleId] = React.useState<string | null>(null)
|
|
@@ -424,7 +478,9 @@ export function PermissionsManager({
|
|
|
424
478
|
const [deleteOpen, setDeleteOpen] = React.useState(false)
|
|
425
479
|
const [deleting, setDeleting] = React.useState(false)
|
|
426
480
|
|
|
427
|
-
const loading =
|
|
481
|
+
const loading = groups === null || roles === null
|
|
482
|
+
|
|
483
|
+
const allModules = React.useMemo(() => (groups ? flattenGroups(groups) : []), [groups])
|
|
428
484
|
|
|
429
485
|
// ---- initial load: catalog + roles in parallel -------------------------
|
|
430
486
|
React.useEffect(() => {
|
|
@@ -432,10 +488,14 @@ export function PermissionsManager({
|
|
|
432
488
|
Promise.all([loadModules(), loadRoles()])
|
|
433
489
|
.then(([cat, rs]) => {
|
|
434
490
|
if (cancelled) return
|
|
435
|
-
|
|
491
|
+
const grouped = normalizeCatalogGroups(cat)
|
|
492
|
+
setGroups(grouped)
|
|
493
|
+
setGeneral(cat.general ?? [])
|
|
436
494
|
setRoles(rs)
|
|
437
495
|
setActiveRoleId((prev) => prev ?? rs[0]?.id ?? null)
|
|
438
|
-
setActiveModuleKey(
|
|
496
|
+
setActiveModuleKey(
|
|
497
|
+
(prev) => prev ?? flattenGroups(grouped)[0]?.key ?? null,
|
|
498
|
+
)
|
|
439
499
|
})
|
|
440
500
|
.catch(() => {
|
|
441
501
|
if (!cancelled) setLoadError(true)
|
|
@@ -481,19 +541,17 @@ export function PermissionsManager({
|
|
|
481
541
|
[roles, activeRoleId],
|
|
482
542
|
)
|
|
483
543
|
const activeModule = React.useMemo(
|
|
484
|
-
() =>
|
|
485
|
-
[
|
|
544
|
+
() => allModules.find((m) => m.key === activeModuleKey) ?? null,
|
|
545
|
+
[allModules, activeModuleKey],
|
|
486
546
|
)
|
|
487
547
|
|
|
488
548
|
const dirty = baseline !== null && draft !== null && !capabilitySetsEqual(baseline, draft)
|
|
489
549
|
|
|
490
|
-
//
|
|
491
|
-
const allGroups = React.useMemo(() => groupModules(catalog?.modules ?? []), [catalog])
|
|
550
|
+
// Flat module list, optionally filtered by the search.
|
|
492
551
|
const visibleGroups = React.useMemo(
|
|
493
|
-
() => filterModuleGroups(
|
|
494
|
-
[
|
|
552
|
+
() => filterModuleGroups(groups ?? [], moduleQuery),
|
|
553
|
+
[groups, moduleQuery],
|
|
495
554
|
)
|
|
496
|
-
const searching = moduleQuery.trim().length > 0
|
|
497
555
|
|
|
498
556
|
// ---- capability edits ---------------------------------------------------
|
|
499
557
|
const toggleCapability = React.useCallback((cap: string) => {
|
|
@@ -544,14 +602,6 @@ export function PermissionsManager({
|
|
|
544
602
|
else setActiveRoleId(roleId)
|
|
545
603
|
}
|
|
546
604
|
|
|
547
|
-
const toggleGroup = (label: string) =>
|
|
548
|
-
setCollapsedGroups((prev) => {
|
|
549
|
-
const next = new Set(prev)
|
|
550
|
-
if (next.has(label)) next.delete(label)
|
|
551
|
-
else next.add(label)
|
|
552
|
-
return next
|
|
553
|
-
})
|
|
554
|
-
|
|
555
605
|
// ---- role CRUD -----------------------------------------------------------
|
|
556
606
|
const refreshRoles = async (selectId?: string | null) => {
|
|
557
607
|
const rs = await loadRoles()
|
|
@@ -631,6 +681,11 @@ export function PermissionsManager({
|
|
|
631
681
|
const moduleGranted = activeModule && draft ? grantedCountForModule(draft, activeModule) : 0
|
|
632
682
|
const moduleTotal = activeModule?.actions.length ?? 0
|
|
633
683
|
const checksDisabled = !activeRole || !draft || loadingPerms || saving
|
|
684
|
+
const activeModuleGroupTitle = React.useMemo(() => {
|
|
685
|
+
if (!activeModule || !groups) return ''
|
|
686
|
+
const g = groups.find((grp) => grp.modules.some((m) => m.key === activeModule.key))
|
|
687
|
+
return g?.title ?? ''
|
|
688
|
+
}, [activeModule, groups])
|
|
634
689
|
|
|
635
690
|
// ---- render --------------------------------------------------------------
|
|
636
691
|
if (loadError) {
|
|
@@ -809,13 +864,13 @@ export function PermissionsManager({
|
|
|
809
864
|
)}
|
|
810
865
|
</div>
|
|
811
866
|
|
|
812
|
-
{(
|
|
867
|
+
{(general?.length ?? 0) > 0 && (
|
|
813
868
|
<>
|
|
814
869
|
<Separator />
|
|
815
870
|
<div>
|
|
816
871
|
<h3 className="mb-2 text-sm font-semibold">Permisos Generales</h3>
|
|
817
872
|
<div className="flex flex-col gap-2">
|
|
818
|
-
{
|
|
873
|
+
{general!.map((g) => (
|
|
819
874
|
<CapabilityCheck
|
|
820
875
|
key={g.key}
|
|
821
876
|
checked={draft?.has(g.key) ?? false}
|
|
@@ -832,7 +887,7 @@ export function PermissionsManager({
|
|
|
832
887
|
</CardContent>
|
|
833
888
|
</Card>
|
|
834
889
|
|
|
835
|
-
{/* Card: Módulos (
|
|
890
|
+
{/* Card: Módulos (flat list, mirrors the sidebar — no folders) */}
|
|
836
891
|
<Card>
|
|
837
892
|
<CardHeader>
|
|
838
893
|
<CardTitle className="text-base">Módulos</CardTitle>
|
|
@@ -855,72 +910,48 @@ export function PermissionsManager({
|
|
|
855
910
|
</div>
|
|
856
911
|
|
|
857
912
|
<div
|
|
858
|
-
role="
|
|
913
|
+
role="list"
|
|
859
914
|
aria-label="Módulos"
|
|
860
|
-
className="-mx-1 max-h-[460px] overflow-y-auto px-1"
|
|
915
|
+
className="-mx-1 flex max-h-[460px] flex-col gap-0.5 overflow-y-auto px-1"
|
|
861
916
|
>
|
|
862
917
|
{visibleGroups.length === 0 ? (
|
|
863
918
|
<p className="px-2 py-6 text-center text-sm text-muted-foreground">
|
|
864
919
|
Sin módulos.
|
|
865
920
|
</p>
|
|
866
921
|
) : (
|
|
867
|
-
visibleGroups.map((group) =>
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
{group.modules.map((mod) => (
|
|
901
|
-
<ModuleTreeItem
|
|
902
|
-
key={mod.key}
|
|
903
|
-
module={mod}
|
|
904
|
-
active={mod.key === activeModuleKey}
|
|
905
|
-
granted={
|
|
906
|
-
draft
|
|
907
|
-
? grantedCountForModule(
|
|
908
|
-
draft,
|
|
909
|
-
mod,
|
|
910
|
-
)
|
|
911
|
-
: 0
|
|
912
|
-
}
|
|
913
|
-
total={mod.actions.length}
|
|
914
|
-
onSelect={() =>
|
|
915
|
-
setActiveModuleKey(mod.key)
|
|
916
|
-
}
|
|
917
|
-
/>
|
|
918
|
-
))}
|
|
919
|
-
</div>
|
|
920
|
-
</CollapsibleContent>
|
|
921
|
-
</Collapsible>
|
|
922
|
-
)
|
|
923
|
-
})
|
|
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
|
|
948
|
+
}
|
|
949
|
+
total={mod.actions.length}
|
|
950
|
+
onSelect={() => setActiveModuleKey(mod.key)}
|
|
951
|
+
/>
|
|
952
|
+
))}
|
|
953
|
+
</div>
|
|
954
|
+
))
|
|
924
955
|
)}
|
|
925
956
|
</div>
|
|
926
957
|
</CardContent>
|
|
@@ -935,7 +966,10 @@ export function PermissionsManager({
|
|
|
935
966
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
936
967
|
{activeModule && (
|
|
937
968
|
<DynamicIcon
|
|
938
|
-
name={
|
|
969
|
+
name={
|
|
970
|
+
activeModule.icon ||
|
|
971
|
+
(activeModule.kind === 'screen' ? 'Eye' : 'Square')
|
|
972
|
+
}
|
|
939
973
|
className="h-4 w-4 shrink-0 text-primary"
|
|
940
974
|
/>
|
|
941
975
|
)}
|
|
@@ -945,7 +979,9 @@ export function PermissionsManager({
|
|
|
945
979
|
</CardTitle>
|
|
946
980
|
<CardDescription>
|
|
947
981
|
{activeModule
|
|
948
|
-
? `${
|
|
982
|
+
? `${
|
|
983
|
+
activeModuleGroupTitle || 'Sistema'
|
|
984
|
+
} · configura las acciones permitidas`
|
|
949
985
|
: 'Configura los permisos del módulo seleccionado.'}
|
|
950
986
|
</CardDescription>
|
|
951
987
|
</div>
|
|
@@ -986,7 +1022,7 @@ export function PermissionsManager({
|
|
|
986
1022
|
))}
|
|
987
1023
|
</div>
|
|
988
1024
|
) : !activeModule ? (
|
|
989
|
-
<EmptyHint text="Selecciona un módulo
|
|
1025
|
+
<EmptyHint text="Selecciona un módulo de la lista para ver sus acciones." />
|
|
990
1026
|
) : (
|
|
991
1027
|
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
|
|
992
1028
|
{activeModule.actions.map((action) => {
|