@asteby/metacore-runtime-react 23.11.1 → 23.12.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 +12 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +12 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/use-addon-settings.d.ts +56 -0
- package/dist/use-addon-settings.d.ts.map +1 -0
- package/dist/use-addon-settings.js +109 -0
- package/package.json +1 -1
- package/src/__tests__/use-addon-settings.test.ts +74 -0
- package/src/dynamic-form.tsx +16 -6
- package/src/index.ts +1 -0
- package/src/use-addon-settings.ts +152 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9376722: Add `useAddonSettings` / `useUpdateAddonSettings` — the standard primitive for reading an addon's per-organization configuration from a federated addon, backed by react-query and the host-injected api client. Merges caller-provided defaults (mirroring the manifest's `settings[].default`) under the saved org values, so a never-saved setting falls back to its default. Also exports the pure `mergeAddonSettings` helper and the stable `addonSettingsKey` query key.
|
|
8
|
+
|
|
9
|
+
## 23.11.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 990bb63: DynamicForm oculta los campos readonly (estado de conexión, valores escritos por sync): el servidor los posee, el form no los captura ni los envía en el submit.
|
|
14
|
+
|
|
3
15
|
## 23.11.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -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,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;AAO9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,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,+
|
|
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,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;AAO9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,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,+BA8FlB"}
|
package/dist/dynamic-form.js
CHANGED
|
@@ -19,22 +19,28 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
|
|
|
19
19
|
const [values, setValues] = useState({});
|
|
20
20
|
const [errors, setErrors] = useState({});
|
|
21
21
|
const [submitting, setSubmitting] = useState(false);
|
|
22
|
-
|
|
22
|
+
// Readonly fields are system-owned (connection state, sync-written values):
|
|
23
|
+
// the server writes them, so the form never captures them — hidden outright
|
|
24
|
+
// instead of rendered disabled, matching the metadata contract
|
|
25
|
+
// (ColumnDef.Readonly). They are also excluded from defaults/submit so a
|
|
26
|
+
// create never posts e.g. `status: ""` over the column default.
|
|
27
|
+
const editableFields = useMemo(() => fields.filter((f) => !f.readonly), [fields]);
|
|
28
|
+
const schema = useMemo(() => buildZodSchema(editableFields), [editableFields]);
|
|
23
29
|
// Line-items fields carrying a balance rule gate submit: an unbalanced entry
|
|
24
30
|
// (Σdebit ≠ Σcredit, or all-zero when require_nonzero) can't be saved. This
|
|
25
31
|
// is fully declarative — `evaluateBalance` returns undefined for fields with
|
|
26
32
|
// no rule, so non-balanced forms are unaffected.
|
|
27
33
|
const balanceBlocked = useMemo(() => {
|
|
28
|
-
for (const f of
|
|
34
|
+
for (const f of editableFields) {
|
|
29
35
|
const state = evaluateBalance(f, values[f.key]);
|
|
30
36
|
if (state && !state.balanced)
|
|
31
37
|
return true;
|
|
32
38
|
}
|
|
33
39
|
return false;
|
|
34
|
-
}, [
|
|
40
|
+
}, [editableFields, values]);
|
|
35
41
|
useEffect(() => {
|
|
36
42
|
const defaults = {};
|
|
37
|
-
for (const f of
|
|
43
|
+
for (const f of editableFields) {
|
|
38
44
|
if (isLineItemsField(f)) {
|
|
39
45
|
defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? [];
|
|
40
46
|
continue;
|
|
@@ -43,7 +49,7 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
|
|
|
43
49
|
}
|
|
44
50
|
setValues(defaults);
|
|
45
51
|
setErrors({});
|
|
46
|
-
}, [
|
|
52
|
+
}, [editableFields, initialValues]);
|
|
47
53
|
const update = (k, v) => setValues((prev) => ({ ...prev, [k]: v }));
|
|
48
54
|
const handleSubmit = async (e) => {
|
|
49
55
|
e.preventDefault();
|
|
@@ -73,7 +79,7 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
|
|
|
73
79
|
// line-items grids (and textareas) span the full width so the row table /
|
|
74
80
|
// memo gets room. Mirrors the pro look of the federated journal modal but
|
|
75
81
|
// stays fully declarative — driven only by field shape.
|
|
76
|
-
return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children:
|
|
82
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: editableFields.map((field) => (_jsx(FieldRow, { field: field, value: values[field.key], onChange: (v) => update(field.key, v), values: values, error: errors[field.key], initialValues: initialValues }, 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 || balanceBlocked, children: submitLabel })] })] }));
|
|
77
83
|
}
|
|
78
84
|
// One form field row: label + renderer + inline error. Encapsulated as its own
|
|
79
85
|
// component so a STATIC enum select whose options are all gated out by a sibling
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from './org-runtime-provider';
|
|
|
25
25
|
export * from './navigation-builder';
|
|
26
26
|
export * from './i18n-provider';
|
|
27
27
|
export * from './api-context';
|
|
28
|
+
export * from './use-addon-settings';
|
|
28
29
|
export * from './metadata-cache';
|
|
29
30
|
export { ADDON_MANIFEST_CHANGED_TYPE, wireHotSwapInvalidation, useManifestHotSwapSubscriber, type AddonManifestChangedMessage, type ManifestHotSwapClient, type WireHotSwapInvalidationOptions, } from './manifest-hotswap-subscriber';
|
|
30
31
|
export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederationContainer, shortenHash, type HotSwapReloadStrategy, type HotSwapReloadConfig, type HotSwapReloadAction, type HotSwapReloadDeps, type UseHotSwapReloadResult, } from './hotswap-reload-policy';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export * from './org-runtime-provider';
|
|
|
30
30
|
export * from './navigation-builder';
|
|
31
31
|
export * from './i18n-provider';
|
|
32
32
|
export * from './api-context';
|
|
33
|
+
export * from './use-addon-settings';
|
|
33
34
|
export * from './metadata-cache';
|
|
34
35
|
export { ADDON_MANIFEST_CHANGED_TYPE, wireHotSwapInvalidation, useManifestHotSwapSubscriber, } from './manifest-hotswap-subscriber';
|
|
35
36
|
export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederationContainer, shortenHash, } from './hotswap-reload-policy';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Stable react-query key for an addon's per-org settings. */
|
|
2
|
+
export declare function addonSettingsKey(addonKey: string): readonly ["addon-settings", string];
|
|
3
|
+
/**
|
|
4
|
+
* Merge saved org values over the caller's defaults so a never-saved setting
|
|
5
|
+
* falls back to its default. Pure + exported for unit testing. Only keys that
|
|
6
|
+
* are actually present in `stored` win — an explicit `undefined` in the stored
|
|
7
|
+
* payload is treated as "not saved" and yields the default (the host omits
|
|
8
|
+
* unsaved keys, but this keeps a sparse payload well-behaved).
|
|
9
|
+
*/
|
|
10
|
+
export declare function mergeAddonSettings<T extends Record<string, unknown>>(defaults: Partial<T> | undefined, stored: Partial<T> | null | undefined): T;
|
|
11
|
+
export interface UseAddonSettingsOptions<T> {
|
|
12
|
+
/**
|
|
13
|
+
* Fallback values (typically the addon manifest's `settings[].default`)
|
|
14
|
+
* merged UNDER the saved org values. A setting the org hasn't saved
|
|
15
|
+
* resolves to its default here.
|
|
16
|
+
*/
|
|
17
|
+
defaults?: Partial<T>;
|
|
18
|
+
/** Override staleTime (default 30s — org config changes rarely). */
|
|
19
|
+
staleTime?: number;
|
|
20
|
+
/** Defer fetching (e.g. until the addon key is known). */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface UseAddonSettingsResult<T> {
|
|
24
|
+
/** Saved org values merged over `defaults`. Never null. */
|
|
25
|
+
settings: T;
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
error: unknown;
|
|
28
|
+
refetch: () => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read an addon's per-organization settings.
|
|
32
|
+
*
|
|
33
|
+
* This is the ecosystem-standard way for a federated addon to read its own
|
|
34
|
+
* config; prefer it over a hand-rolled fetch so every addon caches, merges
|
|
35
|
+
* defaults, and invalidates identically.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* type PosSettings = { allowNegativeStock: boolean; roundingMode: string }
|
|
39
|
+
* const { settings, isLoading } = useAddonSettings<PosSettings>('pos', {
|
|
40
|
+
* defaults: { allowNegativeStock: false, roundingMode: 'nearest' },
|
|
41
|
+
* })
|
|
42
|
+
* if (!isLoading && settings.allowNegativeStock) { ... }
|
|
43
|
+
*/
|
|
44
|
+
export declare function useAddonSettings<T extends Record<string, unknown> = Record<string, unknown>>(addonKey: string, opts?: UseAddonSettingsOptions<T>): UseAddonSettingsResult<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Optional write companion — PUTs a (partial) settings object and invalidates
|
|
47
|
+
* the read query so `useAddonSettings` re-resolves. The host's generic config
|
|
48
|
+
* modal does its own PUT; this exists for addons that ship a custom config
|
|
49
|
+
* surface. Backward-compatible: purely additive.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const update = useUpdateAddonSettings('pos')
|
|
53
|
+
* update.mutate({ allowNegativeStock: true })
|
|
54
|
+
*/
|
|
55
|
+
export declare function useUpdateAddonSettings<T extends Record<string, unknown> = Record<string, unknown>>(addonKey: string): import("@tanstack/react-query").UseMutationResult<Partial<T>, Error, Partial<T>, unknown>;
|
|
56
|
+
//# sourceMappingURL=use-addon-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-addon-settings.d.ts","sourceRoot":"","sources":["../src/use-addon-settings.ts"],"names":[],"mappings":"AA6BA,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,uCAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,EAChC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,GACtC,CAAC,CAQH;AAED,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACrB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACrC,2DAA2D;IAC3D,QAAQ,EAAE,CAAC,CAAA;IACX,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxF,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,uBAAuB,CAAC,CAAC,CAAM,GACtC,sBAAsB,CAAC,CAAC,CAAC,CA8B3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9F,QAAQ,EAAE,MAAM,6FAgBnB"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// useAddonSettings — THE standard primitive for reading an addon's
|
|
2
|
+
// per-organization configuration from a federated addon (or any host
|
|
3
|
+
// surface) without every addon re-implementing its own fetch.
|
|
4
|
+
//
|
|
5
|
+
// The host (ops) persists settings per org on `AddonInstallation.settings`
|
|
6
|
+
// (jsonb) and exposes:
|
|
7
|
+
// GET /api/addons/:key/settings → { success, data: { <key>: <value> } }
|
|
8
|
+
// PUT /api/addons/:key/settings → body = object of values
|
|
9
|
+
// A setting the org has never saved simply isn't present in `data`; the
|
|
10
|
+
// caller supplies the fallback via `opts.defaults` (which mirror the
|
|
11
|
+
// manifest's `settings[].default`). runtime-react has no access to the
|
|
12
|
+
// addon manifest at runtime, so defaults are a parameter, not automatic.
|
|
13
|
+
//
|
|
14
|
+
// react-query is a PEER dependency of runtime-react: `useQuery`/`useMutation`
|
|
15
|
+
// resolve to the HOST's singleton QueryClient (the MF shared singleton), so
|
|
16
|
+
// this hook never constructs its own client and stays cache-coherent with
|
|
17
|
+
// the rest of the app. Do not add a QueryClientProvider here.
|
|
18
|
+
//
|
|
19
|
+
// Server-side gating (wasm/Go backends) is out of scope here: a backend that
|
|
20
|
+
// must gate behaviour on org config reads the same `AddonInstallation.settings`
|
|
21
|
+
// jsonb directly — this hook is the client-read half of that contract.
|
|
22
|
+
import { useMemo } from 'react';
|
|
23
|
+
import { useMutation, useQuery, useQueryClient, } from '@tanstack/react-query';
|
|
24
|
+
import { useApi } from './api-context';
|
|
25
|
+
/** Stable react-query key for an addon's per-org settings. */
|
|
26
|
+
export function addonSettingsKey(addonKey) {
|
|
27
|
+
return ['addon-settings', addonKey];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Merge saved org values over the caller's defaults so a never-saved setting
|
|
31
|
+
* falls back to its default. Pure + exported for unit testing. Only keys that
|
|
32
|
+
* are actually present in `stored` win — an explicit `undefined` in the stored
|
|
33
|
+
* payload is treated as "not saved" and yields the default (the host omits
|
|
34
|
+
* unsaved keys, but this keeps a sparse payload well-behaved).
|
|
35
|
+
*/
|
|
36
|
+
export function mergeAddonSettings(defaults, stored) {
|
|
37
|
+
const out = { ...(defaults ?? {}) };
|
|
38
|
+
if (stored) {
|
|
39
|
+
for (const [k, v] of Object.entries(stored)) {
|
|
40
|
+
if (v !== undefined)
|
|
41
|
+
out[k] = v;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read an addon's per-organization settings.
|
|
48
|
+
*
|
|
49
|
+
* This is the ecosystem-standard way for a federated addon to read its own
|
|
50
|
+
* config; prefer it over a hand-rolled fetch so every addon caches, merges
|
|
51
|
+
* defaults, and invalidates identically.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* type PosSettings = { allowNegativeStock: boolean; roundingMode: string }
|
|
55
|
+
* const { settings, isLoading } = useAddonSettings<PosSettings>('pos', {
|
|
56
|
+
* defaults: { allowNegativeStock: false, roundingMode: 'nearest' },
|
|
57
|
+
* })
|
|
58
|
+
* if (!isLoading && settings.allowNegativeStock) { ... }
|
|
59
|
+
*/
|
|
60
|
+
export function useAddonSettings(addonKey, opts = {}) {
|
|
61
|
+
const api = useApi();
|
|
62
|
+
const { defaults, staleTime, enabled } = opts;
|
|
63
|
+
const query = useQuery({
|
|
64
|
+
queryKey: addonSettingsKey(addonKey),
|
|
65
|
+
queryFn: async () => {
|
|
66
|
+
const res = await api.get(`/api/addons/${addonKey}/settings`);
|
|
67
|
+
const body = res.data;
|
|
68
|
+
// Host envelope: { success, data: { <key>: <value> } }.
|
|
69
|
+
const values = body?.success ? body.data : body;
|
|
70
|
+
return (values ?? {});
|
|
71
|
+
},
|
|
72
|
+
staleTime: staleTime ?? 30_000,
|
|
73
|
+
enabled: (enabled ?? true) && !!addonKey,
|
|
74
|
+
});
|
|
75
|
+
const settings = useMemo(() => mergeAddonSettings(defaults, query.data), [defaults, query.data]);
|
|
76
|
+
return {
|
|
77
|
+
settings,
|
|
78
|
+
isLoading: query.isLoading,
|
|
79
|
+
error: query.error,
|
|
80
|
+
refetch: () => {
|
|
81
|
+
void query.refetch();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Optional write companion — PUTs a (partial) settings object and invalidates
|
|
87
|
+
* the read query so `useAddonSettings` re-resolves. The host's generic config
|
|
88
|
+
* modal does its own PUT; this exists for addons that ship a custom config
|
|
89
|
+
* surface. Backward-compatible: purely additive.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const update = useUpdateAddonSettings('pos')
|
|
93
|
+
* update.mutate({ allowNegativeStock: true })
|
|
94
|
+
*/
|
|
95
|
+
export function useUpdateAddonSettings(addonKey) {
|
|
96
|
+
const api = useApi();
|
|
97
|
+
const qc = useQueryClient();
|
|
98
|
+
return useMutation({
|
|
99
|
+
mutationFn: async (values) => {
|
|
100
|
+
const res = await api.put(`/api/addons/${addonKey}/settings`, values);
|
|
101
|
+
const body = res.data;
|
|
102
|
+
const saved = body?.success ? body.data : body;
|
|
103
|
+
return (saved ?? values);
|
|
104
|
+
},
|
|
105
|
+
onSuccess: () => {
|
|
106
|
+
void qc.invalidateQueries({ queryKey: addonSettingsKey(addonKey) });
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Locks the default-merge semantics of the useAddonSettings primitive: saved
|
|
2
|
+
// org values win over caller defaults, never-saved keys fall back to their
|
|
3
|
+
// default, and a sparse/undefined stored payload stays well-behaved. The hook
|
|
4
|
+
// itself is a thin react-query wrapper over `useApi`; the load-bearing logic
|
|
5
|
+
// is this pure merge, so that's what we test directly.
|
|
6
|
+
import { describe, it, expect } from 'vitest'
|
|
7
|
+
import { mergeAddonSettings, addonSettingsKey } from '../use-addon-settings'
|
|
8
|
+
|
|
9
|
+
interface PosSettings extends Record<string, unknown> {
|
|
10
|
+
allowNegativeStock: boolean
|
|
11
|
+
roundingMode: string
|
|
12
|
+
taxRate: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('mergeAddonSettings', () => {
|
|
16
|
+
const defaults: Partial<PosSettings> = {
|
|
17
|
+
allowNegativeStock: false,
|
|
18
|
+
roundingMode: 'nearest',
|
|
19
|
+
taxRate: 0,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
it('falls back to defaults when nothing is stored', () => {
|
|
23
|
+
expect(mergeAddonSettings<PosSettings>(defaults, null)).toEqual({
|
|
24
|
+
allowNegativeStock: false,
|
|
25
|
+
roundingMode: 'nearest',
|
|
26
|
+
taxRate: 0,
|
|
27
|
+
})
|
|
28
|
+
expect(mergeAddonSettings<PosSettings>(defaults, undefined)).toEqual(defaults)
|
|
29
|
+
expect(mergeAddonSettings<PosSettings>(defaults, {})).toEqual(defaults)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('lets saved org values override defaults', () => {
|
|
33
|
+
expect(
|
|
34
|
+
mergeAddonSettings<PosSettings>(defaults, { allowNegativeStock: true, taxRate: 16 }),
|
|
35
|
+
).toEqual({
|
|
36
|
+
allowNegativeStock: true,
|
|
37
|
+
roundingMode: 'nearest',
|
|
38
|
+
taxRate: 16,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('keeps falsy saved values (0, false, "") instead of treating them as unset', () => {
|
|
43
|
+
expect(
|
|
44
|
+
mergeAddonSettings<PosSettings>(
|
|
45
|
+
{ allowNegativeStock: true, roundingMode: 'up', taxRate: 5 },
|
|
46
|
+
{ allowNegativeStock: false, roundingMode: '', taxRate: 0 },
|
|
47
|
+
),
|
|
48
|
+
).toEqual({ allowNegativeStock: false, roundingMode: '', taxRate: 0 })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('treats an explicit undefined in the stored payload as unset', () => {
|
|
52
|
+
expect(
|
|
53
|
+
mergeAddonSettings<PosSettings>(defaults, { roundingMode: undefined }),
|
|
54
|
+
).toEqual(defaults)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns an object even with no defaults', () => {
|
|
58
|
+
expect(mergeAddonSettings(undefined, { a: 1 })).toEqual({ a: 1 })
|
|
59
|
+
expect(mergeAddonSettings(undefined, null)).toEqual({})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('does not mutate the provided defaults object', () => {
|
|
63
|
+
const d = { ...defaults }
|
|
64
|
+
mergeAddonSettings<PosSettings>(d, { taxRate: 99 })
|
|
65
|
+
expect(d).toEqual(defaults)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('addonSettingsKey', () => {
|
|
70
|
+
it('is stable and namespaced per addon', () => {
|
|
71
|
+
expect(addonSettingsKey('pos')).toEqual(['addon-settings', 'pos'])
|
|
72
|
+
expect(addonSettingsKey('inventory')).toEqual(['addon-settings', 'inventory'])
|
|
73
|
+
})
|
|
74
|
+
})
|
package/src/dynamic-form.tsx
CHANGED
|
@@ -58,23 +58,33 @@ export function DynamicForm({
|
|
|
58
58
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
|
59
59
|
const [submitting, setSubmitting] = useState(false)
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// Readonly fields are system-owned (connection state, sync-written values):
|
|
62
|
+
// the server writes them, so the form never captures them — hidden outright
|
|
63
|
+
// instead of rendered disabled, matching the metadata contract
|
|
64
|
+
// (ColumnDef.Readonly). They are also excluded from defaults/submit so a
|
|
65
|
+
// create never posts e.g. `status: ""` over the column default.
|
|
66
|
+
const editableFields = useMemo(
|
|
67
|
+
() => fields.filter((f) => !(f as { readonly?: boolean }).readonly),
|
|
68
|
+
[fields],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const schema = useMemo(() => buildZodSchema(editableFields), [editableFields])
|
|
62
72
|
|
|
63
73
|
// Line-items fields carrying a balance rule gate submit: an unbalanced entry
|
|
64
74
|
// (Σdebit ≠ Σcredit, or all-zero when require_nonzero) can't be saved. This
|
|
65
75
|
// is fully declarative — `evaluateBalance` returns undefined for fields with
|
|
66
76
|
// no rule, so non-balanced forms are unaffected.
|
|
67
77
|
const balanceBlocked = useMemo(() => {
|
|
68
|
-
for (const f of
|
|
78
|
+
for (const f of editableFields) {
|
|
69
79
|
const state = evaluateBalance(f, values[f.key])
|
|
70
80
|
if (state && !state.balanced) return true
|
|
71
81
|
}
|
|
72
82
|
return false
|
|
73
|
-
}, [
|
|
83
|
+
}, [editableFields, values])
|
|
74
84
|
|
|
75
85
|
useEffect(() => {
|
|
76
86
|
const defaults: Record<string, any> = {}
|
|
77
|
-
for (const f of
|
|
87
|
+
for (const f of editableFields) {
|
|
78
88
|
if (isLineItemsField(f)) {
|
|
79
89
|
defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? []
|
|
80
90
|
continue
|
|
@@ -83,7 +93,7 @@ export function DynamicForm({
|
|
|
83
93
|
}
|
|
84
94
|
setValues(defaults)
|
|
85
95
|
setErrors({})
|
|
86
|
-
}, [
|
|
96
|
+
}, [editableFields, initialValues])
|
|
87
97
|
|
|
88
98
|
const update = (k: string, v: any) =>
|
|
89
99
|
setValues((prev: Record<string, any>) => ({ ...prev, [k]: v }))
|
|
@@ -113,7 +123,7 @@ export function DynamicForm({
|
|
|
113
123
|
return (
|
|
114
124
|
<form onSubmit={handleSubmit} className="grid gap-4">
|
|
115
125
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
116
|
-
{
|
|
126
|
+
{editableFields.map((field) => (
|
|
117
127
|
<FieldRow
|
|
118
128
|
key={field.key}
|
|
119
129
|
field={field}
|
package/src/index.ts
CHANGED
|
@@ -165,6 +165,7 @@ export * from './org-runtime-provider'
|
|
|
165
165
|
export * from './navigation-builder'
|
|
166
166
|
export * from './i18n-provider'
|
|
167
167
|
export * from './api-context'
|
|
168
|
+
export * from './use-addon-settings'
|
|
168
169
|
export * from './metadata-cache'
|
|
169
170
|
export {
|
|
170
171
|
ADDON_MANIFEST_CHANGED_TYPE,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// useAddonSettings — THE standard primitive for reading an addon's
|
|
2
|
+
// per-organization configuration from a federated addon (or any host
|
|
3
|
+
// surface) without every addon re-implementing its own fetch.
|
|
4
|
+
//
|
|
5
|
+
// The host (ops) persists settings per org on `AddonInstallation.settings`
|
|
6
|
+
// (jsonb) and exposes:
|
|
7
|
+
// GET /api/addons/:key/settings → { success, data: { <key>: <value> } }
|
|
8
|
+
// PUT /api/addons/:key/settings → body = object of values
|
|
9
|
+
// A setting the org has never saved simply isn't present in `data`; the
|
|
10
|
+
// caller supplies the fallback via `opts.defaults` (which mirror the
|
|
11
|
+
// manifest's `settings[].default`). runtime-react has no access to the
|
|
12
|
+
// addon manifest at runtime, so defaults are a parameter, not automatic.
|
|
13
|
+
//
|
|
14
|
+
// react-query is a PEER dependency of runtime-react: `useQuery`/`useMutation`
|
|
15
|
+
// resolve to the HOST's singleton QueryClient (the MF shared singleton), so
|
|
16
|
+
// this hook never constructs its own client and stays cache-coherent with
|
|
17
|
+
// the rest of the app. Do not add a QueryClientProvider here.
|
|
18
|
+
//
|
|
19
|
+
// Server-side gating (wasm/Go backends) is out of scope here: a backend that
|
|
20
|
+
// must gate behaviour on org config reads the same `AddonInstallation.settings`
|
|
21
|
+
// jsonb directly — this hook is the client-read half of that contract.
|
|
22
|
+
import { useMemo } from 'react'
|
|
23
|
+
import {
|
|
24
|
+
useMutation,
|
|
25
|
+
useQuery,
|
|
26
|
+
useQueryClient,
|
|
27
|
+
} from '@tanstack/react-query'
|
|
28
|
+
import { useApi } from './api-context'
|
|
29
|
+
|
|
30
|
+
/** Stable react-query key for an addon's per-org settings. */
|
|
31
|
+
export function addonSettingsKey(addonKey: string) {
|
|
32
|
+
return ['addon-settings', addonKey] as const
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Merge saved org values over the caller's defaults so a never-saved setting
|
|
37
|
+
* falls back to its default. Pure + exported for unit testing. Only keys that
|
|
38
|
+
* are actually present in `stored` win — an explicit `undefined` in the stored
|
|
39
|
+
* payload is treated as "not saved" and yields the default (the host omits
|
|
40
|
+
* unsaved keys, but this keeps a sparse payload well-behaved).
|
|
41
|
+
*/
|
|
42
|
+
export function mergeAddonSettings<T extends Record<string, unknown>>(
|
|
43
|
+
defaults: Partial<T> | undefined,
|
|
44
|
+
stored: Partial<T> | null | undefined,
|
|
45
|
+
): T {
|
|
46
|
+
const out: Record<string, unknown> = { ...(defaults ?? {}) }
|
|
47
|
+
if (stored) {
|
|
48
|
+
for (const [k, v] of Object.entries(stored)) {
|
|
49
|
+
if (v !== undefined) out[k] = v
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out as T
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UseAddonSettingsOptions<T> {
|
|
56
|
+
/**
|
|
57
|
+
* Fallback values (typically the addon manifest's `settings[].default`)
|
|
58
|
+
* merged UNDER the saved org values. A setting the org hasn't saved
|
|
59
|
+
* resolves to its default here.
|
|
60
|
+
*/
|
|
61
|
+
defaults?: Partial<T>
|
|
62
|
+
/** Override staleTime (default 30s — org config changes rarely). */
|
|
63
|
+
staleTime?: number
|
|
64
|
+
/** Defer fetching (e.g. until the addon key is known). */
|
|
65
|
+
enabled?: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface UseAddonSettingsResult<T> {
|
|
69
|
+
/** Saved org values merged over `defaults`. Never null. */
|
|
70
|
+
settings: T
|
|
71
|
+
isLoading: boolean
|
|
72
|
+
error: unknown
|
|
73
|
+
refetch: () => void
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read an addon's per-organization settings.
|
|
78
|
+
*
|
|
79
|
+
* This is the ecosystem-standard way for a federated addon to read its own
|
|
80
|
+
* config; prefer it over a hand-rolled fetch so every addon caches, merges
|
|
81
|
+
* defaults, and invalidates identically.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* type PosSettings = { allowNegativeStock: boolean; roundingMode: string }
|
|
85
|
+
* const { settings, isLoading } = useAddonSettings<PosSettings>('pos', {
|
|
86
|
+
* defaults: { allowNegativeStock: false, roundingMode: 'nearest' },
|
|
87
|
+
* })
|
|
88
|
+
* if (!isLoading && settings.allowNegativeStock) { ... }
|
|
89
|
+
*/
|
|
90
|
+
export function useAddonSettings<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
91
|
+
addonKey: string,
|
|
92
|
+
opts: UseAddonSettingsOptions<T> = {},
|
|
93
|
+
): UseAddonSettingsResult<T> {
|
|
94
|
+
const api = useApi()
|
|
95
|
+
const { defaults, staleTime, enabled } = opts
|
|
96
|
+
|
|
97
|
+
const query = useQuery<Partial<T>>({
|
|
98
|
+
queryKey: addonSettingsKey(addonKey),
|
|
99
|
+
queryFn: async () => {
|
|
100
|
+
const res = await api.get(`/api/addons/${addonKey}/settings`)
|
|
101
|
+
const body = (res as { data: any }).data
|
|
102
|
+
// Host envelope: { success, data: { <key>: <value> } }.
|
|
103
|
+
const values = body?.success ? body.data : body
|
|
104
|
+
return (values ?? {}) as Partial<T>
|
|
105
|
+
},
|
|
106
|
+
staleTime: staleTime ?? 30_000,
|
|
107
|
+
enabled: (enabled ?? true) && !!addonKey,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const settings = useMemo(
|
|
111
|
+
() => mergeAddonSettings<T>(defaults, query.data),
|
|
112
|
+
[defaults, query.data],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
settings,
|
|
117
|
+
isLoading: query.isLoading,
|
|
118
|
+
error: query.error,
|
|
119
|
+
refetch: () => {
|
|
120
|
+
void query.refetch()
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Optional write companion — PUTs a (partial) settings object and invalidates
|
|
127
|
+
* the read query so `useAddonSettings` re-resolves. The host's generic config
|
|
128
|
+
* modal does its own PUT; this exists for addons that ship a custom config
|
|
129
|
+
* surface. Backward-compatible: purely additive.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* const update = useUpdateAddonSettings('pos')
|
|
133
|
+
* update.mutate({ allowNegativeStock: true })
|
|
134
|
+
*/
|
|
135
|
+
export function useUpdateAddonSettings<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
136
|
+
addonKey: string,
|
|
137
|
+
) {
|
|
138
|
+
const api = useApi()
|
|
139
|
+
const qc = useQueryClient()
|
|
140
|
+
|
|
141
|
+
return useMutation<Partial<T>, Error, Partial<T>>({
|
|
142
|
+
mutationFn: async (values) => {
|
|
143
|
+
const res = await api.put(`/api/addons/${addonKey}/settings`, values)
|
|
144
|
+
const body = (res as { data: any }).data
|
|
145
|
+
const saved = body?.success ? body.data : body
|
|
146
|
+
return (saved ?? values) as Partial<T>
|
|
147
|
+
},
|
|
148
|
+
onSuccess: () => {
|
|
149
|
+
void qc.invalidateQueries({ queryKey: addonSettingsKey(addonKey) })
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
}
|