@asteby/metacore-runtime-react 23.12.1 → 23.12.3

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.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 5e46e61: fix(dynamic-table): don't strip a deep-linked filter on first mount (URL race)
8
+
9
+ The URL-write effect ran on the very first commit — when `initializedFromUrl`
10
+ (a ref) is already true but the init effect's `setDynamicFilters` hasn't
11
+ re-rendered yet — so it wrote a URL WITHOUT `f_status`, stripping a
12
+ deep-linked/reloaded filter and flickering the address bar until it settled
13
+ without the filter. Gate the write on a STATE flag (`urlSynced`) set once the
14
+ URL has been adopted, so the first write only happens on the render where
15
+ `dynamicFilters`/`pagination` already mirror the URL (a true no-op).
16
+
17
+ ## 23.12.2
18
+
19
+ ### Patch Changes
20
+
21
+ - 5333315: fix(dynamic-table): don't rewrite a clean filtered deep-link (no URL flicker)
22
+
23
+ Normalise the `eq:` operator when fingerprinting the query string and stop
24
+ stamping `per_page` for the plain server default, so opening/clicking a
25
+ sidebar deep-link (`f_status=eq:in_progress&view=list`) is a true no-op: the
26
+ table adopts the filter without rewriting the URL to its own spelling
27
+ (`f_status=in_progress&per_page=15`). This removes the visible URL flicker and
28
+ keeps the sidebar active-state exact-href match working.
29
+
3
30
  ## 23.12.1
4
31
 
5
32
  ### 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,+BAmlCnB"}
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,+BAsnCnB"}
@@ -78,6 +78,24 @@ 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);
88
+ // Has the table finished adopting its state FROM the URL yet? The write
89
+ // effect must not run before this flips true. `initializedFromUrl` is a ref
90
+ // set synchronously at the top of the init effect, so on the very first
91
+ // commit the write effect (which runs right after, in the SAME commit) sees
92
+ // `initialized=true` but `dynamicFilters` still EMPTY — the init effect's
93
+ // setState hasn't re-rendered yet — and writes a URL WITHOUT `f_status`,
94
+ // stripping a deep-linked filter (the "recarga quita el filtro y parpadea"
95
+ // bug). A STATE flag defers the first write to the render AFTER the URL has
96
+ // been adopted, where `dynamicFilters`/`pagination` already mirror the URL,
97
+ // so the write is a no-op and the filter never gets stripped.
98
+ const [urlSynced, setUrlSynced] = useState(false);
81
99
  useEffect(() => {
82
100
  if (prevBranchId.current !== currentBranch?.id) {
83
101
  prevBranchId.current = currentBranch?.id;
@@ -114,18 +132,26 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
114
132
  const alias = operatorToUrlAlias[prefix];
115
133
  return alias ? `${alias}:${rest}` : value;
116
134
  };
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.
135
+ // Order-, encoding- and operator-independent fingerprint of a query string.
136
+ // The host router (TanStack) and the sidebar deep-link the filter in THEIR
137
+ // canonical form — different key order, percent-encoded operator colons, and
138
+ // the explicit `eq:` operator (`f_status=eq%3Ain_progress`) — while the table
139
+ // internally unwraps `eq:` for selects and would write the bare value
140
+ // (`f_status=in_progress`). Comparing raw (or even just decoded) strings then
141
+ // NEVER matches, so the table rewrites the URL on every navigation and the
142
+ // two spellings visibly ping-pong ("parpadeo", "Throttling navigation…" +
143
+ // React #185). Normalising each `f_` value through `urlValueToInternal`
144
+ // collapses `eq:X` and `X` to the same token, so a sorted `key=value` list is
145
+ // a true semantic fingerprint and the write effect can detect real no-ops —
146
+ // leaving the router's own spelling untouched (so the sidebar active-state
147
+ // match on the exact href keeps working too).
125
148
  const canonicalSearch = (searchStr) => {
126
149
  const p = new URLSearchParams(searchStr);
127
150
  const entries = [];
128
- p.forEach((value, key) => entries.push(`${key}=${value}`));
151
+ p.forEach((value, key) => {
152
+ const norm = key.startsWith('f_') ? urlValueToInternal(value) : value;
153
+ entries.push(`${key}=${norm}`);
154
+ });
129
155
  entries.sort();
130
156
  return entries.join('&');
131
157
  };
@@ -166,13 +192,16 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
166
192
  });
167
193
  if (Object.keys(filters).length > 0)
168
194
  setDynamicFilters(filters);
195
+ // Adopted everything the URL carries; the write effect may now run on the
196
+ // NEXT render (where the setState above has landed) without clobbering it.
197
+ setUrlSynced(true);
169
198
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
170
199
  // The exact query string this table last wrote to the URL — lets the
171
200
  // resync effect below tell "our own replaceState" apart from an external
172
201
  // rewrite by the host router.
173
202
  const lastSelfSearch = useRef(null);
174
203
  useEffect(() => {
175
- if (!enableUrlSync || !initializedFromUrl.current)
204
+ if (!enableUrlSync || !urlSynced)
176
205
  return;
177
206
  const params = new URLSearchParams();
178
207
  // Preserve the route-owned view params. The table rebuilds the query
@@ -191,7 +220,10 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
191
220
  }
192
221
  if (pagination.pageIndex > 0)
193
222
  params.set('page', String(pagination.pageIndex + 1));
194
- if (pagination.pageSize !== 10)
223
+ // Only pin per_page when it deviates from the model's default (an explicit
224
+ // user page-size change) or the URL already carried one — not for the
225
+ // plain server default, which would needlessly mutate a clean deep-link.
226
+ if (pagination.pageSize !== defaultPerPage.current || urlHadPerPage.current)
195
227
  params.set('per_page', String(pagination.pageSize));
196
228
  if (sorting.length > 0) {
197
229
  params.set('sortBy', sorting[0].id);
@@ -223,7 +255,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
223
255
  const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname;
224
256
  lastSelfSearch.current = search ? `?${search}` : '';
225
257
  window.history.replaceState(null, '', newUrl);
226
- }, [enableUrlSync, pagination, sorting, globalFilter, dynamicFilters, defaultFilters]);
258
+ }, [enableUrlSync, urlSynced, pagination, sorting, globalFilter, dynamicFilters, defaultFilters]);
227
259
  // The host router can rewrite the query string WITHOUT remounting the
228
260
  // table — e.g. sidebar sibling entries deep-link different `f_` filters
229
261
  // into the same model route. The mount-time init above never re-runs, so
@@ -296,6 +328,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
296
328
  // localStorage.
297
329
  if (cached) {
298
330
  setMetadata(cached);
331
+ defaultPerPage.current = cached.defaultPerPage || 10;
299
332
  if (!urlHadPerPage.current)
300
333
  setPagination((prev) => ({ ...prev, pageSize: cached.defaultPerPage || 10 }));
301
334
  setLoading(false);
@@ -311,6 +344,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
311
344
  meta = fresh;
312
345
  setMetadata(fresh);
313
346
  cacheMetadata(model, fresh);
347
+ defaultPerPage.current = fresh.defaultPerPage || 10;
314
348
  if (!urlHadPerPage.current)
315
349
  setPagination((prev) => ({ ...prev, pageSize: fresh.defaultPerPage || 10 }));
316
350
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "23.12.1",
3
+ "version": "23.12.3",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -190,6 +190,24 @@ 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)
200
+ // Has the table finished adopting its state FROM the URL yet? The write
201
+ // effect must not run before this flips true. `initializedFromUrl` is a ref
202
+ // set synchronously at the top of the init effect, so on the very first
203
+ // commit the write effect (which runs right after, in the SAME commit) sees
204
+ // `initialized=true` but `dynamicFilters` still EMPTY — the init effect's
205
+ // setState hasn't re-rendered yet — and writes a URL WITHOUT `f_status`,
206
+ // stripping a deep-linked filter (the "recarga quita el filtro y parpadea"
207
+ // bug). A STATE flag defers the first write to the render AFTER the URL has
208
+ // been adopted, where `dynamicFilters`/`pagination` already mirror the URL,
209
+ // so the write is a no-op and the filter never gets stripped.
210
+ const [urlSynced, setUrlSynced] = useState(false)
193
211
 
194
212
  useEffect(() => {
195
213
  if (prevBranchId.current !== currentBranch?.id) {
@@ -229,18 +247,26 @@ export function DynamicTable({
229
247
  return alias ? `${alias}:${rest}` : value
230
248
  }
231
249
 
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.
250
+ // Order-, encoding- and operator-independent fingerprint of a query string.
251
+ // The host router (TanStack) and the sidebar deep-link the filter in THEIR
252
+ // canonical form — different key order, percent-encoded operator colons, and
253
+ // the explicit `eq:` operator (`f_status=eq%3Ain_progress`) — while the table
254
+ // internally unwraps `eq:` for selects and would write the bare value
255
+ // (`f_status=in_progress`). Comparing raw (or even just decoded) strings then
256
+ // NEVER matches, so the table rewrites the URL on every navigation and the
257
+ // two spellings visibly ping-pong ("parpadeo", "Throttling navigation…" +
258
+ // React #185). Normalising each `f_` value through `urlValueToInternal`
259
+ // collapses `eq:X` and `X` to the same token, so a sorted `key=value` list is
260
+ // a true semantic fingerprint and the write effect can detect real no-ops —
261
+ // leaving the router's own spelling untouched (so the sidebar active-state
262
+ // match on the exact href keeps working too).
240
263
  const canonicalSearch = (searchStr: string): string => {
241
264
  const p = new URLSearchParams(searchStr)
242
265
  const entries: string[] = []
243
- p.forEach((value, key) => entries.push(`${key}=${value}`))
266
+ p.forEach((value, key) => {
267
+ const norm = key.startsWith('f_') ? urlValueToInternal(value) : value
268
+ entries.push(`${key}=${norm}`)
269
+ })
244
270
  entries.sort()
245
271
  return entries.join('&')
246
272
  }
@@ -274,6 +300,9 @@ export function DynamicTable({
274
300
  }
275
301
  })
276
302
  if (Object.keys(filters).length > 0) setDynamicFilters(filters)
303
+ // Adopted everything the URL carries; the write effect may now run on the
304
+ // NEXT render (where the setState above has landed) without clobbering it.
305
+ setUrlSynced(true)
277
306
  }, []) // eslint-disable-line react-hooks/exhaustive-deps
278
307
 
279
308
  // The exact query string this table last wrote to the URL — lets the
@@ -282,7 +311,7 @@ export function DynamicTable({
282
311
  const lastSelfSearch = useRef<string | null>(null)
283
312
 
284
313
  useEffect(() => {
285
- if (!enableUrlSync || !initializedFromUrl.current) return
314
+ if (!enableUrlSync || !urlSynced) return
286
315
  const params = new URLSearchParams()
287
316
  // Preserve the route-owned view params. The table rebuilds the query
288
317
  // string from scratch (it only knows its own page/sort/filter keys), but
@@ -298,7 +327,11 @@ export function DynamicTable({
298
327
  if (v) params.set(key, v)
299
328
  }
300
329
  if (pagination.pageIndex > 0) params.set('page', String(pagination.pageIndex + 1))
301
- if (pagination.pageSize !== 10) params.set('per_page', String(pagination.pageSize))
330
+ // Only pin per_page when it deviates from the model's default (an explicit
331
+ // user page-size change) or the URL already carried one — not for the
332
+ // plain server default, which would needlessly mutate a clean deep-link.
333
+ if (pagination.pageSize !== defaultPerPage.current || urlHadPerPage.current)
334
+ params.set('per_page', String(pagination.pageSize))
302
335
  if (sorting.length > 0) {
303
336
  params.set('sortBy', sorting[0].id)
304
337
  params.set('order', sorting[0].desc ? 'desc' : 'asc')
@@ -324,7 +357,7 @@ export function DynamicTable({
324
357
  const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname
325
358
  lastSelfSearch.current = search ? `?${search}` : ''
326
359
  window.history.replaceState(null, '', newUrl)
327
- }, [enableUrlSync, pagination, sorting, globalFilter, dynamicFilters, defaultFilters])
360
+ }, [enableUrlSync, urlSynced, pagination, sorting, globalFilter, dynamicFilters, defaultFilters])
328
361
 
329
362
  // The host router can rewrite the query string WITHOUT remounting the
330
363
  // table — e.g. sidebar sibling entries deep-link different `f_` filters
@@ -389,6 +422,7 @@ export function DynamicTable({
389
422
  // localStorage.
390
423
  if (cached) {
391
424
  setMetadata(cached)
425
+ defaultPerPage.current = cached.defaultPerPage || 10
392
426
  if (!urlHadPerPage.current) setPagination((prev: PaginationState) => ({ ...prev, pageSize: cached.defaultPerPage || 10 }))
393
427
  setLoading(false)
394
428
  } else {
@@ -402,6 +436,7 @@ export function DynamicTable({
402
436
  meta = fresh
403
437
  setMetadata(fresh)
404
438
  cacheMetadata(model, fresh)
439
+ defaultPerPage.current = fresh.defaultPerPage || 10
405
440
  if (!urlHadPerPage.current) setPagination((prev: PaginationState) => ({ ...prev, pageSize: fresh.defaultPerPage || 10 }))
406
441
  }
407
442
  } catch (error) {