@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
|
@@ -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
|
+
})
|
|
244
288
|
|
|
245
|
-
|
|
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
|
+
}
|
|
295
|
+
|
|
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)! }))
|
|
309
|
+
}
|
|
310
|
+
|
|
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)
|
|
257
314
|
}
|
|
258
315
|
|
|
259
|
-
/** Filter the grouped
|
|
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
|
|
|
@@ -407,9 +463,7 @@ export function PermissionsManager({
|
|
|
407
463
|
const [saving, setSaving] = React.useState(false)
|
|
408
464
|
|
|
409
465
|
const [roleOpen, setRoleOpen] = React.useState(false)
|
|
410
|
-
const [
|
|
411
|
-
// Groups the user explicitly collapsed (default: every group open).
|
|
412
|
-
const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set())
|
|
466
|
+
const [moduleOpen, setModuleOpen] = React.useState(false)
|
|
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,20 +541,12 @@ 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
|
-
// Module tree: grouped by addon label, optionally filtered by the search.
|
|
491
|
-
const allGroups = React.useMemo(() => groupModules(catalog?.modules ?? []), [catalog])
|
|
492
|
-
const visibleGroups = React.useMemo(
|
|
493
|
-
() => filterModuleGroups(allGroups, moduleQuery),
|
|
494
|
-
[allGroups, moduleQuery],
|
|
495
|
-
)
|
|
496
|
-
const searching = moduleQuery.trim().length > 0
|
|
497
|
-
|
|
498
550
|
// ---- capability edits ---------------------------------------------------
|
|
499
551
|
const toggleCapability = React.useCallback((cap: string) => {
|
|
500
552
|
setDraft((prev) => {
|
|
@@ -544,14 +596,6 @@ export function PermissionsManager({
|
|
|
544
596
|
else setActiveRoleId(roleId)
|
|
545
597
|
}
|
|
546
598
|
|
|
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
599
|
// ---- role CRUD -----------------------------------------------------------
|
|
556
600
|
const refreshRoles = async (selectId?: string | null) => {
|
|
557
601
|
const rs = await loadRoles()
|
|
@@ -631,6 +675,11 @@ export function PermissionsManager({
|
|
|
631
675
|
const moduleGranted = activeModule && draft ? grantedCountForModule(draft, activeModule) : 0
|
|
632
676
|
const moduleTotal = activeModule?.actions.length ?? 0
|
|
633
677
|
const checksDisabled = !activeRole || !draft || loadingPerms || saving
|
|
678
|
+
const activeModuleGroupTitle = React.useMemo(() => {
|
|
679
|
+
if (!activeModule || !groups) return ''
|
|
680
|
+
const g = groups.find((grp) => grp.modules.some((m) => m.key === activeModule.key))
|
|
681
|
+
return g?.title ?? ''
|
|
682
|
+
}, [activeModule, groups])
|
|
634
683
|
|
|
635
684
|
// ---- render --------------------------------------------------------------
|
|
636
685
|
if (loadError) {
|
|
@@ -809,13 +858,13 @@ export function PermissionsManager({
|
|
|
809
858
|
)}
|
|
810
859
|
</div>
|
|
811
860
|
|
|
812
|
-
{(
|
|
861
|
+
{(general?.length ?? 0) > 0 && (
|
|
813
862
|
<>
|
|
814
863
|
<Separator />
|
|
815
864
|
<div>
|
|
816
865
|
<h3 className="mb-2 text-sm font-semibold">Permisos Generales</h3>
|
|
817
866
|
<div className="flex flex-col gap-2">
|
|
818
|
-
{
|
|
867
|
+
{general!.map((g) => (
|
|
819
868
|
<CapabilityCheck
|
|
820
869
|
key={g.key}
|
|
821
870
|
checked={draft?.has(g.key) ?? false}
|
|
@@ -832,97 +881,104 @@ export function PermissionsManager({
|
|
|
832
881
|
</CardContent>
|
|
833
882
|
</Card>
|
|
834
883
|
|
|
835
|
-
{/* Card:
|
|
884
|
+
{/* Card: Módulo — a grouped combobox, same pattern as the role
|
|
885
|
+
selector above (compact; the long flat list felt heavy). */}
|
|
836
886
|
<Card>
|
|
837
887
|
<CardHeader>
|
|
838
|
-
<CardTitle className="text-base">
|
|
888
|
+
<CardTitle className="text-base">Módulo</CardTitle>
|
|
839
889
|
<CardDescription>
|
|
840
890
|
Elige el módulo cuyas acciones quieres configurar.
|
|
841
891
|
</CardDescription>
|
|
842
892
|
</CardHeader>
|
|
843
|
-
<CardContent
|
|
844
|
-
<
|
|
845
|
-
<
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
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)
|
|
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')
|
|
910
|
+
}
|
|
911
|
+
className="h-4 w-4 shrink-0 opacity-70"
|
|
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')
|
|
916
951
|
}
|
|
952
|
+
className="mr-2 h-4 w-4 shrink-0 opacity-70"
|
|
917
953
|
/>
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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>
|
|
977
|
+
))}
|
|
978
|
+
</CommandList>
|
|
979
|
+
</Command>
|
|
980
|
+
</PopoverContent>
|
|
981
|
+
</Popover>
|
|
926
982
|
</CardContent>
|
|
927
983
|
</Card>
|
|
928
984
|
</div>
|
|
@@ -935,7 +991,10 @@ export function PermissionsManager({
|
|
|
935
991
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
936
992
|
{activeModule && (
|
|
937
993
|
<DynamicIcon
|
|
938
|
-
name={
|
|
994
|
+
name={
|
|
995
|
+
activeModule.icon ||
|
|
996
|
+
(activeModule.kind === 'screen' ? 'Eye' : 'Square')
|
|
997
|
+
}
|
|
939
998
|
className="h-4 w-4 shrink-0 text-primary"
|
|
940
999
|
/>
|
|
941
1000
|
)}
|
|
@@ -945,7 +1004,9 @@ export function PermissionsManager({
|
|
|
945
1004
|
</CardTitle>
|
|
946
1005
|
<CardDescription>
|
|
947
1006
|
{activeModule
|
|
948
|
-
? `${
|
|
1007
|
+
? `${
|
|
1008
|
+
activeModuleGroupTitle || 'Sistema'
|
|
1009
|
+
} · configura las acciones permitidas`
|
|
949
1010
|
: 'Configura los permisos del módulo seleccionado.'}
|
|
950
1011
|
</CardDescription>
|
|
951
1012
|
</div>
|
|
@@ -986,7 +1047,7 @@ export function PermissionsManager({
|
|
|
986
1047
|
))}
|
|
987
1048
|
</div>
|
|
988
1049
|
) : !activeModule ? (
|
|
989
|
-
<EmptyHint text="Selecciona un módulo
|
|
1050
|
+
<EmptyHint text="Selecciona un módulo de la lista para ver sus acciones." />
|
|
990
1051
|
) : (
|
|
991
1052
|
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
|
|
992
1053
|
{activeModule.actions.map((action) => {
|