@asteby/metacore-runtime-react 9.1.0 → 10.0.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 +175 -0
- package/dist/addon-layout-context.d.ts +49 -0
- package/dist/addon-layout-context.d.ts.map +1 -0
- package/dist/addon-layout-context.js +94 -0
- package/dist/addon-loader.d.ts +14 -2
- package/dist/addon-loader.d.ts.map +1 -1
- package/dist/addon-loader.js +7 -1
- package/dist/dynamic-form-schema.d.ts +5 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +34 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +18 -2
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +59 -22
- package/dist/hotswap-reload-policy.d.ts +155 -0
- package/dist/hotswap-reload-policy.d.ts.map +1 -0
- package/dist/hotswap-reload-policy.js +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/manifest-hotswap-subscriber.d.ts +83 -0
- package/dist/manifest-hotswap-subscriber.d.ts.map +1 -0
- package/dist/manifest-hotswap-subscriber.js +104 -0
- package/dist/metadata-cache.d.ts +35 -0
- package/dist/metadata-cache.d.ts.map +1 -1
- package/dist/metadata-cache.js +55 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-options-resolver.d.ts +87 -0
- package/dist/use-options-resolver.d.ts.map +1 -0
- package/dist/use-options-resolver.js +147 -0
- package/dist/use-org-config-bridge.d.ts +28 -0
- package/dist/use-org-config-bridge.d.ts.map +1 -0
- package/dist/use-org-config-bridge.js +50 -0
- package/package.json +4 -4
- package/src/__tests__/hotswap-reload-policy.test.ts +249 -0
- package/src/__tests__/manifest-hotswap-subscriber.test.ts +179 -0
- package/src/__tests__/use-options-resolver.test.ts +127 -0
- package/src/__tests__/wasm-client-sri.test.ts +82 -0
- package/src/addon-layout-context.tsx +137 -0
- package/src/addon-loader.tsx +21 -1
- package/src/dynamic-form-schema.ts +36 -0
- package/src/dynamic-form.tsx +40 -2
- package/src/dynamic-relation.tsx +55 -20
- package/src/hotswap-reload-policy.ts +360 -0
- package/src/index.ts +43 -0
- package/src/manifest-hotswap-subscriber.ts +164 -0
- package/src/metadata-cache.ts +86 -0
- package/src/types.ts +24 -0
- package/src/use-options-resolver.ts +232 -0
- package/src/use-org-config-bridge.ts +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,180 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 10.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9ce8269: feat: hot-swap reload policy (RFC-0001 D4 close)
|
|
8
|
+
|
|
9
|
+
Closes the gap between `useManifestHotSwapSubscriber` (already invalidates
|
|
10
|
+
the metadata cache) and the federation container of an already-mounted
|
|
11
|
+
addon, which keeps the old code in memory until something forces a
|
|
12
|
+
re-evaluation.
|
|
13
|
+
|
|
14
|
+
**runtime-react:**
|
|
15
|
+
- New `hotswap-reload-policy` module ships three policies and a single
|
|
16
|
+
hook that wires the chosen policy to the manifest hot-swap stream:
|
|
17
|
+
- `useHotSwapReload(client, { strategy })` returns `{ addonVersionMap }`
|
|
18
|
+
— a reactive map `addonKey → hashShort` that hosts wire into
|
|
19
|
+
`<AddonRoute version={addonVersionMap[addonKey]} ... />`. Default
|
|
20
|
+
strategy is `"rekey"`: React unmounts and remounts the addon subtree
|
|
21
|
+
on every swap, which forces the federation loader to re-fetch
|
|
22
|
+
`remoteEntry.js?v=<hash8>` and re-evaluate the exposed module.
|
|
23
|
+
- `strategy: "page-reload"` is an opt-in `window.location.reload()`
|
|
24
|
+
escape hatch for immersive addons with critical in-progress state
|
|
25
|
+
(POS, kitchen-display). Pair with `onBeforeReload` to surface an
|
|
26
|
+
"unsaved changes" prompt — returning `false` cancels the reload.
|
|
27
|
+
- `strategy: "manual"` only invokes `onSwap` with the message;
|
|
28
|
+
the host decides what to do.
|
|
29
|
+
- `clearFederationContainer(scope)` helper for hosts that hit
|
|
30
|
+
`Container already registered` after a re-key; call it from the
|
|
31
|
+
`onSwap` callback before the addon route re-mounts.
|
|
32
|
+
- `applyHotSwapReload` exported for non-React shells that want to
|
|
33
|
+
drive the policy from a vanilla container.
|
|
34
|
+
|
|
35
|
+
**sdk:**
|
|
36
|
+
- `loadFederatedAddon(spec, addonKey, version?)` accepts an optional
|
|
37
|
+
`version` so the loader cache-busts `remoteEntry.js` via
|
|
38
|
+
`?v=<hash8>` when the manifest hash bumps. Cache key includes the
|
|
39
|
+
version so a fresh hash triggers a fresh load instead of returning
|
|
40
|
+
the memoized old container.
|
|
41
|
+
- New `withVersionParam(url, hash)` helper (idempotent, fragment-safe,
|
|
42
|
+
replaces prior `v=` entries) exported for symmetry — the
|
|
43
|
+
runtime-react module re-uses the same algorithm.
|
|
44
|
+
|
|
45
|
+
**starter-core:**
|
|
46
|
+
- `<AddonRoute>` accepts a new optional `version?: string` prop. When
|
|
47
|
+
it changes, the route's children are wrapped in a `Fragment` with a
|
|
48
|
+
new `key`, forcing the federation loader to re-evaluate.
|
|
49
|
+
|
|
50
|
+
### Host wire-up (4-5 lines)
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
const ws = useWebSocket()
|
|
54
|
+
useManifestHotSwapSubscriber(ws) // metadata cache
|
|
55
|
+
const { addonVersionMap } = useHotSwapReload(ws, { strategy: 'rekey' })
|
|
56
|
+
// …in your router:
|
|
57
|
+
<AddonRoute version={addonVersionMap[addonKey]} shell={renderShell}>
|
|
58
|
+
<AddonLoader scope={addonKey} url={remoteEntryUrl} api={api} />
|
|
59
|
+
</AddonRoute>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Re-keying is intentionally destructive: any state inside the addon is
|
|
63
|
+
lost because the code version changed. Hosts that need a confirmation
|
|
64
|
+
gate should pass `onBeforeReload` and prompt the user before the swap
|
|
65
|
+
applies.
|
|
66
|
+
|
|
67
|
+
- 04362f2: feat: immersive layout, federation shared-deps helper polish, wasm client
|
|
68
|
+
|
|
69
|
+
**sdk:**
|
|
70
|
+
- `FrontendSpec` now carries `layout?: "shell" | "immersive"`. Mirrors the
|
|
71
|
+
upcoming kernel-side `manifest.FrontendSpec.Layout` field. `undefined` is
|
|
72
|
+
treated as `"shell"` (legacy behaviour) so the change is purely additive.
|
|
73
|
+
Exposed as the `AddonLayout` type alias for explicit consumers.
|
|
74
|
+
- New `wasm-client` module — frontend twin of `kernel/runtime/wasm`. Ships
|
|
75
|
+
`loadAddonWasm({ url, integrity, imports })` (SRI verification + instantiate
|
|
76
|
+
pipeline) and `callAddonExport(instance, fn, payload)` honouring the same
|
|
77
|
+
`ptr<<32 | len` packed ABI the Go example backends use (`alloc`, `free`,
|
|
78
|
+
`memory`). Lets POS / kitchen-display / signage addons run their compiled
|
|
79
|
+
module locally for sub-50ms latency without a webhook round trip. Typed
|
|
80
|
+
errors (`WasmIntegrityError`, `WasmAbiError`) surface failure cause cleanly.
|
|
81
|
+
|
|
82
|
+
**runtime-react:**
|
|
83
|
+
- New `<AddonLayoutProvider>`, `useAddonLayout()`, `useAddonLayoutControl()`
|
|
84
|
+
and `useDeclareAddonLayout()` API in `addon-layout-context`. The host shell
|
|
85
|
+
reads the active layout and hides Sidebar / Topbar / breadcrumbs when an
|
|
86
|
+
addon declares `layout: "immersive"`. Cleanup restores chrome on unmount,
|
|
87
|
+
so navigating away from an immersive addon brings the shell back.
|
|
88
|
+
- `<AddonLoader>` accepts an optional `layout` prop and propagates it through
|
|
89
|
+
the context, so hosts get the chrome switch wired without per-route plumbing.
|
|
90
|
+
|
|
91
|
+
**starter-config:**
|
|
92
|
+
- `metacoreFederationShared()` now accepts `extra: Record<string, ShareConfig>`
|
|
93
|
+
for the typical "I just want to add a package with explicit config" case
|
|
94
|
+
(`extra: { lodash: { singleton: true } }`). The existing `extras: string[]`
|
|
95
|
+
and `overrides` knobs are retained for backwards compatibility.
|
|
96
|
+
- `METACORE_FEDERATION_SINGLETONS` adds `@asteby/metacore-app-providers` so
|
|
97
|
+
the SDK's transport-agnostic platform provider keeps a single instance
|
|
98
|
+
between host and addons.
|
|
99
|
+
|
|
100
|
+
- ba60c8f: feat: immersive route wrapper + manifest hot-swap subscriber (RFC-0001 D1 + D4)
|
|
101
|
+
|
|
102
|
+
**runtime-react:**
|
|
103
|
+
- `metadata-cache` gains `invalidateAddon(addonKey, matcher?)` and `clearAll()`
|
|
104
|
+
so consumers can flush scoped cache entries when an addon's manifest hash
|
|
105
|
+
changes. The default matcher recognises `addonKey`, `${addonKey}.`,
|
|
106
|
+
`${addonKey}:` and `${addonKey}/` prefixes; hosts that namespace their
|
|
107
|
+
`model` keys differently can pass a custom matcher.
|
|
108
|
+
- New `manifest-hotswap-subscriber` module ships:
|
|
109
|
+
- `ADDON_MANIFEST_CHANGED_TYPE` — the `ws.MessageType` constant the kernel
|
|
110
|
+
emits via `bridge.WSManifestBroadcaster`.
|
|
111
|
+
- `wireHotSwapInvalidation(client, options?)` — imperative helper hosts call
|
|
112
|
+
once at boot. Accepts any object exposing `subscribe(type, handler)`
|
|
113
|
+
(structurally compatible with `useWebSocket().subscribe`), invalidates
|
|
114
|
+
the metadata cache for the bumped addon, and optionally invokes an
|
|
115
|
+
`onSwap` side-effect callback (handy for forcing a `window.location.reload()`
|
|
116
|
+
when the running addon's bundle hash changes, since metadata invalidation
|
|
117
|
+
alone does not swap the federation container already in memory).
|
|
118
|
+
- `useManifestHotSwapSubscriber(client)` — React hook variant for hosts
|
|
119
|
+
that prefer mounting the wire-up next to their WebSocket provider.
|
|
120
|
+
|
|
121
|
+
**starter-core:**
|
|
122
|
+
- New `AddonRoute` component closes the host side of RFC-0001 D1 (immersive
|
|
123
|
+
end-to-end). It reads `useAddonLayout()` from runtime-react and either
|
|
124
|
+
renders the addon inside a caller-provided shell renderer (default
|
|
125
|
+
`"shell"` layout) or strips chrome and pins the addon to the viewport
|
|
126
|
+
(`fixed inset-0 z-50`) when the active layout is `"immersive"`. Supports
|
|
127
|
+
both prop-driven layout (no shell flash for always-immersive routes like
|
|
128
|
+
POS / kitchen-display) and context-driven layout (addon calls
|
|
129
|
+
`useDeclareAddonLayout("immersive")` after mount). Cleanup restores
|
|
130
|
+
`"shell"` so navigating away brings the chrome back.
|
|
131
|
+
|
|
132
|
+
Together these closures unblock zero-polling hot-swap reloads in the
|
|
133
|
+
metadata layer and let immersive addons own the viewport without each app
|
|
134
|
+
re-implementing the shell branch.
|
|
135
|
+
|
|
136
|
+
### Patch Changes
|
|
137
|
+
|
|
138
|
+
- Updated dependencies [9ce8269]
|
|
139
|
+
- Updated dependencies [04362f2]
|
|
140
|
+
- @asteby/metacore-sdk@2.5.0
|
|
141
|
+
|
|
142
|
+
## 9.2.0
|
|
143
|
+
|
|
144
|
+
### Minor Changes
|
|
145
|
+
|
|
146
|
+
- 150a907: feat: useOptionsResolver hook + locale-aware Validation via OrgConfigProvider
|
|
147
|
+
|
|
148
|
+
**runtime-react:**
|
|
149
|
+
- New `useOptionsResolver(args)` hook that consumes the v0.9.0 kernel
|
|
150
|
+
envelope `{ success, data, meta: { type, count } }` from
|
|
151
|
+
`GET /api/options/:model?field=…`. Replaces the ad-hoc `/data/<model>`
|
|
152
|
+
reads `<DynamicRelation>` used to do.
|
|
153
|
+
- `<DynamicForm>` now renders a Ref-driven `<RefSelect>` whenever an
|
|
154
|
+
`ActionFieldDef.ref` is present — apps stop hardcoding option lists for
|
|
155
|
+
belongs_to FKs.
|
|
156
|
+
- `<DynamicRelation>` (kind="many_to_many") prefers the canonical options
|
|
157
|
+
endpoint via `useOptionsResolver`. The legacy `referencesEndpoint` prop
|
|
158
|
+
remains a working escape hatch for apps wired against custom routes.
|
|
159
|
+
- `ColumnDefinition.ref` and `ColumnDefinition.validation` are now part of
|
|
160
|
+
the metadata contract the SDK reads. `ActionFieldDef.ref` joins the
|
|
161
|
+
field-level type so addons can declare ref-aware modal fields.
|
|
162
|
+
- New `setOrgConfigBridge` / `resolveValidatorToken` surface lets apps
|
|
163
|
+
feed a `useOrgConfig`-backed resolver into the SDK's validator
|
|
164
|
+
pipeline. Validators with `custom: '$org.<key>'` are resolved at form
|
|
165
|
+
build time; unresolved tokens degrade to no-op so missing config does
|
|
166
|
+
not crash forms.
|
|
167
|
+
- New `registerValidator(slug, fn)` lets apps install their own
|
|
168
|
+
region-specific validators (e.g. `mx.rfc`, `co.nit`) without leaking
|
|
169
|
+
fiscal vocabulary into the SDK.
|
|
170
|
+
|
|
171
|
+
**app-providers:**
|
|
172
|
+
- New `OrgConfigProvider` + `useOrgConfig()` companion to
|
|
173
|
+
`PlatformConfigProvider`. Apps wire a per-org config fetcher and the
|
|
174
|
+
provider exposes typed `currency`, `locale`, `validators` plus a
|
|
175
|
+
`resolveValidator(refOrKey)` helper for the `$org.<key>` reference
|
|
176
|
+
contract the kernel ≥ v0.9.0 emits.
|
|
177
|
+
|
|
3
178
|
## 9.1.0
|
|
4
179
|
|
|
5
180
|
### Minor Changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AddonLayout } from '@asteby/metacore-sdk';
|
|
2
|
+
export type { AddonLayout };
|
|
3
|
+
interface AddonLayoutState {
|
|
4
|
+
/** Active layout. `"shell"` (default) or `"immersive"`. */
|
|
5
|
+
layout: AddonLayout;
|
|
6
|
+
/**
|
|
7
|
+
* Imperative setter for the host or an addon-loader to mutate the active
|
|
8
|
+
* layout. Exposed for advanced use; most callers should use
|
|
9
|
+
* `useDeclareAddonLayout(layout)` from a route component, which scopes the
|
|
10
|
+
* change to the route's mount lifetime.
|
|
11
|
+
*/
|
|
12
|
+
setLayout: (layout: AddonLayout) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface AddonLayoutProviderProps {
|
|
15
|
+
/** Initial layout — usually `"shell"`. */
|
|
16
|
+
initial?: AddonLayout;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wrap the host app once, above the router outlet. The provider keeps the
|
|
21
|
+
* currently-active layout in state; addon-loader and `useDeclareAddonLayout`
|
|
22
|
+
* mutate it from below.
|
|
23
|
+
*/
|
|
24
|
+
export declare function AddonLayoutProvider({ initial, children, }: AddonLayoutProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
/**
|
|
26
|
+
* Read the currently-active layout. The host shell calls this and decides
|
|
27
|
+
* whether to render its chrome. Returns `"shell"` when no provider is
|
|
28
|
+
* mounted, so apps that have not adopted immersive addons keep working.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useAddonLayout(): AddonLayout;
|
|
31
|
+
/**
|
|
32
|
+
* Imperative API — the value returned mirrors `useAddonLayout()` but also
|
|
33
|
+
* exposes the setter for hosts that need to flip the layout outside of a
|
|
34
|
+
* route lifecycle (e.g. a hotkey forcing kiosk mode). Most addon entries do
|
|
35
|
+
* NOT need this; prefer `useDeclareAddonLayout`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function useAddonLayoutControl(): AddonLayoutState;
|
|
38
|
+
/**
|
|
39
|
+
* Declare the layout from the addon side. Mounts the value, restores
|
|
40
|
+
* `"shell"` on unmount. Skip when `layout` is undefined so route components
|
|
41
|
+
* can pass `manifest.frontend?.layout` directly without branching.
|
|
42
|
+
*
|
|
43
|
+
* function PosEntry({ manifest }: { manifest: Manifest }) {
|
|
44
|
+
* useDeclareAddonLayout(manifest.frontend?.layout)
|
|
45
|
+
* return <PosScreen />
|
|
46
|
+
* }
|
|
47
|
+
*/
|
|
48
|
+
export declare function useDeclareAddonLayout(layout: AddonLayout | undefined): void;
|
|
49
|
+
//# sourceMappingURL=addon-layout-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addon-layout-context.d.ts","sourceRoot":"","sources":["../src/addon-layout-context.tsx"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAEvD,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,UAAU,gBAAgB;IACtB,2DAA2D;IAC3D,MAAM,EAAE,WAAW,CAAA;IACnB;;;;;OAKG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;CAC3C;AAWD,MAAM,WAAW,wBAAwB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAChC,OAAiB,EACjB,QAAQ,GACX,EAAE,wBAAwB,2CAW1B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,IAAI,gBAAgB,CAExD;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAY3E"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// AddonLayoutContext — broadcast the active addon entry's layout selection
|
|
3
|
+
// (`shell` vs `immersive`) up to the host so it can hide/show its chrome
|
|
4
|
+
// (Sidebar, Topbar, breadcrumbs) when an immersive addon is mounted.
|
|
5
|
+
//
|
|
6
|
+
// Why a context rather than a prop on the host shell:
|
|
7
|
+
//
|
|
8
|
+
// 1. The host shell is rendered ABOVE the addon route in the tree, but the
|
|
9
|
+
// decision about what layout the addon wants comes from the addon itself
|
|
10
|
+
// (manifest.frontend.layout) which the AddonLoader knows about at mount
|
|
11
|
+
// time. A bottom-up signal via context inverts the dependency cleanly.
|
|
12
|
+
//
|
|
13
|
+
// 2. Addon entries can swap layouts at runtime (think a kiosk-mode toggle
|
|
14
|
+
// inside a POS). A context value reactively updates the host without
|
|
15
|
+
// asking each route to wire props.
|
|
16
|
+
//
|
|
17
|
+
// 3. When the user navigates AWAY from an immersive addon, the AddonLoader
|
|
18
|
+
// unmounts, its layout context updater fires `setLayout("shell")` from
|
|
19
|
+
// a cleanup effect, and the chrome restores automatically.
|
|
20
|
+
//
|
|
21
|
+
// Host integration (starter-core, ops, …):
|
|
22
|
+
//
|
|
23
|
+
// function AppShell({ children }) {
|
|
24
|
+
// const layout = useAddonLayout()
|
|
25
|
+
// const chrome = layout !== "immersive"
|
|
26
|
+
// return (
|
|
27
|
+
// <div className={chrome ? "grid grid-cols-[280px_1fr]" : "h-dvh w-dvw"}>
|
|
28
|
+
// {chrome && <Sidebar />}
|
|
29
|
+
// <main>{chrome && <Topbar />}{children}</main>
|
|
30
|
+
// </div>
|
|
31
|
+
// )
|
|
32
|
+
// }
|
|
33
|
+
//
|
|
34
|
+
// The context defaults to `"shell"`, so apps that never mount an
|
|
35
|
+
// `<AddonLayoutProvider>` keep the legacy behaviour.
|
|
36
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
|
|
37
|
+
const defaultState = {
|
|
38
|
+
layout: 'shell',
|
|
39
|
+
setLayout: () => {
|
|
40
|
+
/* noop — provider missing; consumers degrade to legacy "shell" */
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const AddonLayoutContext = createContext(defaultState);
|
|
44
|
+
/**
|
|
45
|
+
* Wrap the host app once, above the router outlet. The provider keeps the
|
|
46
|
+
* currently-active layout in state; addon-loader and `useDeclareAddonLayout`
|
|
47
|
+
* mutate it from below.
|
|
48
|
+
*/
|
|
49
|
+
export function AddonLayoutProvider({ initial = 'shell', children, }) {
|
|
50
|
+
const [layout, setLayout] = useState(initial);
|
|
51
|
+
const value = useMemo(() => ({ layout, setLayout }), [layout]);
|
|
52
|
+
return (_jsx(AddonLayoutContext.Provider, { value: value, children: children }));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Read the currently-active layout. The host shell calls this and decides
|
|
56
|
+
* whether to render its chrome. Returns `"shell"` when no provider is
|
|
57
|
+
* mounted, so apps that have not adopted immersive addons keep working.
|
|
58
|
+
*/
|
|
59
|
+
export function useAddonLayout() {
|
|
60
|
+
return useContext(AddonLayoutContext).layout;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Imperative API — the value returned mirrors `useAddonLayout()` but also
|
|
64
|
+
* exposes the setter for hosts that need to flip the layout outside of a
|
|
65
|
+
* route lifecycle (e.g. a hotkey forcing kiosk mode). Most addon entries do
|
|
66
|
+
* NOT need this; prefer `useDeclareAddonLayout`.
|
|
67
|
+
*/
|
|
68
|
+
export function useAddonLayoutControl() {
|
|
69
|
+
return useContext(AddonLayoutContext);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Declare the layout from the addon side. Mounts the value, restores
|
|
73
|
+
* `"shell"` on unmount. Skip when `layout` is undefined so route components
|
|
74
|
+
* can pass `manifest.frontend?.layout` directly without branching.
|
|
75
|
+
*
|
|
76
|
+
* function PosEntry({ manifest }: { manifest: Manifest }) {
|
|
77
|
+
* useDeclareAddonLayout(manifest.frontend?.layout)
|
|
78
|
+
* return <PosScreen />
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
export function useDeclareAddonLayout(layout) {
|
|
82
|
+
const { setLayout } = useAddonLayoutControl();
|
|
83
|
+
// useCallback so the effect only re-runs on a real layout change, not on
|
|
84
|
+
// every render of the consumer that happens to forward an inline literal.
|
|
85
|
+
const apply = useCallback(setLayout, [setLayout]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!layout || layout === 'shell')
|
|
88
|
+
return;
|
|
89
|
+
apply(layout);
|
|
90
|
+
return () => {
|
|
91
|
+
apply('shell');
|
|
92
|
+
};
|
|
93
|
+
}, [layout, apply]);
|
|
94
|
+
}
|
package/dist/addon-loader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AddonAPI } from '@asteby/metacore-sdk';
|
|
1
|
+
import type { AddonAPI, AddonLayout } from '@asteby/metacore-sdk';
|
|
2
2
|
declare global {
|
|
3
3
|
interface Window {
|
|
4
4
|
[key: string]: any;
|
|
@@ -21,7 +21,19 @@ export interface AddonLoaderProps {
|
|
|
21
21
|
onReady?: () => void;
|
|
22
22
|
/** Called if loading fails. */
|
|
23
23
|
onError?: (err: Error) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Layout the host shell should render the addon under, mirroring
|
|
26
|
+
* `manifest.frontend.layout`. Default (undefined / `"shell"`) keeps the
|
|
27
|
+
* legacy chrome (Sidebar, Topbar, breadcrumbs). `"immersive"` flips the
|
|
28
|
+
* shared {@link useAddonLayout} context so the host shell hides chrome
|
|
29
|
+
* while the addon is mounted and restores it on unmount.
|
|
30
|
+
*
|
|
31
|
+
* Hosts that consume the context (see `useAddonLayout` /
|
|
32
|
+
* `<AddonLayoutProvider>`) do NOT need to branch on this prop themselves
|
|
33
|
+
* — the loader sets the context value via {@link useDeclareAddonLayout}.
|
|
34
|
+
*/
|
|
35
|
+
layout?: AddonLayout;
|
|
24
36
|
children?: React.ReactNode;
|
|
25
37
|
}
|
|
26
|
-
export declare function AddonLoader({ scope, url, module, api, fallback, onReady, onError, children, }: AddonLoaderProps): import("react/jsx-runtime").JSX.Element;
|
|
38
|
+
export declare function AddonLoader({ scope, url, module, api, fallback, onReady, onError, layout, children, }: AddonLoaderProps): import("react/jsx-runtime").JSX.Element;
|
|
27
39
|
//# sourceMappingURL=addon-loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAGjE,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,MAAM;QACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;QAClB,wBAAwB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3D,wBAAwB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KACrD;CACJ;AAED,MAAM,WAAW,gBAAgB;IAC7B,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,GAAG,EAAE,QAAQ,CAAA;IACb,wCAAwC;IACxC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAuCD,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,GAAG,EACH,MAAqB,EACrB,GAAG,EACH,QAAe,EACf,OAAO,EACP,OAAO,EACP,MAAM,EACN,QAAQ,GACX,EAAE,gBAAgB,2CAsClB"}
|
package/dist/addon-loader.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
// waits for the `window[scope]` container to initialize, then calls the
|
|
4
4
|
// addon's `register(api)` export with the AddonAPI injected by the host.
|
|
5
5
|
import { useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { useDeclareAddonLayout } from './addon-layout-context';
|
|
6
7
|
const loadedScripts = new Map();
|
|
7
8
|
function loadScript(url, scope) {
|
|
8
9
|
const key = `${scope}::${url}`;
|
|
@@ -34,10 +35,15 @@ async function loadRemote(scope, module) {
|
|
|
34
35
|
const factory = await container.get(module);
|
|
35
36
|
return factory();
|
|
36
37
|
}
|
|
37
|
-
export function AddonLoader({ scope, url, module = './register', api, fallback = null, onReady, onError, children, }) {
|
|
38
|
+
export function AddonLoader({ scope, url, module = './register', api, fallback = null, onReady, onError, layout, children, }) {
|
|
38
39
|
const [status, setStatus] = useState('loading');
|
|
39
40
|
const [error, setError] = useState(null);
|
|
40
41
|
const didRegister = useRef(false);
|
|
42
|
+
// Propagate the addon's preferred layout to the host shell via context.
|
|
43
|
+
// No-op when `layout` is undefined or `"shell"` (legacy default). Cleanup
|
|
44
|
+
// restores `"shell"` automatically when the loader unmounts, so chrome
|
|
45
|
+
// returns as soon as the user navigates away from an immersive addon.
|
|
46
|
+
useDeclareAddonLayout(layout);
|
|
41
47
|
useEffect(() => {
|
|
42
48
|
let cancelled = false;
|
|
43
49
|
(async () => {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ActionFieldDef } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Apps register validator implementations by slug. The slug is the value
|
|
5
|
+
* `OrgConfig.validators[<key>]` returns for a $org.<key> reference.
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerValidator(slug: string, fn: (s: z.ZodString) => z.ZodString): void;
|
|
3
8
|
export declare function buildZodSchema(fields: ActionFieldDef[]): z.ZodObject<{
|
|
4
9
|
[x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
5
10
|
}, z.core.$strip>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,SAAS,CAAA;AAiB9D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AA4CD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAU3D"}
|
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
// callers (and unit tests) can use the zod schema without pulling in React or
|
|
3
3
|
// metacore-ui primitives.
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { resolveValidatorToken } from './use-org-config-bridge';
|
|
6
|
+
/**
|
|
7
|
+
* Built-in validators the SDK knows how to apply by symbolic name. Apps
|
|
8
|
+
* that wire `OrgConfigProvider` map `$org.<key>` references to one of
|
|
9
|
+
* these slugs (or to a custom slug they register). Unknown slugs are a
|
|
10
|
+
* no-op so unresolved $org references degrade to "no extra check"
|
|
11
|
+
* rather than a runtime crash — matches the kernel's pass-through
|
|
12
|
+
* semantics for unresolved references.
|
|
13
|
+
*/
|
|
14
|
+
const builtinValidators = {
|
|
15
|
+
// The SDK ships ZERO fiscal vocabulary by default. Apps register
|
|
16
|
+
// their own validators (mx.rfc, co.nit, pe.ruc, etc.) via
|
|
17
|
+
// `registerValidator` so kernel/SDK stay region-agnostic.
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Apps register validator implementations by slug. The slug is the value
|
|
21
|
+
* `OrgConfig.validators[<key>]` returns for a $org.<key> reference.
|
|
22
|
+
*/
|
|
23
|
+
export function registerValidator(slug, fn) {
|
|
24
|
+
builtinValidators[slug] = fn;
|
|
25
|
+
}
|
|
26
|
+
function applyCustomValidator(s, customToken) {
|
|
27
|
+
if (!customToken)
|
|
28
|
+
return s;
|
|
29
|
+
const resolved = resolveValidatorToken(customToken);
|
|
30
|
+
if (!resolved)
|
|
31
|
+
return s;
|
|
32
|
+
const fn = builtinValidators[resolved];
|
|
33
|
+
return fn ? fn(s) : s;
|
|
34
|
+
}
|
|
5
35
|
// Builds a zod object schema from an ActionFieldDef[]. Required fields stay
|
|
6
36
|
// non-empty; optional fields accept undefined / "". Validation rules
|
|
7
37
|
// (regex/min/max) layer on top: for numeric columns they bound the value, for
|
|
@@ -46,6 +76,10 @@ function fieldToZod(field) {
|
|
|
46
76
|
s = s.email('Email inválido');
|
|
47
77
|
if (field.type === 'url')
|
|
48
78
|
s = s.url('URL inválida');
|
|
79
|
+
// Custom validator: a literal slug (`mx.rfc`) OR a `$org.<key>`
|
|
80
|
+
// reference resolved through the OrgConfigProvider. Unknown slugs
|
|
81
|
+
// pass through as no-ops so apps never crash on missing config.
|
|
82
|
+
s = applyCustomValidator(s, v.custom);
|
|
49
83
|
if (field.required) {
|
|
50
84
|
return s.min(Math.max(typeof v.min === 'number' ? v.min : 1, 1), `${field.label} es requerido`);
|
|
51
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAGrE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AAExC,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,2CAgElB"}
|
package/dist/dynamic-form.js
CHANGED
|
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
import { useEffect, useMemo, useState } from 'react';
|
|
6
6
|
import { Input, Textarea, Label, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
|
|
7
7
|
import { buildZodSchema, resolveWidget } from './dynamic-form-schema';
|
|
8
|
+
import { useOptionsResolver } from './use-options-resolver';
|
|
8
9
|
export { buildZodSchema, resolveWidget };
|
|
9
10
|
export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, }) {
|
|
10
11
|
const [values, setValues] = useState({});
|
|
@@ -42,10 +43,17 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
|
|
|
42
43
|
setSubmitting(false);
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
|
-
return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [fields.map((field) => (_jsxs("div", { className: "grid gap-2", children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }),
|
|
46
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [fields.map((field) => (_jsxs("div", { className: "grid gap-2", children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), _jsx(FieldRenderer, { field: field, value: values[field.key], onChange: (v) => update(field.key, v) }), errors[field.key] && (_jsx("span", { className: "text-red-500 text-sm", role: "alert", children: errors[field.key] }))] }, field.key))), _jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })), _jsx(Button, { type: "submit", disabled: submitting || disabled, children: submitLabel })] })] }));
|
|
46
47
|
}
|
|
47
|
-
function
|
|
48
|
+
function FieldRenderer({ field, value, onChange }) {
|
|
48
49
|
const widget = resolveWidget(field);
|
|
50
|
+
// Ref-driven select: hook into useOptionsResolver so the canonical
|
|
51
|
+
// /api/options/<ref>?field=id endpoint feeds the dropdown. This is
|
|
52
|
+
// the path the kernel auto-derives for FK columns; legacy callers
|
|
53
|
+
// shipping inline `options` keep working in the branch below.
|
|
54
|
+
if (widget === 'select' && field.ref) {
|
|
55
|
+
return _jsx(RefSelect, { field: field, value: value, onChange: onChange });
|
|
56
|
+
}
|
|
49
57
|
switch (widget) {
|
|
50
58
|
case 'textarea':
|
|
51
59
|
return _jsx(Textarea, { id: field.key, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
|
|
@@ -68,3 +76,11 @@ function renderField(field, value, onChange) {
|
|
|
68
76
|
return _jsx(Input, { id: field.key, type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
|
|
69
77
|
}
|
|
70
78
|
}
|
|
79
|
+
function RefSelect({ field, value, onChange }) {
|
|
80
|
+
const { options, loading } = useOptionsResolver({
|
|
81
|
+
modelKey: '', // unused — `ref` drives the URL
|
|
82
|
+
fieldKey: 'id',
|
|
83
|
+
ref: field.ref,
|
|
84
|
+
});
|
|
85
|
+
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: loading, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : (field.placeholder || 'Seleccionar...') }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
|
|
86
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AA0CA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,eAAe,EACf,cAAc,GACjB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;CACtB;AAiBD,UAAU,WAAW;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,WAAW;IAC9D,IAAI,EAAE,aAAa,CAAA;IACnB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,WAAW;IAC/D,IAAI,EAAE,cAAc,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAA;AAEpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAK1D"}
|