@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.
@@ -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
+ }