@asteby/metacore-runtime-react 23.11.2 → 23.12.1
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 +20 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +28 -1
- 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-table.tsx +29 -1
- package/src/index.ts +1 -0
- package/src/use-addon-settings.ts +152 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8788161: fix(dynamic-table): stop infinite URL replaceState loop on filtered deep-links
|
|
8
|
+
|
|
9
|
+
The host router re-serializes the query string in its own canonical form
|
|
10
|
+
(different key order, percent-encoded operator colons) than the table writes,
|
|
11
|
+
so the raw-string comparison never matched and the table's write effect fought
|
|
12
|
+
the router's re-serialize forever ("Throttling navigation…" + React #185),
|
|
13
|
+
hanging any filtered sidebar view (e.g. `f_status=eq:reception`). Both URL-sync
|
|
14
|
+
effects now compare an order/encoding-independent fingerprint and the write is
|
|
15
|
+
skipped when semantically a no-op.
|
|
16
|
+
|
|
17
|
+
## 23.12.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
3
23
|
## 23.11.2
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAanF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,cAAsB,GACzB,EAAE,iBAAiB,+
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAanF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,cAAsB,GACzB,EAAE,iBAAiB,+BAmlCnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -114,6 +114,21 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
114
114
|
const alias = operatorToUrlAlias[prefix];
|
|
115
115
|
return alias ? `${alias}:${rest}` : value;
|
|
116
116
|
};
|
|
117
|
+
// Order- and encoding-independent fingerprint of a query string. The host
|
|
118
|
+
// router (TanStack) re-serializes the same params in its own canonical form
|
|
119
|
+
// — different key order and percent-encoded operator colons
|
|
120
|
+
// (`f_status=eq%3Areception`) — than the table writes (`f_status=eq:reception`).
|
|
121
|
+
// Comparing raw strings then NEVER matches, so the table's write effect and
|
|
122
|
+
// the router's re-serialize fight forever ("Throttling navigation…" +
|
|
123
|
+
// React #185). URLSearchParams decodes values, so a sorted `key=value` list
|
|
124
|
+
// is stable across both spellings and lets us detect real no-ops.
|
|
125
|
+
const canonicalSearch = (searchStr) => {
|
|
126
|
+
const p = new URLSearchParams(searchStr);
|
|
127
|
+
const entries = [];
|
|
128
|
+
p.forEach((value, key) => entries.push(`${key}=${value}`));
|
|
129
|
+
entries.sort();
|
|
130
|
+
return entries.join('&');
|
|
131
|
+
};
|
|
117
132
|
useEffect(() => {
|
|
118
133
|
if (!enableUrlSync || initializedFromUrl.current)
|
|
119
134
|
return;
|
|
@@ -195,6 +210,16 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
195
210
|
params.set(`f_${key}`, `in:${values.join(',')}`);
|
|
196
211
|
});
|
|
197
212
|
const search = params.toString();
|
|
213
|
+
// If what we'd write is semantically identical to what's already in the
|
|
214
|
+
// bar (only key order / colon-encoding differ from the router's form),
|
|
215
|
+
// skip the write entirely. Rewriting would flip the raw string, the
|
|
216
|
+
// router would re-serialize back to its form, and the two would ping-pong
|
|
217
|
+
// forever. We still adopt the current location as "ours" so the resync
|
|
218
|
+
// effect below doesn't treat the router's spelling as an external change.
|
|
219
|
+
if (canonicalSearch(search) === canonicalSearch(window.location.search)) {
|
|
220
|
+
lastSelfSearch.current = window.location.search;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
198
223
|
const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname;
|
|
199
224
|
lastSelfSearch.current = search ? `?${search}` : '';
|
|
200
225
|
window.history.replaceState(null, '', newUrl);
|
|
@@ -209,7 +234,9 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
209
234
|
useEffect(() => {
|
|
210
235
|
if (!enableUrlSync || !initializedFromUrl.current)
|
|
211
236
|
return;
|
|
212
|
-
|
|
237
|
+
// Semantic compare: the router's spelling of the query string we just
|
|
238
|
+
// wrote (different order/encoding) is NOT an external change.
|
|
239
|
+
if (canonicalSearch(locationSearch) === canonicalSearch(lastSelfSearch.current ?? ''))
|
|
213
240
|
return;
|
|
214
241
|
lastSelfSearch.current = locationSearch;
|
|
215
242
|
const params = new URLSearchParams(locationSearch);
|
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-table.tsx
CHANGED
|
@@ -229,6 +229,22 @@ export function DynamicTable({
|
|
|
229
229
|
return alias ? `${alias}:${rest}` : value
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// Order- and encoding-independent fingerprint of a query string. The host
|
|
233
|
+
// router (TanStack) re-serializes the same params in its own canonical form
|
|
234
|
+
// — different key order and percent-encoded operator colons
|
|
235
|
+
// (`f_status=eq%3Areception`) — than the table writes (`f_status=eq:reception`).
|
|
236
|
+
// Comparing raw strings then NEVER matches, so the table's write effect and
|
|
237
|
+
// the router's re-serialize fight forever ("Throttling navigation…" +
|
|
238
|
+
// React #185). URLSearchParams decodes values, so a sorted `key=value` list
|
|
239
|
+
// is stable across both spellings and lets us detect real no-ops.
|
|
240
|
+
const canonicalSearch = (searchStr: string): string => {
|
|
241
|
+
const p = new URLSearchParams(searchStr)
|
|
242
|
+
const entries: string[] = []
|
|
243
|
+
p.forEach((value, key) => entries.push(`${key}=${value}`))
|
|
244
|
+
entries.sort()
|
|
245
|
+
return entries.join('&')
|
|
246
|
+
}
|
|
247
|
+
|
|
232
248
|
useEffect(() => {
|
|
233
249
|
if (!enableUrlSync || initializedFromUrl.current) return
|
|
234
250
|
initializedFromUrl.current = true
|
|
@@ -295,6 +311,16 @@ export function DynamicTable({
|
|
|
295
311
|
else params.set(`f_${key}`, `in:${values.join(',')}`)
|
|
296
312
|
})
|
|
297
313
|
const search = params.toString()
|
|
314
|
+
// If what we'd write is semantically identical to what's already in the
|
|
315
|
+
// bar (only key order / colon-encoding differ from the router's form),
|
|
316
|
+
// skip the write entirely. Rewriting would flip the raw string, the
|
|
317
|
+
// router would re-serialize back to its form, and the two would ping-pong
|
|
318
|
+
// forever. We still adopt the current location as "ours" so the resync
|
|
319
|
+
// effect below doesn't treat the router's spelling as an external change.
|
|
320
|
+
if (canonicalSearch(search) === canonicalSearch(window.location.search)) {
|
|
321
|
+
lastSelfSearch.current = window.location.search
|
|
322
|
+
return
|
|
323
|
+
}
|
|
298
324
|
const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname
|
|
299
325
|
lastSelfSearch.current = search ? `?${search}` : ''
|
|
300
326
|
window.history.replaceState(null, '', newUrl)
|
|
@@ -309,7 +335,9 @@ export function DynamicTable({
|
|
|
309
335
|
const locationSearch = typeof window !== 'undefined' ? window.location.search : ''
|
|
310
336
|
useEffect(() => {
|
|
311
337
|
if (!enableUrlSync || !initializedFromUrl.current) return
|
|
312
|
-
|
|
338
|
+
// Semantic compare: the router's spelling of the query string we just
|
|
339
|
+
// wrote (different order/encoding) is NOT an external change.
|
|
340
|
+
if (canonicalSearch(locationSearch) === canonicalSearch(lastSelfSearch.current ?? '')) return
|
|
313
341
|
lastSelfSearch.current = locationSearch
|
|
314
342
|
const params = new URLSearchParams(locationSearch)
|
|
315
343
|
const filters: Record<string, string[]> = {}
|
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
|
+
}
|