@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 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,+BAujCnB"}
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"}
@@ -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
- if (locationSearch === lastSelfSearch.current)
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';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "23.11.2",
3
+ "version": "23.12.1",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
+ })
@@ -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
- if (locationSearch === lastSelfSearch.current) return
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
+ }