@asteby/metacore-runtime-react 13.1.0 → 13.3.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 +28 -0
- package/dist/addon-loader.d.ts +1 -8
- package/dist/addon-loader.d.ts.map +1 -1
- package/dist/addon-loader.js +51 -79
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +3 -0
- package/dist/dynamic-form.d.ts +1 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +8 -0
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +6 -0
- package/dist/dynamic-select-field.d.ts +9 -0
- package/dist/dynamic-select-field.d.ts.map +1 -0
- package/dist/dynamic-select-field.js +74 -0
- package/dist/hotswap-reload-policy.d.ts +8 -12
- package/dist/hotswap-reload-policy.d.ts.map +1 -1
- package/dist/hotswap-reload-policy.js +17 -30
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/addon-loader.tsx +67 -95
- package/src/dynamic-form-schema.ts +3 -0
- package/src/dynamic-form.tsx +8 -0
- package/src/dynamic-line-items.tsx +6 -0
- package/src/dynamic-select-field.tsx +164 -0
- package/src/hotswap-reload-policy.ts +18 -31
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 13.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 99477d6: feat(runtime-react): add `dynamic_select` field widget — async searchable FK picker
|
|
8
|
+
|
|
9
|
+
Declarative answer to "I don't want to type a raw FK UUID". A field with
|
|
10
|
+
`type: "dynamic_select"` (or `widget: "dynamic_select"`) + `ref` renders a
|
|
11
|
+
typeahead combobox that queries the canonical options endpoint as the user
|
|
12
|
+
types (`GET /api/options/<ref>?field=id&q=<text>&limit=<n>`), reusing
|
|
13
|
+
`useOptionsResolver` (debounced, abortable). Works both as a flat form field
|
|
14
|
+
and as a line-items column cell (e.g. the account_id per debit/credit row of a
|
|
15
|
+
journal entry). The metacore equivalent of 7leguas' `type: search`, driven
|
|
16
|
+
entirely from the manifest — addons get a searchable picker with zero custom
|
|
17
|
+
React, keeping custom federated frontends for genuinely page-level UIs (POS).
|
|
18
|
+
|
|
19
|
+
## 13.2.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- fb45ad4: Migrate federation tooling from the broken `@originjs/vite-plugin-federation` to the official `@module-federation/vite` + `@module-federation/runtime`.
|
|
24
|
+
|
|
25
|
+
**BREAKING (federation runtime swap — hosts and addons must rebuild):**
|
|
26
|
+
- `metacoreFederationShared()` (starter-config) now returns a `@module-federation/vite` `federation()` config instead of an `@originjs` config. Same signature/call-sites: `metacoreFederationShared({ host })` → host config (name + shared, empty remotes — remotes register dynamically at runtime); `metacoreFederationShared({ host, exposes })` → remote config (name + filename + exposes + shared). Hosts MUST switch their `vite.config.ts` to `import { federation } from '@module-federation/vite'`.
|
|
27
|
+
- The shared singleton list is now `{ singleton: true }` (no `requiredVersion: false`) and matches the addon + ops host contract exactly: `react`, `react-dom`, `react/jsx-runtime`, `react-i18next`, `i18next`, `@asteby/metacore-ui`, `@asteby/metacore-runtime-react`, `@asteby/metacore-sdk`, `@asteby/metacore-app-providers`, `@asteby/metacore-theme`, `@asteby/metacore-auth`. **Build-time gotcha:** `@module-federation/vite` resolves every shared bare specifier at build time, so each must be an installed (dev)dependency of the building package.
|
|
28
|
+
- `AddonLoader` (runtime-react) now uses `@module-federation/runtime` (`registerRemotes` + `loadRemote`) instead of the manual `init`/`get`/`window[scope]` machinery. The host's `@module-federation/vite` build auto-initialises the shared scope, so the remote consumes the HOST's React/SDK singletons — fixing the `useState`-null crash.
|
|
29
|
+
- `clearFederationContainer()` is now a deprecated no-op under the MF runtime (container replacement on hot-swap is handled by `registerRemotes(..., { force: true })`).
|
|
30
|
+
|
|
3
31
|
## 13.1.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
package/dist/addon-loader.d.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { AddonAPI, AddonLayout } from '@asteby/metacore-sdk';
|
|
2
|
-
declare global {
|
|
3
|
-
interface Window {
|
|
4
|
-
[key: string]: any;
|
|
5
|
-
__webpack_init_sharing__?: (scope: string) => Promise<void>;
|
|
6
|
-
__webpack_share_scopes__?: Record<string, unknown>;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
2
|
export interface AddonLoaderProps {
|
|
10
3
|
/** Unique key of the addon — maps to the federation container name. */
|
|
11
4
|
scope: string;
|
|
12
|
-
/** URL of the addon's remoteEntry.js bundle. */
|
|
5
|
+
/** URL of the addon's remoteEntry.js bundle (may carry a `?v=` cache-bust). */
|
|
13
6
|
url: string;
|
|
14
7
|
/** Exposed module to import from the remote (e.g. './register'). */
|
|
15
8
|
module?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"addon-loader.d.ts","sourceRoot":"","sources":["../src/addon-loader.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAGjE,MAAM,WAAW,gBAAgB;IAC7B,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IACb,+EAA+E;IAC/E,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;AA6CD,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,GAAG,EACH,MAAqB,EACrB,GAAG,EACH,QAAe,EACf,OAAO,EACP,OAAO,EACP,MAAM,EACN,QAAQ,GACX,EAAE,gBAAgB,2CA8ClB"}
|
package/dist/addon-loader.js
CHANGED
|
@@ -1,81 +1,46 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Federated-module addon loader, built on the official Module Federation
|
|
3
|
+
// runtime (`@module-federation/runtime`). Per addon it registers the remote's
|
|
4
|
+
// `remoteEntry.js` as an ESM container, loads the exposed `./register` module,
|
|
5
|
+
// and calls `register(api)` with the AddonAPI injected by the host.
|
|
6
|
+
//
|
|
7
|
+
// Why @module-federation/runtime (not the old manual init/get machinery):
|
|
8
|
+
// the host's Vite build uses `@module-federation/vite`'s `federation()` plugin,
|
|
9
|
+
// which auto-initialises the shared scope at host boot. `registerRemotes` +
|
|
10
|
+
// `loadRemote` then transparently wire the remote into that already-initialised
|
|
11
|
+
// share scope — so the remote consumes the HOST's React/SDK singletons instead
|
|
12
|
+
// of bundling its own. That's the whole point: it fixes the `useState`-null
|
|
13
|
+
// crash WITHOUT this loader ever touching a share scope manually.
|
|
5
14
|
import { useEffect, useRef, useState } from 'react';
|
|
15
|
+
import { registerRemotes, loadRemote } from '@module-federation/runtime';
|
|
6
16
|
import { useDeclareAddonLayout } from './addon-layout-context';
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const promise = new Promise((resolve, reject) => {
|
|
18
|
-
const el = document.createElement('script');
|
|
19
|
-
el.src = url;
|
|
20
|
-
el.type = 'text/javascript';
|
|
21
|
-
el.async = true;
|
|
22
|
-
el.onload = () => resolve();
|
|
23
|
-
el.onerror = () => reject(new Error(`Failed to load addon script: ${url}`));
|
|
24
|
-
document.head.appendChild(el);
|
|
25
|
-
});
|
|
26
|
-
loadedScripts.set(key, promise);
|
|
27
|
-
return promise;
|
|
17
|
+
// `registerRemotes` is additive + idempotent across re-mounts; we still track
|
|
18
|
+
// which scopes we've registered to avoid redundant `force` churn (each `force`
|
|
19
|
+
// re-register wipes that remote's module cache and logs a runtime warning).
|
|
20
|
+
const registered = new Set();
|
|
21
|
+
// Derive the `loadRemote` id from the scope + exposed module name. MF resolves
|
|
22
|
+
// `"<remoteName>/<expose>"` — e.g. `metacore_tickets/register` for the
|
|
23
|
+
// `"./register"` expose. We strip the leading `./` of the expose path.
|
|
24
|
+
function remoteId(scope, module) {
|
|
25
|
+
const expose = module.replace(/^\.\//, '');
|
|
26
|
+
return `${scope}/${expose}`;
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (cached)
|
|
41
|
-
return cached;
|
|
42
|
-
const p = (async () => {
|
|
43
|
-
try {
|
|
44
|
-
const mod = await importModule(url);
|
|
45
|
-
if (mod && typeof mod.init === 'function' && typeof mod.get === 'function') {
|
|
46
|
-
return mod;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
// Not an importable module (legacy var-format remote) — fall back.
|
|
51
|
-
}
|
|
52
|
-
await loadScript(url, scope);
|
|
53
|
-
return window[scope];
|
|
54
|
-
})();
|
|
55
|
-
esmContainers.set(key, p);
|
|
56
|
-
p.catch(() => esmContainers.delete(key));
|
|
57
|
-
return p;
|
|
58
|
-
}
|
|
59
|
-
async function loadRemote(scope, url, module) {
|
|
60
|
-
if (typeof window.__webpack_init_sharing__ === 'function') {
|
|
61
|
-
await window.__webpack_init_sharing__('default');
|
|
62
|
-
}
|
|
63
|
-
const container = await resolveContainer(scope, url);
|
|
64
|
-
if (!container) {
|
|
65
|
-
throw new Error(`Addon container "${scope}" not found (neither ESM export nor window[scope])`);
|
|
28
|
+
async function loadAddon(scope, url, module) {
|
|
29
|
+
// Register the remote container as an ES module. `type: 'module'` matches
|
|
30
|
+
// the `@module-federation/vite` remote (remoteEntry.js is an ESM bundle).
|
|
31
|
+
// The `url` already carries the `?v=` cache-bust the host computed, so the
|
|
32
|
+
// browser refetches a fresh remoteEntry when the addon version changes.
|
|
33
|
+
if (!registered.has(scope)) {
|
|
34
|
+
registerRemotes([{ name: scope, entry: url, type: 'module' }],
|
|
35
|
+
// `force: true` so a re-registration with a new `?v=` URL (addon
|
|
36
|
+
// hot-swap / version bump) overwrites the stale entry + cache.
|
|
37
|
+
{ force: true });
|
|
38
|
+
registered.add(scope);
|
|
66
39
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await container.init(shareScope);
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// Container already initialized (re-entrant load) — safe to ignore.
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const factory = await container.get(module);
|
|
78
|
-
return factory();
|
|
40
|
+
// loadRemote("<scope>/<expose>") returns the exposed module namespace (or
|
|
41
|
+
// null if it can't be resolved). No manual share-scope init — the host's
|
|
42
|
+
// federation runtime already initialised it.
|
|
43
|
+
return loadRemote(remoteId(scope, module));
|
|
79
44
|
}
|
|
80
45
|
export function AddonLoader({ scope, url, module = './register', api, fallback = null, onReady, onError, layout, children, }) {
|
|
81
46
|
const [status, setStatus] = useState('loading');
|
|
@@ -90,12 +55,16 @@ export function AddonLoader({ scope, url, module = './register', api, fallback =
|
|
|
90
55
|
let cancelled = false;
|
|
91
56
|
(async () => {
|
|
92
57
|
try {
|
|
93
|
-
const mod = await
|
|
58
|
+
const mod = await loadAddon(scope, url, module);
|
|
94
59
|
if (cancelled)
|
|
95
60
|
return;
|
|
96
|
-
|
|
61
|
+
const register = mod?.register ?? mod?.default;
|
|
62
|
+
if (typeof register !== 'function') {
|
|
63
|
+
throw new Error(`Addon "${scope}" module "${module}" has no register() export`);
|
|
64
|
+
}
|
|
65
|
+
if (!didRegister.current) {
|
|
97
66
|
didRegister.current = true;
|
|
98
|
-
await Promise.resolve(
|
|
67
|
+
await Promise.resolve(register(api));
|
|
99
68
|
}
|
|
100
69
|
setStatus('ready');
|
|
101
70
|
onReady?.();
|
|
@@ -103,12 +72,15 @@ export function AddonLoader({ scope, url, module = './register', api, fallback =
|
|
|
103
72
|
catch (e) {
|
|
104
73
|
if (cancelled)
|
|
105
74
|
return;
|
|
106
|
-
|
|
75
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
76
|
+
setError(err);
|
|
107
77
|
setStatus('error');
|
|
108
|
-
onError?.(
|
|
78
|
+
onError?.(err);
|
|
109
79
|
}
|
|
110
80
|
})();
|
|
111
|
-
return () => {
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
};
|
|
112
84
|
}, [scope, url, module]);
|
|
113
85
|
if (status === 'loading')
|
|
114
86
|
return _jsx(_Fragment, { children: fallback });
|
|
@@ -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;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;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,CAGrE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE/D;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,
|
|
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;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,CAGrE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE/D;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAa3D"}
|
|
@@ -116,6 +116,9 @@ export function resolveWidget(field) {
|
|
|
116
116
|
switch (field.type) {
|
|
117
117
|
case 'textarea': return 'textarea';
|
|
118
118
|
case 'select': return 'select';
|
|
119
|
+
// Async searchable single-select against /api/options/<ref>. The
|
|
120
|
+
// declarative replacement for typing a raw FK UUID.
|
|
121
|
+
case 'dynamic_select': return 'dynamic_select';
|
|
119
122
|
case 'boolean': return 'switch';
|
|
120
123
|
case 'number': return 'number';
|
|
121
124
|
case 'date': return 'date';
|
package/dist/dynamic-form.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ActionFieldDef } from './types';
|
|
|
2
2
|
import { buildZodSchema, resolveWidget } from './dynamic-form-schema';
|
|
3
3
|
export { buildZodSchema, resolveWidget };
|
|
4
4
|
export { DynamicLineItems } from './dynamic-line-items';
|
|
5
|
+
export { DynamicSelectField } from './dynamic-select-field';
|
|
5
6
|
export interface DynamicFormProps {
|
|
6
7
|
fields: ActionFieldDef[];
|
|
7
8
|
initialValues?: Record<string, any>;
|
|
@@ -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,EAAoB,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,EAAoB,MAAM,uBAAuB,CAAA;AAKvF,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAE3D,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,2CAoElB"}
|
package/dist/dynamic-form.js
CHANGED
|
@@ -7,8 +7,10 @@ import { Input, Textarea, Label, Switch, Button, Select, SelectContent, SelectIt
|
|
|
7
7
|
import { buildZodSchema, resolveWidget, isLineItemsField } from './dynamic-form-schema';
|
|
8
8
|
import { useOptionsResolver } from './use-options-resolver';
|
|
9
9
|
import { DynamicLineItems } from './dynamic-line-items';
|
|
10
|
+
import { DynamicSelectField } from './dynamic-select-field';
|
|
10
11
|
export { buildZodSchema, resolveWidget };
|
|
11
12
|
export { DynamicLineItems } from './dynamic-line-items';
|
|
13
|
+
export { DynamicSelectField } from './dynamic-select-field';
|
|
12
14
|
export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, }) {
|
|
13
15
|
const [values, setValues] = useState({});
|
|
14
16
|
const [errors, setErrors] = useState({});
|
|
@@ -58,6 +60,12 @@ function FieldRenderer({ field, value, onChange }) {
|
|
|
58
60
|
return _jsx(DynamicLineItems, { field: field, value: value, onChange: onChange });
|
|
59
61
|
}
|
|
60
62
|
const widget = resolveWidget(field);
|
|
63
|
+
// Async searchable picker (typeahead against /api/options/<ref>?q=…).
|
|
64
|
+
// Preferred for FK fields with large option sets — no UUID typing, no
|
|
65
|
+
// dumping every row into a plain <select>.
|
|
66
|
+
if (widget === 'dynamic_select') {
|
|
67
|
+
return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
|
|
68
|
+
}
|
|
61
69
|
// Ref-driven select: hook into useOptionsResolver so the canonical
|
|
62
70
|
// /api/options/<ref>?field=id endpoint feeds the dropdown. This is
|
|
63
71
|
// the path the kernel auto-derives for FK columns; legacy callers
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAK7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAUD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,EAAE,qBAAqB,2CAwEnG"}
|
|
@@ -11,6 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
import { Input, Textarea, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
|
|
12
12
|
import { Plus, Trash2 } from 'lucide-react';
|
|
13
13
|
import { resolveWidget, getItemFields } from './dynamic-form-schema';
|
|
14
|
+
import { DynamicSelectField } from './dynamic-select-field';
|
|
14
15
|
import { useOptionsResolver } from './use-options-resolver';
|
|
15
16
|
function emptyRow(itemFields) {
|
|
16
17
|
const row = {};
|
|
@@ -33,6 +34,11 @@ export function DynamicLineItems({ field, value, onChange, disabled = false }) {
|
|
|
33
34
|
// a scalar widget).
|
|
34
35
|
function CellRenderer({ field, value, onChange, disabled }) {
|
|
35
36
|
const widget = resolveWidget(field);
|
|
37
|
+
// Async searchable picker per row cell — e.g. the account_id column of a
|
|
38
|
+
// journal entry's debit/credit lines. Same widget as the flat form.
|
|
39
|
+
if (widget === 'dynamic_select') {
|
|
40
|
+
return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
|
|
41
|
+
}
|
|
36
42
|
if (widget === 'select' && field.ref) {
|
|
37
43
|
return _jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled });
|
|
38
44
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ActionFieldDef } from './types';
|
|
2
|
+
export interface DynamicSelectFieldProps {
|
|
3
|
+
field: ActionFieldDef;
|
|
4
|
+
value: any;
|
|
5
|
+
onChange: (v: any) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function DynamicSelectField({ field, value, onChange }: DynamicSelectFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default DynamicSelectField;
|
|
9
|
+
//# sourceMappingURL=dynamic-select-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAW7C,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,2CA0GrF;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// DynamicSelectField — async, searchable single-select for declarative forms.
|
|
3
|
+
//
|
|
4
|
+
// This is the declarative answer to "I don't want to type a raw FK UUID".
|
|
5
|
+
// Instead of a plain <select> that dumps every option (RefSelect) or a free
|
|
6
|
+
// text input, it renders a typeahead combobox that queries the canonical
|
|
7
|
+
// options endpoint as the user types:
|
|
8
|
+
//
|
|
9
|
+
// GET /api/options/<ref>?field=id&q=<text>&limit=<n>
|
|
10
|
+
//
|
|
11
|
+
// reusing `useOptionsResolver` (which already debounce-aborts in-flight
|
|
12
|
+
// requests). It is the metacore equivalent of 7leguas' `search.go` / dynamic
|
|
13
|
+
// `type: search` field, but driven entirely from the manifest — so an addon
|
|
14
|
+
// declares `type: "dynamic_select"` + `ref` and gets a searchable picker with
|
|
15
|
+
// zero custom React.
|
|
16
|
+
//
|
|
17
|
+
// Resolution path (highest priority first):
|
|
18
|
+
// 1. field.ref → /options/<ref>?field=id (canonical, preferred)
|
|
19
|
+
// 2. field.searchEndpoint→ used verbatim as the options endpoint (escape hatch)
|
|
20
|
+
//
|
|
21
|
+
// Edit-mode caveat: resolving an EXISTING value's label requires the id to be
|
|
22
|
+
// in a fetched page (we match by id against loaded options, else show the raw
|
|
23
|
+
// value). A dedicated `?ids=` lookup is a follow-up; create flows — the common
|
|
24
|
+
// case — start empty and never hit this.
|
|
25
|
+
import { useEffect, useState } from 'react';
|
|
26
|
+
import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
|
|
27
|
+
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react';
|
|
28
|
+
import { useOptionsResolver } from './use-options-resolver';
|
|
29
|
+
function useDebounced(value, ms) {
|
|
30
|
+
const [debounced, setDebounced] = useState(value);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const t = setTimeout(() => setDebounced(value), ms);
|
|
33
|
+
return () => clearTimeout(t);
|
|
34
|
+
}, [value, ms]);
|
|
35
|
+
return debounced;
|
|
36
|
+
}
|
|
37
|
+
export function DynamicSelectField({ field, value, onChange }) {
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const [search, setSearch] = useState('');
|
|
40
|
+
const debounced = useDebounced(search, 250);
|
|
41
|
+
// Remember the label of the option the user actually picked so the trigger
|
|
42
|
+
// shows a name (not a UUID) without a round-trip.
|
|
43
|
+
const [picked, setPicked] = useState(null);
|
|
44
|
+
const { options, loading } = useOptionsResolver({
|
|
45
|
+
modelKey: '',
|
|
46
|
+
fieldKey: 'id',
|
|
47
|
+
ref: field.ref,
|
|
48
|
+
// searchEndpoint only drives the URL when there's no ref — ref is the
|
|
49
|
+
// canonical, kernel-derived path and wins.
|
|
50
|
+
endpoint: field.ref ? undefined : field.searchEndpoint,
|
|
51
|
+
query: debounced,
|
|
52
|
+
limit: 20,
|
|
53
|
+
// Don't fetch until the popover opens (and keep fetching as the query
|
|
54
|
+
// changes while open).
|
|
55
|
+
enabled: open,
|
|
56
|
+
});
|
|
57
|
+
const selectedLabel = (picked && String(picked.id) === String(value) ? picked.label : null) ??
|
|
58
|
+
options.find((o) => String(o.id) === String(value))?.label ??
|
|
59
|
+
(value ? String(value) : '');
|
|
60
|
+
const handlePick = (opt) => {
|
|
61
|
+
setPicked(opt);
|
|
62
|
+
onChange(String(opt.id));
|
|
63
|
+
setOpen(false);
|
|
64
|
+
setSearch('');
|
|
65
|
+
};
|
|
66
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "w-full justify-between font-normal", "data-empty": !value, children: [_jsx("span", { className: 'truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
|
|
67
|
+
// Match the trigger width without an arbitrary Tailwind class
|
|
68
|
+
// (those don't always survive a consuming app's Tailwind scan).
|
|
69
|
+
style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: field.placeholder || 'Buscar…', value: search, onValueChange: setSearch }), _jsxs(CommandList, { children: [loading && (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm", children: [_jsx(Loader2, { className: "size-4 animate-spin" }), "Buscando\u2026"] })), !loading && options.length === 0 && (_jsx(CommandEmpty, { children: debounced ? 'Sin resultados' : 'Escribí para buscar…' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
|
|
70
|
+
const isSel = String(opt.id) === String(value);
|
|
71
|
+
return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 ' + (isSel ? 'opacity-100' : 'opacity-0') }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] })] }, String(opt.id)));
|
|
72
|
+
}) }))] })] }) })] }));
|
|
73
|
+
}
|
|
74
|
+
export default DynamicSelectField;
|
|
@@ -126,19 +126,15 @@ export declare function useHotSwapReload(client: ManifestHotSwapClient | undefin
|
|
|
126
126
|
*/
|
|
127
127
|
export declare function withVersionParam(url: string, hash: string | undefined): string;
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
129
|
+
* @deprecated Legacy `@originjs/vite-plugin-federation` helper. Under the
|
|
130
|
+
* current `@module-federation/runtime` loader ({@link AddonLoader}), container
|
|
131
|
+
* replacement on hot-swap is handled by `registerRemotes(..., { force: true })`
|
|
132
|
+
* with the new `?v=` URL — there is no `window[scope]` container to delete.
|
|
133
133
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* **Caveat:** some federation runtimes wrap the container in a Proxy
|
|
139
|
-
* whose internal state survives `delete`. If you hit `Container already
|
|
140
|
-
* registered` after calling this, the federation runtime is holding the
|
|
141
|
-
* reference internally and the only reliable swap is `"page-reload"`.
|
|
134
|
+
* Kept for backward compatibility so existing host `onSwap` wiring keeps
|
|
135
|
+
* compiling. Best-effort: removes a stale `window[scope]` if a legacy
|
|
136
|
+
* `@originjs` remote left one behind, otherwise a no-op. Returns `true` if a
|
|
137
|
+
* value was removed, `false` otherwise.
|
|
142
138
|
*/
|
|
143
139
|
export declare function clearFederationContainer(scope: string): boolean;
|
|
144
140
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hotswap-reload-policy.d.ts","sourceRoot":"","sources":["../src/hotswap-reload-policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hotswap-reload-policy.d.ts","sourceRoot":"","sources":["../src/hotswap-reload-policy.ts"],"names":[],"mappings":"AAyDA,OAAO,EAEH,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,EACtC,MAAM,+BAA+B,CAAA;AAEtC;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,aAAa,GAAG,QAAQ,CAAA;AAEtE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAChC,wDAAwD;IACxD,QAAQ,CAAC,EAAE,qBAAqB,CAAA;IAChC;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,CACb,KAAK,EAAE,2BAA2B,KACjC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CACL,KAAK,EAAE,2BAA2B,EAClC,MAAM,EAAE,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,KACvD,IAAI,CAAA;IACT;;;OAGG;IACH,OAAO,CAAC,EAAE,8BAA8B,CAAC,SAAS,CAAC,CAAA;CACtD;AAED,MAAM,WAAW,sBAAsB;IACnC;;;;;;;;OAQG;IACH,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GACzB,OAAO,GACP,aAAa,GACb,WAAW,GACX,QAAQ,GACR,MAAM,CAAA;AAEZ,MAAM,WAAW,iBAAiB;IAC9B,0EAA0E;IAC1E,aAAa,EAAE,CACX,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAChE,IAAI,CAAA;IACT,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACpC,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,iBAAiB,GACxB,OAAO,CAAC,mBAAmB,CAAC,CA6C9B;AAED,wBAAgB,gBAAgB,CAC5B,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,EAChD,MAAM,GAAE,mBAAwB,GACjC,sBAAsB,CA2BxB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAiB9E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAU/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE"}
|
|
@@ -44,21 +44,15 @@
|
|
|
44
44
|
// ## Federation runtime caveat
|
|
45
45
|
//
|
|
46
46
|
// The `"rekey"` strategy re-fetches `remoteEntry.js` with a `?v=<hash8>` query
|
|
47
|
-
// suffix (see {@link withVersionParam}).
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// module's default behaviour. Some federation runtimes (vite-plugin-federation
|
|
57
|
-
// in dev, webpack 5 with `runtime: false`) wrap the container in a `Proxy`
|
|
58
|
-
// that mutates internal state on every access; blindly deleting it from
|
|
59
|
-
// here would race against any unmounting consumer that still holds a
|
|
60
|
-
// reference. Hosts that hit `Container already registered` should call
|
|
61
|
-
// `clearFederationContainer(scope)` from `onSwap` as documented below.
|
|
47
|
+
// suffix (see {@link withVersionParam}). With `@module-federation/runtime`, the
|
|
48
|
+
// {@link AddonLoader} re-registers the remote on every rekey remount via
|
|
49
|
+
// `registerRemotes([{ name, entry: <new ?v= url> }], { force: true })`. The
|
|
50
|
+
// `force` flag overwrites the previously registered container AND wipes that
|
|
51
|
+
// remote's loaded-module cache, so the new code takes effect without any manual
|
|
52
|
+
// `window[scope]` deletion — the old `@originjs` requirement to `delete
|
|
53
|
+
// window[Container]` no longer applies. {@link clearFederationContainer} is kept
|
|
54
|
+
// for backward compatibility (legacy hosts may still call it from `onSwap`) but
|
|
55
|
+
// is a no-op under the MF runtime.
|
|
62
56
|
import { useMemo, useRef, useState } from 'react';
|
|
63
57
|
import { useManifestHotSwapSubscriber, } from './manifest-hotswap-subscriber';
|
|
64
58
|
/**
|
|
@@ -173,19 +167,15 @@ export function withVersionParam(url, hash) {
|
|
|
173
167
|
return `${head}?${parts.join('&')}${fragment}`;
|
|
174
168
|
}
|
|
175
169
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* Best-effort: if `window` is undefined (SSR) or the scope was never
|
|
182
|
-
* registered, this is a no-op. Returns `true` if a container was actually
|
|
183
|
-
* removed, `false` otherwise — useful for telemetry.
|
|
170
|
+
* @deprecated Legacy `@originjs/vite-plugin-federation` helper. Under the
|
|
171
|
+
* current `@module-federation/runtime` loader ({@link AddonLoader}), container
|
|
172
|
+
* replacement on hot-swap is handled by `registerRemotes(..., { force: true })`
|
|
173
|
+
* with the new `?v=` URL — there is no `window[scope]` container to delete.
|
|
184
174
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
175
|
+
* Kept for backward compatibility so existing host `onSwap` wiring keeps
|
|
176
|
+
* compiling. Best-effort: removes a stale `window[scope]` if a legacy
|
|
177
|
+
* `@originjs` remote left one behind, otherwise a no-op. Returns `true` if a
|
|
178
|
+
* value was removed, `false` otherwise.
|
|
189
179
|
*/
|
|
190
180
|
export function clearFederationContainer(scope) {
|
|
191
181
|
if (typeof window === 'undefined')
|
|
@@ -197,9 +187,6 @@ export function clearFederationContainer(scope) {
|
|
|
197
187
|
return true;
|
|
198
188
|
}
|
|
199
189
|
catch {
|
|
200
|
-
// Some browsers refuse to delete non-configurable globals. Set
|
|
201
|
-
// to undefined as a fallback so the loader's `if (!window[scope])`
|
|
202
|
-
// check still triggers a re-inject.
|
|
203
190
|
;
|
|
204
191
|
window[scope] = undefined;
|
|
205
192
|
return true;
|
package/dist/types.d.ts
CHANGED
|
@@ -100,7 +100,7 @@ export interface FieldValidation {
|
|
|
100
100
|
max?: number;
|
|
101
101
|
custom?: string;
|
|
102
102
|
}
|
|
103
|
-
export type FieldWidget = 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'switch';
|
|
103
|
+
export type FieldWidget = 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'dynamic_select' | 'switch';
|
|
104
104
|
export interface ActionFieldDef {
|
|
105
105
|
key: string;
|
|
106
106
|
label: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.3.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@module-federation/runtime": "^2.5.0",
|
|
20
21
|
"zod": "^4.3.0"
|
|
21
22
|
},
|
|
22
23
|
"peerDependencies": {
|
package/src/addon-loader.tsx
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Federated-module addon loader, built on the official Module Federation
|
|
2
|
+
// runtime (`@module-federation/runtime`). Per addon it registers the remote's
|
|
3
|
+
// `remoteEntry.js` as an ESM container, loads the exposed `./register` module,
|
|
4
|
+
// and calls `register(api)` with the AddonAPI injected by the host.
|
|
5
|
+
//
|
|
6
|
+
// Why @module-federation/runtime (not the old manual init/get machinery):
|
|
7
|
+
// the host's Vite build uses `@module-federation/vite`'s `federation()` plugin,
|
|
8
|
+
// which auto-initialises the shared scope at host boot. `registerRemotes` +
|
|
9
|
+
// `loadRemote` then transparently wire the remote into that already-initialised
|
|
10
|
+
// share scope — so the remote consumes the HOST's React/SDK singletons instead
|
|
11
|
+
// of bundling its own. That's the whole point: it fixes the `useState`-null
|
|
12
|
+
// crash WITHOUT this loader ever touching a share scope manually.
|
|
4
13
|
import { useEffect, useRef, useState } from 'react'
|
|
14
|
+
import { registerRemotes, loadRemote } from '@module-federation/runtime'
|
|
5
15
|
import type { AddonAPI, AddonLayout } from '@asteby/metacore-sdk'
|
|
6
16
|
import { useDeclareAddonLayout } from './addon-layout-context'
|
|
7
17
|
|
|
8
|
-
declare global {
|
|
9
|
-
interface Window {
|
|
10
|
-
[key: string]: any
|
|
11
|
-
__webpack_init_sharing__?: (scope: string) => Promise<void>
|
|
12
|
-
__webpack_share_scopes__?: Record<string, unknown>
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
18
|
export interface AddonLoaderProps {
|
|
17
19
|
/** Unique key of the addon — maps to the federation container name. */
|
|
18
20
|
scope: string
|
|
19
|
-
/** URL of the addon's remoteEntry.js bundle. */
|
|
21
|
+
/** URL of the addon's remoteEntry.js bundle (may carry a `?v=` cache-bust). */
|
|
20
22
|
url: string
|
|
21
23
|
/** Exposed module to import from the remote (e.g. './register'). */
|
|
22
24
|
module?: string
|
|
@@ -43,87 +45,47 @@ export interface AddonLoaderProps {
|
|
|
43
45
|
children?: React.ReactNode
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Runtime dynamic import of an external URL. Wrapped in `new Function` so no
|
|
52
|
-
// build tool (tsc here, Vite in the consuming host) tries to statically
|
|
53
|
-
// analyse or rewrite the import — it stays a genuine runtime ESM fetch.
|
|
54
|
-
const importModule = new Function('u', 'return import(u)') as (
|
|
55
|
-
u: string,
|
|
56
|
-
) => Promise<Record<string, unknown>>
|
|
57
|
-
|
|
58
|
-
const loadedScripts = new Map<string, Promise<void>>()
|
|
59
|
-
|
|
60
|
-
function loadScript(url: string, scope: string): Promise<void> {
|
|
61
|
-
const key = `${scope}::${url}`
|
|
62
|
-
const existing = loadedScripts.get(key)
|
|
63
|
-
if (existing) return existing
|
|
64
|
-
const promise = new Promise<void>((resolve, reject) => {
|
|
65
|
-
const el = document.createElement('script')
|
|
66
|
-
el.src = url
|
|
67
|
-
el.type = 'text/javascript'
|
|
68
|
-
el.async = true
|
|
69
|
-
el.onload = () => resolve()
|
|
70
|
-
el.onerror = () => reject(new Error(`Failed to load addon script: ${url}`))
|
|
71
|
-
document.head.appendChild(el)
|
|
72
|
-
})
|
|
73
|
-
loadedScripts.set(key, promise)
|
|
74
|
-
return promise
|
|
48
|
+
/** Shape of the exposed `./register` module. */
|
|
49
|
+
interface AddonRegisterModule {
|
|
50
|
+
register?: (api: AddonAPI) => void | Promise<void>
|
|
51
|
+
default?: (api: AddonAPI) => void | Promise<void>
|
|
75
52
|
}
|
|
76
53
|
|
|
77
|
-
|
|
54
|
+
// `registerRemotes` is additive + idempotent across re-mounts; we still track
|
|
55
|
+
// which scopes we've registered to avoid redundant `force` churn (each `force`
|
|
56
|
+
// re-register wipes that remote's module cache and logs a runtime warning).
|
|
57
|
+
const registered = new Set<string>()
|
|
78
58
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// still supported via the classic <script> fallback.
|
|
86
|
-
async function resolveContainer(scope: string, url: string): Promise<FederationContainer | undefined> {
|
|
87
|
-
const key = `${scope}::${url}`
|
|
88
|
-
const cached = esmContainers.get(key)
|
|
89
|
-
if (cached) return cached
|
|
90
|
-
const p = (async () => {
|
|
91
|
-
try {
|
|
92
|
-
const mod = await importModule(url)
|
|
93
|
-
if (mod && typeof mod.init === 'function' && typeof mod.get === 'function') {
|
|
94
|
-
return mod as unknown as FederationContainer
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
// Not an importable module (legacy var-format remote) — fall back.
|
|
98
|
-
}
|
|
99
|
-
await loadScript(url, scope)
|
|
100
|
-
return (window as any)[scope] as FederationContainer | undefined
|
|
101
|
-
})()
|
|
102
|
-
esmContainers.set(key, p)
|
|
103
|
-
p.catch(() => esmContainers.delete(key))
|
|
104
|
-
return p
|
|
59
|
+
// Derive the `loadRemote` id from the scope + exposed module name. MF resolves
|
|
60
|
+
// `"<remoteName>/<expose>"` — e.g. `metacore_tickets/register` for the
|
|
61
|
+
// `"./register"` expose. We strip the leading `./` of the expose path.
|
|
62
|
+
function remoteId(scope: string, module: string): string {
|
|
63
|
+
const expose = module.replace(/^\.\//, '')
|
|
64
|
+
return `${scope}/${expose}`
|
|
105
65
|
}
|
|
106
66
|
|
|
107
|
-
async function
|
|
108
|
-
|
|
109
|
-
|
|
67
|
+
async function loadAddon(
|
|
68
|
+
scope: string,
|
|
69
|
+
url: string,
|
|
70
|
+
module: string,
|
|
71
|
+
): Promise<AddonRegisterModule | null> {
|
|
72
|
+
// Register the remote container as an ES module. `type: 'module'` matches
|
|
73
|
+
// the `@module-federation/vite` remote (remoteEntry.js is an ESM bundle).
|
|
74
|
+
// The `url` already carries the `?v=` cache-bust the host computed, so the
|
|
75
|
+
// browser refetches a fresh remoteEntry when the addon version changes.
|
|
76
|
+
if (!registered.has(scope)) {
|
|
77
|
+
registerRemotes(
|
|
78
|
+
[{ name: scope, entry: url, type: 'module' }],
|
|
79
|
+
// `force: true` so a re-registration with a new `?v=` URL (addon
|
|
80
|
+
// hot-swap / version bump) overwrites the stale entry + cache.
|
|
81
|
+
{ force: true },
|
|
82
|
+
)
|
|
83
|
+
registered.add(scope)
|
|
110
84
|
}
|
|
111
|
-
|
|
112
|
-
if
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (typeof container.init === 'function') {
|
|
116
|
-
const shareScope =
|
|
117
|
-
window.__webpack_share_scopes__?.default ??
|
|
118
|
-
((window as any).__METACORE_SHARE_SCOPE__ ??= {})
|
|
119
|
-
try {
|
|
120
|
-
await container.init(shareScope)
|
|
121
|
-
} catch {
|
|
122
|
-
// Container already initialized (re-entrant load) — safe to ignore.
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const factory = await container.get(module)
|
|
126
|
-
return factory()
|
|
85
|
+
// loadRemote("<scope>/<expose>") returns the exposed module namespace (or
|
|
86
|
+
// null if it can't be resolved). No manual share-scope init — the host's
|
|
87
|
+
// federation runtime already initialised it.
|
|
88
|
+
return loadRemote<AddonRegisterModule>(remoteId(scope, module))
|
|
127
89
|
}
|
|
128
90
|
|
|
129
91
|
export function AddonLoader({
|
|
@@ -151,25 +113,35 @@ export function AddonLoader({
|
|
|
151
113
|
let cancelled = false
|
|
152
114
|
;(async () => {
|
|
153
115
|
try {
|
|
154
|
-
const mod = await
|
|
116
|
+
const mod = await loadAddon(scope, url, module)
|
|
155
117
|
if (cancelled) return
|
|
156
|
-
|
|
118
|
+
const register = mod?.register ?? mod?.default
|
|
119
|
+
if (typeof register !== 'function') {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Addon "${scope}" module "${module}" has no register() export`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
if (!didRegister.current) {
|
|
157
125
|
didRegister.current = true
|
|
158
|
-
await Promise.resolve(
|
|
126
|
+
await Promise.resolve(register(api))
|
|
159
127
|
}
|
|
160
128
|
setStatus('ready')
|
|
161
129
|
onReady?.()
|
|
162
|
-
} catch (e:
|
|
130
|
+
} catch (e: unknown) {
|
|
163
131
|
if (cancelled) return
|
|
164
|
-
|
|
132
|
+
const err = e instanceof Error ? e : new Error(String(e))
|
|
133
|
+
setError(err)
|
|
165
134
|
setStatus('error')
|
|
166
|
-
onError?.(
|
|
135
|
+
onError?.(err)
|
|
167
136
|
}
|
|
168
137
|
})()
|
|
169
|
-
return () => {
|
|
138
|
+
return () => {
|
|
139
|
+
cancelled = true
|
|
140
|
+
}
|
|
170
141
|
}, [scope, url, module])
|
|
171
142
|
|
|
172
143
|
if (status === 'loading') return <>{fallback}</>
|
|
173
|
-
if (status === 'error')
|
|
144
|
+
if (status === 'error')
|
|
145
|
+
return <div className="text-sm text-red-500">Addon load error: {error?.message}</div>
|
|
174
146
|
return <>{children}</>
|
|
175
147
|
}
|
|
@@ -119,6 +119,9 @@ export function resolveWidget(field: ActionFieldDef): string {
|
|
|
119
119
|
switch (field.type) {
|
|
120
120
|
case 'textarea': return 'textarea'
|
|
121
121
|
case 'select': return 'select'
|
|
122
|
+
// Async searchable single-select against /api/options/<ref>. The
|
|
123
|
+
// declarative replacement for typing a raw FK UUID.
|
|
124
|
+
case 'dynamic_select': return 'dynamic_select'
|
|
122
125
|
case 'boolean': return 'switch'
|
|
123
126
|
case 'number': return 'number'
|
|
124
127
|
case 'date': return 'date'
|
package/src/dynamic-form.tsx
CHANGED
|
@@ -18,9 +18,11 @@ import type { ActionFieldDef } from './types'
|
|
|
18
18
|
import { buildZodSchema, resolveWidget, isLineItemsField } from './dynamic-form-schema'
|
|
19
19
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
20
20
|
import { DynamicLineItems } from './dynamic-line-items'
|
|
21
|
+
import { DynamicSelectField } from './dynamic-select-field'
|
|
21
22
|
|
|
22
23
|
export { buildZodSchema, resolveWidget }
|
|
23
24
|
export { DynamicLineItems } from './dynamic-line-items'
|
|
25
|
+
export { DynamicSelectField } from './dynamic-select-field'
|
|
24
26
|
|
|
25
27
|
export interface DynamicFormProps {
|
|
26
28
|
fields: ActionFieldDef[]
|
|
@@ -123,6 +125,12 @@ function FieldRenderer({ field, value, onChange }: FieldRendererProps) {
|
|
|
123
125
|
return <DynamicLineItems field={field} value={value} onChange={onChange} />
|
|
124
126
|
}
|
|
125
127
|
const widget = resolveWidget(field)
|
|
128
|
+
// Async searchable picker (typeahead against /api/options/<ref>?q=…).
|
|
129
|
+
// Preferred for FK fields with large option sets — no UUID typing, no
|
|
130
|
+
// dumping every row into a plain <select>.
|
|
131
|
+
if (widget === 'dynamic_select') {
|
|
132
|
+
return <DynamicSelectField field={field} value={value} onChange={onChange} />
|
|
133
|
+
}
|
|
126
134
|
// Ref-driven select: hook into useOptionsResolver so the canonical
|
|
127
135
|
// /api/options/<ref>?field=id endpoint feeds the dropdown. This is
|
|
128
136
|
// the path the kernel auto-derives for FK columns; legacy callers
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { Plus, Trash2 } from 'lucide-react'
|
|
22
22
|
import type { ActionFieldDef } from './types'
|
|
23
23
|
import { resolveWidget, getItemFields } from './dynamic-form-schema'
|
|
24
|
+
import { DynamicSelectField } from './dynamic-select-field'
|
|
24
25
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
25
26
|
|
|
26
27
|
export interface DynamicLineItemsProps {
|
|
@@ -125,6 +126,11 @@ interface CellRendererProps {
|
|
|
125
126
|
// a scalar widget).
|
|
126
127
|
function CellRenderer({ field, value, onChange, disabled }: CellRendererProps) {
|
|
127
128
|
const widget = resolveWidget(field)
|
|
129
|
+
// Async searchable picker per row cell — e.g. the account_id column of a
|
|
130
|
+
// journal entry's debit/credit lines. Same widget as the flat form.
|
|
131
|
+
if (widget === 'dynamic_select') {
|
|
132
|
+
return <DynamicSelectField field={field} value={value} onChange={onChange} />
|
|
133
|
+
}
|
|
128
134
|
if (widget === 'select' && field.ref) {
|
|
129
135
|
return <RefCell field={field} value={value} onChange={onChange} disabled={disabled} />
|
|
130
136
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// DynamicSelectField — async, searchable single-select for declarative forms.
|
|
2
|
+
//
|
|
3
|
+
// This is the declarative answer to "I don't want to type a raw FK UUID".
|
|
4
|
+
// Instead of a plain <select> that dumps every option (RefSelect) or a free
|
|
5
|
+
// text input, it renders a typeahead combobox that queries the canonical
|
|
6
|
+
// options endpoint as the user types:
|
|
7
|
+
//
|
|
8
|
+
// GET /api/options/<ref>?field=id&q=<text>&limit=<n>
|
|
9
|
+
//
|
|
10
|
+
// reusing `useOptionsResolver` (which already debounce-aborts in-flight
|
|
11
|
+
// requests). It is the metacore equivalent of 7leguas' `search.go` / dynamic
|
|
12
|
+
// `type: search` field, but driven entirely from the manifest — so an addon
|
|
13
|
+
// declares `type: "dynamic_select"` + `ref` and gets a searchable picker with
|
|
14
|
+
// zero custom React.
|
|
15
|
+
//
|
|
16
|
+
// Resolution path (highest priority first):
|
|
17
|
+
// 1. field.ref → /options/<ref>?field=id (canonical, preferred)
|
|
18
|
+
// 2. field.searchEndpoint→ used verbatim as the options endpoint (escape hatch)
|
|
19
|
+
//
|
|
20
|
+
// Edit-mode caveat: resolving an EXISTING value's label requires the id to be
|
|
21
|
+
// in a fetched page (we match by id against loaded options, else show the raw
|
|
22
|
+
// value). A dedicated `?ids=` lookup is a follow-up; create flows — the common
|
|
23
|
+
// case — start empty and never hit this.
|
|
24
|
+
import { useEffect, useState } from 'react'
|
|
25
|
+
import {
|
|
26
|
+
Button,
|
|
27
|
+
Command,
|
|
28
|
+
CommandEmpty,
|
|
29
|
+
CommandGroup,
|
|
30
|
+
CommandInput,
|
|
31
|
+
CommandItem,
|
|
32
|
+
CommandList,
|
|
33
|
+
Popover,
|
|
34
|
+
PopoverContent,
|
|
35
|
+
PopoverTrigger,
|
|
36
|
+
} from '@asteby/metacore-ui/primitives'
|
|
37
|
+
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
|
|
38
|
+
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
39
|
+
import type { ActionFieldDef } from './types'
|
|
40
|
+
|
|
41
|
+
function useDebounced<T>(value: T, ms: number): T {
|
|
42
|
+
const [debounced, setDebounced] = useState(value)
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const t = setTimeout(() => setDebounced(value), ms)
|
|
45
|
+
return () => clearTimeout(t)
|
|
46
|
+
}, [value, ms])
|
|
47
|
+
return debounced
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DynamicSelectFieldProps {
|
|
51
|
+
field: ActionFieldDef
|
|
52
|
+
value: any
|
|
53
|
+
onChange: (v: any) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function DynamicSelectField({ field, value, onChange }: DynamicSelectFieldProps) {
|
|
57
|
+
const [open, setOpen] = useState(false)
|
|
58
|
+
const [search, setSearch] = useState('')
|
|
59
|
+
const debounced = useDebounced(search, 250)
|
|
60
|
+
// Remember the label of the option the user actually picked so the trigger
|
|
61
|
+
// shows a name (not a UUID) without a round-trip.
|
|
62
|
+
const [picked, setPicked] = useState<ResolvedOption | null>(null)
|
|
63
|
+
|
|
64
|
+
const { options, loading } = useOptionsResolver({
|
|
65
|
+
modelKey: '',
|
|
66
|
+
fieldKey: 'id',
|
|
67
|
+
ref: field.ref,
|
|
68
|
+
// searchEndpoint only drives the URL when there's no ref — ref is the
|
|
69
|
+
// canonical, kernel-derived path and wins.
|
|
70
|
+
endpoint: field.ref ? undefined : field.searchEndpoint,
|
|
71
|
+
query: debounced,
|
|
72
|
+
limit: 20,
|
|
73
|
+
// Don't fetch until the popover opens (and keep fetching as the query
|
|
74
|
+
// changes while open).
|
|
75
|
+
enabled: open,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const selectedLabel =
|
|
79
|
+
(picked && String(picked.id) === String(value) ? picked.label : null) ??
|
|
80
|
+
options.find((o) => String(o.id) === String(value))?.label ??
|
|
81
|
+
(value ? String(value) : '')
|
|
82
|
+
|
|
83
|
+
const handlePick = (opt: ResolvedOption) => {
|
|
84
|
+
setPicked(opt)
|
|
85
|
+
onChange(String(opt.id))
|
|
86
|
+
setOpen(false)
|
|
87
|
+
setSearch('')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
92
|
+
<PopoverTrigger asChild>
|
|
93
|
+
<Button
|
|
94
|
+
type="button"
|
|
95
|
+
variant="outline"
|
|
96
|
+
role="combobox"
|
|
97
|
+
aria-expanded={open}
|
|
98
|
+
id={field.key}
|
|
99
|
+
className="w-full justify-between font-normal"
|
|
100
|
+
data-empty={!value}
|
|
101
|
+
>
|
|
102
|
+
<span className={'truncate ' + (selectedLabel ? '' : 'text-muted-foreground')}>
|
|
103
|
+
{selectedLabel || field.placeholder || 'Buscar…'}
|
|
104
|
+
</span>
|
|
105
|
+
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
|
|
106
|
+
</Button>
|
|
107
|
+
</PopoverTrigger>
|
|
108
|
+
<PopoverContent
|
|
109
|
+
className="p-0"
|
|
110
|
+
align="start"
|
|
111
|
+
// Match the trigger width without an arbitrary Tailwind class
|
|
112
|
+
// (those don't always survive a consuming app's Tailwind scan).
|
|
113
|
+
style={{ width: 'var(--radix-popover-trigger-width)' }}
|
|
114
|
+
>
|
|
115
|
+
<Command shouldFilter={false}>
|
|
116
|
+
<CommandInput
|
|
117
|
+
placeholder={field.placeholder || 'Buscar…'}
|
|
118
|
+
value={search}
|
|
119
|
+
onValueChange={setSearch}
|
|
120
|
+
/>
|
|
121
|
+
<CommandList>
|
|
122
|
+
{loading && (
|
|
123
|
+
<div className="text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm">
|
|
124
|
+
<Loader2 className="size-4 animate-spin" />
|
|
125
|
+
Buscando…
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
{!loading && options.length === 0 && (
|
|
129
|
+
<CommandEmpty>
|
|
130
|
+
{debounced ? 'Sin resultados' : 'Escribí para buscar…'}
|
|
131
|
+
</CommandEmpty>
|
|
132
|
+
)}
|
|
133
|
+
{!loading && options.length > 0 && (
|
|
134
|
+
<CommandGroup className="max-h-64 overflow-auto">
|
|
135
|
+
{options.map((opt) => {
|
|
136
|
+
const isSel = String(opt.id) === String(value)
|
|
137
|
+
return (
|
|
138
|
+
<CommandItem
|
|
139
|
+
key={String(opt.id)}
|
|
140
|
+
value={String(opt.id)}
|
|
141
|
+
onSelect={() => handlePick(opt)}
|
|
142
|
+
>
|
|
143
|
+
<Check className={'mr-2 size-4 ' + (isSel ? 'opacity-100' : 'opacity-0')} />
|
|
144
|
+
<div className="flex min-w-0 flex-col">
|
|
145
|
+
<span className="truncate">{opt.label}</span>
|
|
146
|
+
{opt.description && (
|
|
147
|
+
<span className="text-muted-foreground truncate text-xs">
|
|
148
|
+
{opt.description}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</CommandItem>
|
|
153
|
+
)
|
|
154
|
+
})}
|
|
155
|
+
</CommandGroup>
|
|
156
|
+
)}
|
|
157
|
+
</CommandList>
|
|
158
|
+
</Command>
|
|
159
|
+
</PopoverContent>
|
|
160
|
+
</Popover>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default DynamicSelectField
|
|
@@ -44,21 +44,15 @@
|
|
|
44
44
|
// ## Federation runtime caveat
|
|
45
45
|
//
|
|
46
46
|
// The `"rekey"` strategy re-fetches `remoteEntry.js` with a `?v=<hash8>` query
|
|
47
|
-
// suffix (see {@link withVersionParam}).
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// module's default behaviour. Some federation runtimes (vite-plugin-federation
|
|
57
|
-
// in dev, webpack 5 with `runtime: false`) wrap the container in a `Proxy`
|
|
58
|
-
// that mutates internal state on every access; blindly deleting it from
|
|
59
|
-
// here would race against any unmounting consumer that still holds a
|
|
60
|
-
// reference. Hosts that hit `Container already registered` should call
|
|
61
|
-
// `clearFederationContainer(scope)` from `onSwap` as documented below.
|
|
47
|
+
// suffix (see {@link withVersionParam}). With `@module-federation/runtime`, the
|
|
48
|
+
// {@link AddonLoader} re-registers the remote on every rekey remount via
|
|
49
|
+
// `registerRemotes([{ name, entry: <new ?v= url> }], { force: true })`. The
|
|
50
|
+
// `force` flag overwrites the previously registered container AND wipes that
|
|
51
|
+
// remote's loaded-module cache, so the new code takes effect without any manual
|
|
52
|
+
// `window[scope]` deletion — the old `@originjs` requirement to `delete
|
|
53
|
+
// window[Container]` no longer applies. {@link clearFederationContainer} is kept
|
|
54
|
+
// for backward compatibility (legacy hosts may still call it from `onSwap`) but
|
|
55
|
+
// is a no-op under the MF runtime.
|
|
62
56
|
|
|
63
57
|
import { useMemo, useRef, useState } from 'react'
|
|
64
58
|
import {
|
|
@@ -311,30 +305,23 @@ export function withVersionParam(url: string, hash: string | undefined): string
|
|
|
311
305
|
}
|
|
312
306
|
|
|
313
307
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* Best-effort: if `window` is undefined (SSR) or the scope was never
|
|
320
|
-
* registered, this is a no-op. Returns `true` if a container was actually
|
|
321
|
-
* removed, `false` otherwise — useful for telemetry.
|
|
308
|
+
* @deprecated Legacy `@originjs/vite-plugin-federation` helper. Under the
|
|
309
|
+
* current `@module-federation/runtime` loader ({@link AddonLoader}), container
|
|
310
|
+
* replacement on hot-swap is handled by `registerRemotes(..., { force: true })`
|
|
311
|
+
* with the new `?v=` URL — there is no `window[scope]` container to delete.
|
|
322
312
|
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
313
|
+
* Kept for backward compatibility so existing host `onSwap` wiring keeps
|
|
314
|
+
* compiling. Best-effort: removes a stale `window[scope]` if a legacy
|
|
315
|
+
* `@originjs` remote left one behind, otherwise a no-op. Returns `true` if a
|
|
316
|
+
* value was removed, `false` otherwise.
|
|
327
317
|
*/
|
|
328
318
|
export function clearFederationContainer(scope: string): boolean {
|
|
329
319
|
if (typeof window === 'undefined') return false
|
|
330
|
-
if (!(scope in window)) return false
|
|
320
|
+
if (!(scope in (window as Record<string, unknown>))) return false
|
|
331
321
|
try {
|
|
332
322
|
delete (window as Record<string, unknown>)[scope]
|
|
333
323
|
return true
|
|
334
324
|
} catch {
|
|
335
|
-
// Some browsers refuse to delete non-configurable globals. Set
|
|
336
|
-
// to undefined as a fallback so the loader's `if (!window[scope])`
|
|
337
|
-
// check still triggers a re-inject.
|
|
338
325
|
;(window as Record<string, unknown>)[scope] = undefined
|
|
339
326
|
return true
|
|
340
327
|
}
|