@asteby/metacore-runtime-react 23.12.0 → 23.12.2

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,32 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 23.12.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 5333315: fix(dynamic-table): don't rewrite a clean filtered deep-link (no URL flicker)
8
+
9
+ Normalise the `eq:` operator when fingerprinting the query string and stop
10
+ stamping `per_page` for the plain server default, so opening/clicking a
11
+ sidebar deep-link (`f_status=eq:in_progress&view=list`) is a true no-op: the
12
+ table adopts the filter without rewriting the URL to its own spelling
13
+ (`f_status=in_progress&per_page=15`). This removes the visible URL flicker and
14
+ keeps the sidebar active-state exact-href match working.
15
+
16
+ ## 23.12.1
17
+
18
+ ### Patch Changes
19
+
20
+ - 8788161: fix(dynamic-table): stop infinite URL replaceState loop on filtered deep-links
21
+
22
+ The host router re-serializes the query string in its own canonical form
23
+ (different key order, percent-encoded operator colons) than the table writes,
24
+ so the raw-string comparison never matched and the table's write effect fought
25
+ the router's re-serialize forever ("Throttling navigation…" + React #185),
26
+ hanging any filtered sidebar view (e.g. `f_status=eq:reception`). Both URL-sync
27
+ effects now compare an order/encoding-independent fingerprint and the write is
28
+ skipped when semantically a no-op.
29
+
3
30
  ## 23.12.0
4
31
 
5
32
  ### Minor 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,+BAwmCnB"}
@@ -78,6 +78,13 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
78
78
  const [filterOptionsMap, setFilterOptionsMap] = useState(new Map());
79
79
  const initializedFromUrl = useRef(false);
80
80
  const urlHadPerPage = useRef(false);
81
+ // The model's server-declared default page size. When the current page size
82
+ // is just this default (the user hasn't paged/resized and the URL didn't
83
+ // pin one), we deliberately DON'T stamp `per_page` into the URL — otherwise
84
+ // the table appends `per_page=15` to the sidebar's clean deep-link on mount,
85
+ // which reads as a URL flicker and fights the router's spelling. Starts at 10
86
+ // to match the initial pagination state until metadata arrives.
87
+ const defaultPerPage = useRef(10);
81
88
  useEffect(() => {
82
89
  if (prevBranchId.current !== currentBranch?.id) {
83
90
  prevBranchId.current = currentBranch?.id;
@@ -114,6 +121,29 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
114
121
  const alias = operatorToUrlAlias[prefix];
115
122
  return alias ? `${alias}:${rest}` : value;
116
123
  };
124
+ // Order-, encoding- and operator-independent fingerprint of a query string.
125
+ // The host router (TanStack) and the sidebar deep-link the filter in THEIR
126
+ // canonical form — different key order, percent-encoded operator colons, and
127
+ // the explicit `eq:` operator (`f_status=eq%3Ain_progress`) — while the table
128
+ // internally unwraps `eq:` for selects and would write the bare value
129
+ // (`f_status=in_progress`). Comparing raw (or even just decoded) strings then
130
+ // NEVER matches, so the table rewrites the URL on every navigation and the
131
+ // two spellings visibly ping-pong ("parpadeo", "Throttling navigation…" +
132
+ // React #185). Normalising each `f_` value through `urlValueToInternal`
133
+ // collapses `eq:X` and `X` to the same token, so a sorted `key=value` list is
134
+ // a true semantic fingerprint and the write effect can detect real no-ops —
135
+ // leaving the router's own spelling untouched (so the sidebar active-state
136
+ // match on the exact href keeps working too).
137
+ const canonicalSearch = (searchStr) => {
138
+ const p = new URLSearchParams(searchStr);
139
+ const entries = [];
140
+ p.forEach((value, key) => {
141
+ const norm = key.startsWith('f_') ? urlValueToInternal(value) : value;
142
+ entries.push(`${key}=${norm}`);
143
+ });
144
+ entries.sort();
145
+ return entries.join('&');
146
+ };
117
147
  useEffect(() => {
118
148
  if (!enableUrlSync || initializedFromUrl.current)
119
149
  return;
@@ -176,7 +206,10 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
176
206
  }
177
207
  if (pagination.pageIndex > 0)
178
208
  params.set('page', String(pagination.pageIndex + 1));
179
- if (pagination.pageSize !== 10)
209
+ // Only pin per_page when it deviates from the model's default (an explicit
210
+ // user page-size change) or the URL already carried one — not for the
211
+ // plain server default, which would needlessly mutate a clean deep-link.
212
+ if (pagination.pageSize !== defaultPerPage.current || urlHadPerPage.current)
180
213
  params.set('per_page', String(pagination.pageSize));
181
214
  if (sorting.length > 0) {
182
215
  params.set('sortBy', sorting[0].id);
@@ -195,6 +228,16 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
195
228
  params.set(`f_${key}`, `in:${values.join(',')}`);
196
229
  });
197
230
  const search = params.toString();
231
+ // If what we'd write is semantically identical to what's already in the
232
+ // bar (only key order / colon-encoding differ from the router's form),
233
+ // skip the write entirely. Rewriting would flip the raw string, the
234
+ // router would re-serialize back to its form, and the two would ping-pong
235
+ // forever. We still adopt the current location as "ours" so the resync
236
+ // effect below doesn't treat the router's spelling as an external change.
237
+ if (canonicalSearch(search) === canonicalSearch(window.location.search)) {
238
+ lastSelfSearch.current = window.location.search;
239
+ return;
240
+ }
198
241
  const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname;
199
242
  lastSelfSearch.current = search ? `?${search}` : '';
200
243
  window.history.replaceState(null, '', newUrl);
@@ -209,7 +252,9 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
209
252
  useEffect(() => {
210
253
  if (!enableUrlSync || !initializedFromUrl.current)
211
254
  return;
212
- if (locationSearch === lastSelfSearch.current)
255
+ // Semantic compare: the router's spelling of the query string we just
256
+ // wrote (different order/encoding) is NOT an external change.
257
+ if (canonicalSearch(locationSearch) === canonicalSearch(lastSelfSearch.current ?? ''))
213
258
  return;
214
259
  lastSelfSearch.current = locationSearch;
215
260
  const params = new URLSearchParams(locationSearch);
@@ -269,6 +314,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
269
314
  // localStorage.
270
315
  if (cached) {
271
316
  setMetadata(cached);
317
+ defaultPerPage.current = cached.defaultPerPage || 10;
272
318
  if (!urlHadPerPage.current)
273
319
  setPagination((prev) => ({ ...prev, pageSize: cached.defaultPerPage || 10 }));
274
320
  setLoading(false);
@@ -284,6 +330,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
284
330
  meta = fresh;
285
331
  setMetadata(fresh);
286
332
  cacheMetadata(model, fresh);
333
+ defaultPerPage.current = fresh.defaultPerPage || 10;
287
334
  if (!urlHadPerPage.current)
288
335
  setPagination((prev) => ({ ...prev, pageSize: fresh.defaultPerPage || 10 }));
289
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "23.12.0",
3
+ "version": "23.12.2",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -190,6 +190,13 @@ export function DynamicTable({
190
190
 
191
191
  const initializedFromUrl = useRef(false)
192
192
  const urlHadPerPage = useRef(false)
193
+ // The model's server-declared default page size. When the current page size
194
+ // is just this default (the user hasn't paged/resized and the URL didn't
195
+ // pin one), we deliberately DON'T stamp `per_page` into the URL — otherwise
196
+ // the table appends `per_page=15` to the sidebar's clean deep-link on mount,
197
+ // which reads as a URL flicker and fights the router's spelling. Starts at 10
198
+ // to match the initial pagination state until metadata arrives.
199
+ const defaultPerPage = useRef(10)
193
200
 
194
201
  useEffect(() => {
195
202
  if (prevBranchId.current !== currentBranch?.id) {
@@ -229,6 +236,30 @@ export function DynamicTable({
229
236
  return alias ? `${alias}:${rest}` : value
230
237
  }
231
238
 
239
+ // Order-, encoding- and operator-independent fingerprint of a query string.
240
+ // The host router (TanStack) and the sidebar deep-link the filter in THEIR
241
+ // canonical form — different key order, percent-encoded operator colons, and
242
+ // the explicit `eq:` operator (`f_status=eq%3Ain_progress`) — while the table
243
+ // internally unwraps `eq:` for selects and would write the bare value
244
+ // (`f_status=in_progress`). Comparing raw (or even just decoded) strings then
245
+ // NEVER matches, so the table rewrites the URL on every navigation and the
246
+ // two spellings visibly ping-pong ("parpadeo", "Throttling navigation…" +
247
+ // React #185). Normalising each `f_` value through `urlValueToInternal`
248
+ // collapses `eq:X` and `X` to the same token, so a sorted `key=value` list is
249
+ // a true semantic fingerprint and the write effect can detect real no-ops —
250
+ // leaving the router's own spelling untouched (so the sidebar active-state
251
+ // match on the exact href keeps working too).
252
+ const canonicalSearch = (searchStr: string): string => {
253
+ const p = new URLSearchParams(searchStr)
254
+ const entries: string[] = []
255
+ p.forEach((value, key) => {
256
+ const norm = key.startsWith('f_') ? urlValueToInternal(value) : value
257
+ entries.push(`${key}=${norm}`)
258
+ })
259
+ entries.sort()
260
+ return entries.join('&')
261
+ }
262
+
232
263
  useEffect(() => {
233
264
  if (!enableUrlSync || initializedFromUrl.current) return
234
265
  initializedFromUrl.current = true
@@ -282,7 +313,11 @@ export function DynamicTable({
282
313
  if (v) params.set(key, v)
283
314
  }
284
315
  if (pagination.pageIndex > 0) params.set('page', String(pagination.pageIndex + 1))
285
- if (pagination.pageSize !== 10) params.set('per_page', String(pagination.pageSize))
316
+ // Only pin per_page when it deviates from the model's default (an explicit
317
+ // user page-size change) or the URL already carried one — not for the
318
+ // plain server default, which would needlessly mutate a clean deep-link.
319
+ if (pagination.pageSize !== defaultPerPage.current || urlHadPerPage.current)
320
+ params.set('per_page', String(pagination.pageSize))
286
321
  if (sorting.length > 0) {
287
322
  params.set('sortBy', sorting[0].id)
288
323
  params.set('order', sorting[0].desc ? 'desc' : 'asc')
@@ -295,6 +330,16 @@ export function DynamicTable({
295
330
  else params.set(`f_${key}`, `in:${values.join(',')}`)
296
331
  })
297
332
  const search = params.toString()
333
+ // If what we'd write is semantically identical to what's already in the
334
+ // bar (only key order / colon-encoding differ from the router's form),
335
+ // skip the write entirely. Rewriting would flip the raw string, the
336
+ // router would re-serialize back to its form, and the two would ping-pong
337
+ // forever. We still adopt the current location as "ours" so the resync
338
+ // effect below doesn't treat the router's spelling as an external change.
339
+ if (canonicalSearch(search) === canonicalSearch(window.location.search)) {
340
+ lastSelfSearch.current = window.location.search
341
+ return
342
+ }
298
343
  const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname
299
344
  lastSelfSearch.current = search ? `?${search}` : ''
300
345
  window.history.replaceState(null, '', newUrl)
@@ -309,7 +354,9 @@ export function DynamicTable({
309
354
  const locationSearch = typeof window !== 'undefined' ? window.location.search : ''
310
355
  useEffect(() => {
311
356
  if (!enableUrlSync || !initializedFromUrl.current) return
312
- if (locationSearch === lastSelfSearch.current) return
357
+ // Semantic compare: the router's spelling of the query string we just
358
+ // wrote (different order/encoding) is NOT an external change.
359
+ if (canonicalSearch(locationSearch) === canonicalSearch(lastSelfSearch.current ?? '')) return
313
360
  lastSelfSearch.current = locationSearch
314
361
  const params = new URLSearchParams(locationSearch)
315
362
  const filters: Record<string, string[]> = {}
@@ -361,6 +408,7 @@ export function DynamicTable({
361
408
  // localStorage.
362
409
  if (cached) {
363
410
  setMetadata(cached)
411
+ defaultPerPage.current = cached.defaultPerPage || 10
364
412
  if (!urlHadPerPage.current) setPagination((prev: PaginationState) => ({ ...prev, pageSize: cached.defaultPerPage || 10 }))
365
413
  setLoading(false)
366
414
  } else {
@@ -374,6 +422,7 @@ export function DynamicTable({
374
422
  meta = fresh
375
423
  setMetadata(fresh)
376
424
  cacheMetadata(model, fresh)
425
+ defaultPerPage.current = fresh.defaultPerPage || 10
377
426
  if (!urlHadPerPage.current) setPagination((prev: PaginationState) => ({ ...prev, pageSize: fresh.defaultPerPage || 10 }))
378
427
  }
379
428
  } catch (error) {