@asteby/metacore-runtime-react 18.13.3 → 18.15.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 +35 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +8 -3
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +20 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/model-action-toolbar.d.ts.map +1 -1
- package/dist/model-action-toolbar.js +6 -1
- package/dist/permissions-context.d.ts +48 -0
- package/dist/permissions-context.d.ts.map +1 -0
- package/dist/permissions-context.js +124 -0
- package/dist/permissions-manager.d.ts +84 -0
- package/dist/permissions-manager.d.ts.map +1 -0
- package/dist/permissions-manager.js +429 -0
- package/package.json +11 -9
- package/src/__tests__/dynamic-table-permissions.test.tsx +147 -0
- package/src/__tests__/permissions-context.test.tsx +102 -0
- package/src/__tests__/permissions-manager.test.tsx +258 -0
- package/src/dynamic-crud-page.tsx +8 -3
- package/src/dynamic-table.tsx +22 -9
- package/src/index.ts +26 -0
- package/src/model-action-toolbar.tsx +9 -1
- package/src/permissions-context.tsx +158 -0
- package/src/permissions-manager.tsx +1143 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// permissions-context — runtime permission primitives for dynamic hosts.
|
|
2
|
+
//
|
|
3
|
+
// The host loads the session's capability set (e.g. ops `GET /permissions/me`)
|
|
4
|
+
// and mounts <PermissionsProvider permissions={caps} isAdmin={me.is_admin}>
|
|
5
|
+
// once at the root. Any SDK component (or host code) then calls `useCan()` to
|
|
6
|
+
// gate UI by capability:
|
|
7
|
+
//
|
|
8
|
+
// const can = useCan()
|
|
9
|
+
// can('pos_orders.create') // → boolean
|
|
10
|
+
//
|
|
11
|
+
// Capability format is the canonical `lowercase(<model_table>).<action_key>`
|
|
12
|
+
// derived from installed manifests (CRUD: index|create|update|delete|export|
|
|
13
|
+
// import; custom actions use their own key, e.g. `pos_orders.pagar`). General
|
|
14
|
+
// flags live under the `general` module (`general.work_after_hours`).
|
|
15
|
+
//
|
|
16
|
+
// Semantics:
|
|
17
|
+
// - isAdmin → every capability allowed (superrole bypass mirror).
|
|
18
|
+
// - the list contains the exact capability or the `*` wildcard → allowed.
|
|
19
|
+
// - NO provider mounted → `useCan()` returns an always-true function, so
|
|
20
|
+
// every existing host keeps its current behaviour (nothing is hidden).
|
|
21
|
+
// Deny-by-default only kicks in once the host opts in by mounting the
|
|
22
|
+
// provider; the backend enforcement remains the source of truth.
|
|
23
|
+
import React, { createContext, useContext, useMemo } from 'react'
|
|
24
|
+
import type { TableMetadata, ActionDefinition } from './types'
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Types
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Predicate answering "can the current user use this capability?". */
|
|
31
|
+
export type CanFn = (capability: string) => boolean
|
|
32
|
+
|
|
33
|
+
export interface PermissionsProviderProps {
|
|
34
|
+
/** Granted capabilities (`"pos_orders.create"`, `"general.x"`, or `"*"`). */
|
|
35
|
+
permissions: string[]
|
|
36
|
+
/** Superrole bypass — admins/owners see everything, no filtering at all. */
|
|
37
|
+
isAdmin: boolean
|
|
38
|
+
children: React.ReactNode
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Core
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds the capability predicate from a raw permission list. Pure — exported
|
|
47
|
+
* so hosts/tests can evaluate permissions outside React.
|
|
48
|
+
*/
|
|
49
|
+
export function makeCan(permissions: string[], isAdmin: boolean): CanFn {
|
|
50
|
+
if (isAdmin) return () => true
|
|
51
|
+
const set = new Set(permissions)
|
|
52
|
+
if (set.has('*')) return () => true
|
|
53
|
+
return (capability) => set.has(capability)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ALWAYS_ALLOW: CanFn = () => true
|
|
57
|
+
|
|
58
|
+
const PermissionsContext = createContext<CanFn | null>(null)
|
|
59
|
+
|
|
60
|
+
export function PermissionsProvider({ permissions, isAdmin, children }: PermissionsProviderProps) {
|
|
61
|
+
const can = useMemo(() => makeCan(permissions, isAdmin), [permissions, isAdmin])
|
|
62
|
+
return <PermissionsContext.Provider value={can}>{children}</PermissionsContext.Provider>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns the capability predicate. Without a <PermissionsProvider> ancestor
|
|
67
|
+
* it returns an always-true function — existing hosts that never mount the
|
|
68
|
+
* provider keep today's "everything visible" behaviour.
|
|
69
|
+
*/
|
|
70
|
+
export function useCan(): CanFn {
|
|
71
|
+
return useContext(PermissionsContext) ?? ALWAYS_ALLOW
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True when a <PermissionsProvider> is mounted above (permission gating active). */
|
|
75
|
+
export function usePermissionsActive(): boolean {
|
|
76
|
+
return useContext(PermissionsContext) !== null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Table-metadata gating (consumed by DynamicTable / DynamicCRUDPage /
|
|
81
|
+
// ModelActionToolbar — exported for hosts with bespoke tables)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Maps a row/table action key onto the capability action segment. The UI's
|
|
86
|
+
* legacy `view`/`edit` keys correspond to the kernel's `index`/`update`
|
|
87
|
+
* capabilities; everything else (delete, custom keys) maps verbatim.
|
|
88
|
+
*/
|
|
89
|
+
export function capabilityForActionKey(actionKey: string): string {
|
|
90
|
+
if (actionKey === 'view') return 'index'
|
|
91
|
+
if (actionKey === 'edit') return 'update'
|
|
92
|
+
return actionKey
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Canonical capability for an action on a model: `lowercase(model).<action>`. */
|
|
96
|
+
export function modelCapability(model: string, actionKey: string): string {
|
|
97
|
+
return `${model.toLowerCase()}.${capabilityForActionKey(actionKey)}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const DEFAULT_TRIO: { key: string; i18nKey: string; fallback: string; icon: string }[] = [
|
|
101
|
+
{ key: 'view', i18nKey: 'datatable.view', fallback: 'Ver', icon: 'Eye' },
|
|
102
|
+
{ key: 'edit', i18nKey: 'datatable.edit', fallback: 'Editar', icon: 'Pencil' },
|
|
103
|
+
{ key: 'delete', i18nKey: 'datatable.delete', fallback: 'Eliminar', icon: 'Trash2' },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Applies the capability predicate to a model's table metadata:
|
|
108
|
+
* - `canExport` / `canImport` are ANDed with `can(model.export|import)`.
|
|
109
|
+
* - explicit row/table actions are filtered by `can(model.<key>)` (with the
|
|
110
|
+
* view→index / edit→update mapping above).
|
|
111
|
+
* - when the metadata has NO explicit actions but `enableCRUDActions` is on,
|
|
112
|
+
* the implicit View/Edit/Delete trio is materialized here as explicit
|
|
113
|
+
* actions so individual entries can be dropped; `tx` resolves their labels
|
|
114
|
+
* (defaults to the Spanish fallbacks used by the column factory).
|
|
115
|
+
*
|
|
116
|
+
* Pure + idempotent. Callers should only invoke it when a provider is active
|
|
117
|
+
* (`usePermissionsActive()`), otherwise pass the metadata through untouched.
|
|
118
|
+
*/
|
|
119
|
+
export function gateTableMetadata(
|
|
120
|
+
metadata: TableMetadata,
|
|
121
|
+
model: string,
|
|
122
|
+
can: CanFn,
|
|
123
|
+
tx: (i18nKey: string, fallback: string) => string = (_k, fallback) => fallback,
|
|
124
|
+
): TableMetadata {
|
|
125
|
+
const allowed = (key: string) => can(modelCapability(model, key))
|
|
126
|
+
|
|
127
|
+
const explicit = metadata.actions ?? []
|
|
128
|
+
const hasExplicit = (metadata.hasActions ?? explicit.length > 0) && explicit.length > 0
|
|
129
|
+
const base: ActionDefinition[] = hasExplicit
|
|
130
|
+
? explicit
|
|
131
|
+
: metadata.enableCRUDActions
|
|
132
|
+
? DEFAULT_TRIO.map(
|
|
133
|
+
(a) =>
|
|
134
|
+
({
|
|
135
|
+
key: a.key,
|
|
136
|
+
name: a.key,
|
|
137
|
+
label: tx(a.i18nKey, a.fallback),
|
|
138
|
+
icon: a.icon,
|
|
139
|
+
}) as ActionDefinition,
|
|
140
|
+
)
|
|
141
|
+
: []
|
|
142
|
+
const actions = base.filter((a) => allowed(a.key))
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...metadata,
|
|
146
|
+
actions,
|
|
147
|
+
hasActions: actions.length > 0,
|
|
148
|
+
// The column factory synthesizes the implicit CRUD trio whenever the
|
|
149
|
+
// action list is empty and this flag is on — turn it off once gating
|
|
150
|
+
// has materialized (and possibly emptied) the list so nothing leaks
|
|
151
|
+
// back in.
|
|
152
|
+
enableCRUDActions: metadata.enableCRUDActions && actions.length > 0,
|
|
153
|
+
canExport: Boolean(metadata.canExport) && allowed('export'),
|
|
154
|
+
canImport: Boolean(metadata.canImport) && allowed('import'),
|
|
155
|
+
canCreate:
|
|
156
|
+
metadata.canCreate === undefined ? undefined : metadata.canCreate && allowed('create'),
|
|
157
|
+
}
|
|
158
|
+
}
|