@asteby/metacore-runtime-react 25.1.1 → 25.1.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,29 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 25.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 2f117ad: Fix FK violation (SQLSTATE 23503) when creating/updating a record with an empty optional relation. DynamicRecordDialog now normalizes the submit payload so empty reference pickers (and any nil-UUID value) are sent as `null` instead of `""`/"00000000-…", which a nullable FK column (e.g. products.category_id) would otherwise reject. Generic across all addons — keyed off field metadata (ref / dynamic_select / search). Exposed as `normalizeRefFieldsForSubmit`.
8
+ - 579ab21: DynamicRecordDialog now surfaces the server's real error cause (`details`) in the create/update/delete error toast via `toastServerError`, instead of showing only the generic headline ("Error creating record"). This is the modal the generic model CRUD page uses, so a failed create now shows the underlying Postgres/validation reason as the toast description.
9
+
10
+ ## 25.1.2
11
+
12
+ ### Patch Changes
13
+
14
+ - 7c19de1: fix(table): stop infinite reload loop when applying a column filter
15
+
16
+ The URL-resync effect read the location's search string from a render-time
17
+ snapshot, which lagged one commit behind the sibling write effect's imperative
18
+ `history.replaceState` (that write does not re-render). Applying any column
19
+ filter (e.g. the "Almacén" facet on /m/stock) made the resync misread its own
20
+ write as an external change, reset the filters, and ping-pong `{k:[v]} <-> {}`
21
+ every render — spinning the infinite-scroll fetch forever. The resync now reads
22
+ `window.location.search` fresh inside the effect, so a self-write is recognized
23
+ and skipped; only genuine external rewrites (sidebar deep-links, back/forward)
24
+ are adopted. Also lifts the pagination reset out of the `setDynamicFilters`
25
+ updater.
26
+
3
27
  ## 25.1.1
4
28
 
5
29
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
@@ -19,12 +19,14 @@ import { format, parseISO } from 'date-fns';
19
19
  import { es } from 'date-fns/locale';
20
20
  import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react';
21
21
  import { useApi } from '../api-context';
22
+ import { toastServerError } from '../server-error';
22
23
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
23
24
  import { DynamicRelations } from '../dynamic-relations';
24
25
  import { useOptionsResolver } from '../use-options-resolver';
25
26
  import { getFieldRef } from '../dynamic-form-schema';
26
27
  import { FieldCell } from '../field-grid';
27
28
  import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
29
+ import { normalizeRefFieldsForSubmit } from './normalize-submit';
28
30
  import { DynamicIcon, isLucideIconName } from '../dynamic-icon';
29
31
  import { humanizeToken } from '../dynamic-columns-helpers';
30
32
  import { formatDateCell } from '../dynamic-columns';
@@ -434,17 +436,20 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
434
436
  }
435
437
  }
436
438
  }
439
+ // Empty reference pickers → null (not "" / nil-UUID) so nullable FK
440
+ // columns accept them instead of raising a 23503 FK violation.
441
+ const payload = normalizeRefFieldsForSubmit(formValues, modalMeta.fields);
437
442
  setSaving(true);
438
443
  try {
439
444
  if (isCreate && onCreate) {
440
- const created = await onCreate(formValues);
445
+ const created = await onCreate(payload);
441
446
  toast.success(modalMeta?.messages?.created || t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' }));
442
447
  onSaved?.(created ?? undefined);
443
448
  onOpenChange(false);
444
449
  return;
445
450
  }
446
451
  if (!isCreate && recordId && onUpdate) {
447
- const updated = await onUpdate(String(recordId), formValues);
452
+ const updated = await onUpdate(String(recordId), payload);
448
453
  toast.success(modalMeta?.messages?.updated || t('dynamic.update_success', { defaultValue: 'Guardado correctamente' }));
449
454
  onSaved?.(updated ?? undefined);
450
455
  onOpenChange(false);
@@ -453,13 +458,13 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
453
458
  let res;
454
459
  if (isCreate) {
455
460
  const createEndpoint = endpoint || `/dynamic/${model}`;
456
- res = await api.post(createEndpoint, formValues);
461
+ res = await api.post(createEndpoint, payload);
457
462
  }
458
463
  else {
459
464
  const updateEndpoint = endpoint
460
465
  ? `${endpoint}/${recordId}`
461
466
  : `/dynamic/${model}/${recordId}`;
462
- res = await api.put(updateEndpoint, formValues);
467
+ res = await api.put(updateEndpoint, payload);
463
468
  }
464
469
  if (res.data?.success !== false) {
465
470
  // Prefer the addon's localized message (modal metadata), then a
@@ -474,11 +479,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
474
479
  onOpenChange(false);
475
480
  }
476
481
  else {
477
- toast.error(res.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
482
+ // Surface the server's real cause (`details`) as the toast
483
+ // description, not just the generic headline. `res.data` is the
484
+ // `{ success:false, message, details }` envelope.
485
+ toastServerError(res.data, { t, fallback: t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }) });
478
486
  }
479
487
  }
480
488
  catch (err) {
481
- toast.error(err?.response?.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
489
+ toastServerError(err, { t, fallback: t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }) });
482
490
  }
483
491
  finally {
484
492
  setSaving(false);
@@ -494,7 +502,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
494
502
  }
495
503
  catch (err) {
496
504
  console.error('[DynamicRecordDialog] delete error:', err);
497
- toast.error(err?.response?.data?.message || err?.message || t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }));
505
+ toastServerError(err, { t, fallback: t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }) });
498
506
  }
499
507
  finally {
500
508
  setDeleting(false);
@@ -0,0 +1,17 @@
1
+ /** Minimal field-metadata shape the normalizer reads. Kept permissive so any
2
+ * caller's field type (e.g. the dialog's `FieldDef[]`) is accepted. */
3
+ type RefFieldMeta = Record<string, any>;
4
+ /**
5
+ * Normalize a submit payload so that EMPTY reference fields go to the server as
6
+ * `null` rather than as `""` or the nil UUID ("00000000-…"). A dynamic FK column
7
+ * (e.g. products.category_id, a nullable `ref`) rejects a non-null value that
8
+ * points at no row: sending `""`/nil-UUID triggers
9
+ * insert ... violates foreign key constraint "fk_products_category" (23503)
10
+ * even though the user left the picker empty. This is generic — it keys off the
11
+ * field metadata (`getFieldRef` / dynamic_select / search), so every addon's
12
+ * optional relations benefit, not just products. Non-reference fields are left
13
+ * untouched (an empty text field is a legitimate `""`). Does not mutate `values`.
14
+ */
15
+ export declare function normalizeRefFieldsForSubmit(values: Record<string, any>, fields: RefFieldMeta[] | undefined): Record<string, any>;
16
+ export {};
17
+ //# sourceMappingURL=normalize-submit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-submit.d.ts","sourceRoot":"","sources":["../../src/dialogs/normalize-submit.ts"],"names":[],"mappings":"AAOA;wEACwE;AACxE,KAAK,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAEvC;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,GACnC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAuBrB"}
@@ -0,0 +1,36 @@
1
+ // Pure submit-payload normalization for dynamic record forms. Kept in its own
2
+ // module (no React / UI imports) so it is unit-testable in isolation and
3
+ // reusable by any caller that builds a create/update payload.
4
+ import { isNilUuid } from '../nil-uuid';
5
+ import { getFieldRef } from '../dynamic-form-schema';
6
+ /**
7
+ * Normalize a submit payload so that EMPTY reference fields go to the server as
8
+ * `null` rather than as `""` or the nil UUID ("00000000-…"). A dynamic FK column
9
+ * (e.g. products.category_id, a nullable `ref`) rejects a non-null value that
10
+ * points at no row: sending `""`/nil-UUID triggers
11
+ * insert ... violates foreign key constraint "fk_products_category" (23503)
12
+ * even though the user left the picker empty. This is generic — it keys off the
13
+ * field metadata (`getFieldRef` / dynamic_select / search), so every addon's
14
+ * optional relations benefit, not just products. Non-reference fields are left
15
+ * untouched (an empty text field is a legitimate `""`). Does not mutate `values`.
16
+ */
17
+ export function normalizeRefFieldsForSubmit(values, fields) {
18
+ const refKeys = new Set((fields ?? [])
19
+ .filter((f) => !!getFieldRef(f) ||
20
+ f.type === 'dynamic_select' ||
21
+ f.widget === 'dynamic_select' ||
22
+ f.type === 'search')
23
+ .map((f) => f.key));
24
+ const out = { ...values };
25
+ for (const [k, v] of Object.entries(out)) {
26
+ // A nil UUID is never a real target row — null it out on any field.
27
+ if (isNilUuid(v)) {
28
+ out[k] = null;
29
+ continue;
30
+ }
31
+ // An empty reference picker means "no relation" → null (not "").
32
+ if (refKeys.has(k) && (v === '' || v == null))
33
+ out[k] = null;
34
+ }
35
+ return out;
36
+ }
@@ -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;AAiEnF,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,+BA4pCnB"}
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;AAiEnF,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,+BA6qCnB"}
@@ -314,16 +314,30 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
314
314
  // the table kept showing the previous filter. On every render where the
315
315
  // location's search differs from the last URL we ourselves wrote,
316
316
  // re-parse the `f_` params and adopt them.
317
- const locationSearch = typeof window !== 'undefined' ? window.location.search : '';
318
317
  useEffect(() => {
319
318
  if (!enableUrlSync || !initializedFromUrl.current)
320
319
  return;
320
+ // Read the URL FRESH inside the effect — NOT a value captured during
321
+ // render. The write effect above mutates the URL imperatively via
322
+ // `history.replaceState`, which does NOT trigger a re-render, so a search
323
+ // string snapshotted at render time lags one commit behind what we just
324
+ // wrote. Comparing that stale snapshot against `lastSelfSearch` (which the
325
+ // write effect updated in this same commit) always mismatched, so the
326
+ // resync "adopted" a phantom external change and reset the filters — the
327
+ // write effect then re-wrote them, and the two effects ping-ponged the
328
+ // filters `{almacen:['Test']} <-> {}` on every render, spinning the
329
+ // infinite-scroll fetch forever the instant any column filter was applied.
330
+ // Reading fresh here means that right after our own write, `now` already
331
+ // equals `lastSelfSearch` and we bail out early. Only a genuine external
332
+ // rewrite (router deep-link from a sidebar sibling, browser back/forward)
333
+ // makes `now` diverge, which is exactly when we want to adopt it.
334
+ const now = typeof window !== 'undefined' ? window.location.search : '';
321
335
  // Semantic compare: the router's spelling of the query string we just
322
336
  // wrote (different order/encoding) is NOT an external change.
323
- if (canonicalSearch(locationSearch) === canonicalSearch(lastSelfSearch.current ?? ''))
337
+ if (canonicalSearch(now) === canonicalSearch(lastSelfSearch.current ?? ''))
324
338
  return;
325
- lastSelfSearch.current = locationSearch;
326
- const params = new URLSearchParams(locationSearch);
339
+ lastSelfSearch.current = now;
340
+ const params = new URLSearchParams(now);
327
341
  const filters = {};
328
342
  params.forEach((rawValue, key) => {
329
343
  if (!key.startsWith('f_'))
@@ -337,14 +351,16 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
337
351
  else
338
352
  filters[filterKey] = [value];
339
353
  });
340
- setDynamicFilters((prev) => {
341
- if (JSON.stringify(prev) === JSON.stringify(filters))
342
- return prev;
354
+ // Compare against the current filters directly (the effect re-runs every
355
+ // render, so this closure is always fresh) and only touch state on a real
356
+ // change — resetting pagination OUTSIDE any updater, never nested inside
357
+ // another setState's updater.
358
+ if (JSON.stringify(dynamicFilters) !== JSON.stringify(filters)) {
359
+ setDynamicFilters(filters);
343
360
  setPagination((p) => ({ ...p, pageIndex: 0 }));
344
- return filters;
345
- });
361
+ }
346
362
  const search = params.get('search');
347
- if (search !== null)
363
+ if (search !== null && search !== globalFilter)
348
364
  setGlobalFilter(search);
349
365
  });
350
366
  const prefetchOptions = useCallback(async (endpoints) => {
package/dist/index.d.ts CHANGED
@@ -38,6 +38,7 @@ export { UrlChip, FileChip, ImageThumbnail, MediaValue, RichText, linkifyText, c
38
38
  export { CollectionCell, formatScalar, prettifyKey, countLabel, type CollectionCellProps, type Translate as CollectionCellTranslate, } from './collection-cell';
39
39
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
40
40
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
41
+ export { normalizeRefFieldsForSubmit } from './dialogs/normalize-submit';
41
42
  export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record';
42
43
  export { CreateRecordDialog } from './dialogs/create-record-dialog';
43
44
  export { ViewRecordDialog } from './dialogs/view-record-dialog';
@@ -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
@@ -42,6 +42,7 @@ export { UrlChip, FileChip, ImageThumbnail, MediaValue, RichText, linkifyText, c
42
42
  export { CollectionCell, formatScalar, prettifyKey, countLabel, } from './collection-cell';
43
43
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
44
44
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
45
+ export { normalizeRefFieldsForSubmit } from './dialogs/normalize-submit';
45
46
  export { CreateRecordDialog } from './dialogs/create-record-dialog';
46
47
  export { ViewRecordDialog } from './dialogs/view-record-dialog';
47
48
  export { ExportDialog } from './dialogs/export';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "25.1.1",
3
+ "version": "25.1.3",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -67,8 +67,8 @@
67
67
  "typescript": "^6.0.0",
68
68
  "vitest": "^4.0.0",
69
69
  "zustand": "^5.0.0",
70
- "@asteby/metacore-sdk": "3.3.0",
71
- "@asteby/metacore-ui": "2.10.1"
70
+ "@asteby/metacore-ui": "2.10.1",
71
+ "@asteby/metacore-sdk": "3.3.0"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { normalizeRefFieldsForSubmit } from '../dialogs/normalize-submit'
3
+
4
+ const NIL = '00000000-0000-0000-0000-000000000000'
5
+
6
+ // A minimal field-metadata shape — only the keys the normalizer reads.
7
+ const fields: any[] = [
8
+ { key: 'category_id', ref: 'categories' }, // reference (getFieldRef)
9
+ { key: 'brand_id', type: 'dynamic_select' }, // reference by type
10
+ { key: 'owner_id', type: 'search' }, // reference by type
11
+ { key: 'name', type: 'text' }, // plain scalar
12
+ { key: 'description', type: 'textarea' },
13
+ ]
14
+
15
+ describe('normalizeRefFieldsForSubmit', () => {
16
+ it('nulls an empty-string reference so a nullable FK accepts it (no 23503)', () => {
17
+ const out = normalizeRefFieldsForSubmit(
18
+ { category_id: '', name: 'Test' },
19
+ fields,
20
+ )
21
+ expect(out.category_id).toBeNull()
22
+ })
23
+
24
+ it('nulls a nil-UUID reference value', () => {
25
+ const out = normalizeRefFieldsForSubmit({ brand_id: NIL }, fields)
26
+ expect(out.brand_id).toBeNull()
27
+ })
28
+
29
+ it('nulls a nil-UUID on ANY field, reference or not', () => {
30
+ const out = normalizeRefFieldsForSubmit({ some_id: NIL }, fields)
31
+ expect(out.some_id).toBeNull()
32
+ })
33
+
34
+ it('leaves a populated reference untouched', () => {
35
+ const id = '11111111-1111-1111-1111-111111111111'
36
+ const out = normalizeRefFieldsForSubmit({ category_id: id }, fields)
37
+ expect(out.category_id).toBe(id)
38
+ })
39
+
40
+ it('does NOT null an empty plain text field (empty string is legitimate)', () => {
41
+ const out = normalizeRefFieldsForSubmit(
42
+ { name: '', description: '' },
43
+ fields,
44
+ )
45
+ expect(out.name).toBe('')
46
+ expect(out.description).toBe('')
47
+ })
48
+
49
+ it('nulls empty search / dynamic_select references', () => {
50
+ const out = normalizeRefFieldsForSubmit(
51
+ { brand_id: '', owner_id: '' },
52
+ fields,
53
+ )
54
+ expect(out.brand_id).toBeNull()
55
+ expect(out.owner_id).toBeNull()
56
+ })
57
+
58
+ it('does not mutate the input object', () => {
59
+ const input = { category_id: '' }
60
+ normalizeRefFieldsForSubmit(input, fields)
61
+ expect(input.category_id).toBe('')
62
+ })
63
+ })
@@ -48,12 +48,14 @@ import { format, parseISO } from 'date-fns'
48
48
  import { es } from 'date-fns/locale'
49
49
  import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react'
50
50
  import { useApi } from '../api-context'
51
+ import { toastServerError } from '../server-error'
51
52
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field'
52
53
  import { DynamicRelations } from '../dynamic-relations'
53
54
  import { useOptionsResolver, type ResolvedOption } from '../use-options-resolver'
54
55
  import { getFieldRef } from '../dynamic-form-schema'
55
56
  import { FieldCell } from '../field-grid'
56
57
  import { isNilUuid, normalizeNilUuid } from '../nil-uuid'
58
+ import { normalizeRefFieldsForSubmit } from './normalize-submit'
57
59
  import { DynamicIcon, isLucideIconName } from '../dynamic-icon'
58
60
  import { humanizeToken } from '../dynamic-columns-helpers'
59
61
  import { formatDateCell } from '../dynamic-columns'
@@ -682,10 +684,14 @@ export function DynamicRecordDialog({
682
684
  }
683
685
  }
684
686
 
687
+ // Empty reference pickers → null (not "" / nil-UUID) so nullable FK
688
+ // columns accept them instead of raising a 23503 FK violation.
689
+ const payload = normalizeRefFieldsForSubmit(formValues, modalMeta.fields)
690
+
685
691
  setSaving(true)
686
692
  try {
687
693
  if (isCreate && onCreate) {
688
- const created = await onCreate(formValues)
694
+ const created = await onCreate(payload)
689
695
  toast.success(modalMeta?.messages?.created || t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' }))
690
696
  onSaved?.(created ?? undefined)
691
697
  onOpenChange(false)
@@ -693,7 +699,7 @@ export function DynamicRecordDialog({
693
699
  }
694
700
 
695
701
  if (!isCreate && recordId && onUpdate) {
696
- const updated = await onUpdate(String(recordId), formValues)
702
+ const updated = await onUpdate(String(recordId), payload)
697
703
  toast.success(modalMeta?.messages?.updated || t('dynamic.update_success', { defaultValue: 'Guardado correctamente' }))
698
704
  onSaved?.(updated ?? undefined)
699
705
  onOpenChange(false)
@@ -703,12 +709,12 @@ export function DynamicRecordDialog({
703
709
  let res
704
710
  if (isCreate) {
705
711
  const createEndpoint = endpoint || `/dynamic/${model}`
706
- res = await api.post(createEndpoint, formValues)
712
+ res = await api.post(createEndpoint, payload)
707
713
  } else {
708
714
  const updateEndpoint = endpoint
709
715
  ? `${endpoint}/${recordId}`
710
716
  : `/dynamic/${model}/${recordId}`
711
- res = await api.put(updateEndpoint, formValues)
717
+ res = await api.put(updateEndpoint, payload)
712
718
  }
713
719
 
714
720
  if (res.data?.success !== false) {
@@ -725,10 +731,13 @@ export function DynamicRecordDialog({
725
731
  onSaved?.(res.data?.data ?? res.data ?? undefined)
726
732
  onOpenChange(false)
727
733
  } else {
728
- toast.error(res.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }))
734
+ // Surface the server's real cause (`details`) as the toast
735
+ // description, not just the generic headline. `res.data` is the
736
+ // `{ success:false, message, details }` envelope.
737
+ toastServerError(res.data, { t, fallback: t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }) })
729
738
  }
730
739
  } catch (err: any) {
731
- toast.error(err?.response?.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }))
740
+ toastServerError(err, { t, fallback: t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }) })
732
741
  } finally {
733
742
  setSaving(false)
734
743
  }
@@ -742,7 +751,7 @@ export function DynamicRecordDialog({
742
751
  onOpenChange(false)
743
752
  } catch (err: any) {
744
753
  console.error('[DynamicRecordDialog] delete error:', err)
745
- toast.error(err?.response?.data?.message || err?.message || t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }))
754
+ toastServerError(err, { t, fallback: t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }) })
746
755
  } finally {
747
756
  setDeleting(false)
748
757
  }
@@ -0,0 +1,49 @@
1
+ // Pure submit-payload normalization for dynamic record forms. Kept in its own
2
+ // module (no React / UI imports) so it is unit-testable in isolation and
3
+ // reusable by any caller that builds a create/update payload.
4
+ import { isNilUuid } from '../nil-uuid'
5
+ import { getFieldRef } from '../dynamic-form-schema'
6
+ import type { ActionFieldDef } from '../types'
7
+
8
+ /** Minimal field-metadata shape the normalizer reads. Kept permissive so any
9
+ * caller's field type (e.g. the dialog's `FieldDef[]`) is accepted. */
10
+ type RefFieldMeta = Record<string, any>
11
+
12
+ /**
13
+ * Normalize a submit payload so that EMPTY reference fields go to the server as
14
+ * `null` rather than as `""` or the nil UUID ("00000000-…"). A dynamic FK column
15
+ * (e.g. products.category_id, a nullable `ref`) rejects a non-null value that
16
+ * points at no row: sending `""`/nil-UUID triggers
17
+ * insert ... violates foreign key constraint "fk_products_category" (23503)
18
+ * even though the user left the picker empty. This is generic — it keys off the
19
+ * field metadata (`getFieldRef` / dynamic_select / search), so every addon's
20
+ * optional relations benefit, not just products. Non-reference fields are left
21
+ * untouched (an empty text field is a legitimate `""`). Does not mutate `values`.
22
+ */
23
+ export function normalizeRefFieldsForSubmit(
24
+ values: Record<string, any>,
25
+ fields: RefFieldMeta[] | undefined,
26
+ ): Record<string, any> {
27
+ const refKeys = new Set(
28
+ (fields ?? [])
29
+ .filter(
30
+ (f) =>
31
+ !!getFieldRef(f as unknown as ActionFieldDef) ||
32
+ f.type === 'dynamic_select' ||
33
+ f.widget === 'dynamic_select' ||
34
+ f.type === 'search',
35
+ )
36
+ .map((f) => f.key),
37
+ )
38
+ const out: Record<string, any> = { ...values }
39
+ for (const [k, v] of Object.entries(out)) {
40
+ // A nil UUID is never a real target row — null it out on any field.
41
+ if (isNilUuid(v)) {
42
+ out[k] = null
43
+ continue
44
+ }
45
+ // An empty reference picker means "no relation" → null (not "").
46
+ if (refKeys.has(k) && (v === '' || v == null)) out[k] = null
47
+ }
48
+ return out
49
+ }
@@ -432,14 +432,28 @@ export function DynamicTable({
432
432
  // the table kept showing the previous filter. On every render where the
433
433
  // location's search differs from the last URL we ourselves wrote,
434
434
  // re-parse the `f_` params and adopt them.
435
- const locationSearch = typeof window !== 'undefined' ? window.location.search : ''
436
435
  useEffect(() => {
437
436
  if (!enableUrlSync || !initializedFromUrl.current) return
437
+ // Read the URL FRESH inside the effect — NOT a value captured during
438
+ // render. The write effect above mutates the URL imperatively via
439
+ // `history.replaceState`, which does NOT trigger a re-render, so a search
440
+ // string snapshotted at render time lags one commit behind what we just
441
+ // wrote. Comparing that stale snapshot against `lastSelfSearch` (which the
442
+ // write effect updated in this same commit) always mismatched, so the
443
+ // resync "adopted" a phantom external change and reset the filters — the
444
+ // write effect then re-wrote them, and the two effects ping-ponged the
445
+ // filters `{almacen:['Test']} <-> {}` on every render, spinning the
446
+ // infinite-scroll fetch forever the instant any column filter was applied.
447
+ // Reading fresh here means that right after our own write, `now` already
448
+ // equals `lastSelfSearch` and we bail out early. Only a genuine external
449
+ // rewrite (router deep-link from a sidebar sibling, browser back/forward)
450
+ // makes `now` diverge, which is exactly when we want to adopt it.
451
+ const now = typeof window !== 'undefined' ? window.location.search : ''
438
452
  // Semantic compare: the router's spelling of the query string we just
439
453
  // wrote (different order/encoding) is NOT an external change.
440
- if (canonicalSearch(locationSearch) === canonicalSearch(lastSelfSearch.current ?? '')) return
441
- lastSelfSearch.current = locationSearch
442
- const params = new URLSearchParams(locationSearch)
454
+ if (canonicalSearch(now) === canonicalSearch(lastSelfSearch.current ?? '')) return
455
+ lastSelfSearch.current = now
456
+ const params = new URLSearchParams(now)
443
457
  const filters: Record<string, string[]> = {}
444
458
  params.forEach((rawValue, key) => {
445
459
  if (!key.startsWith('f_')) return
@@ -449,13 +463,16 @@ export function DynamicTable({
449
463
  if (value.startsWith('IN:')) filters[filterKey] = value.substring(3).split(',')
450
464
  else filters[filterKey] = [value]
451
465
  })
452
- setDynamicFilters((prev: Record<string, string[]>) => {
453
- if (JSON.stringify(prev) === JSON.stringify(filters)) return prev
466
+ // Compare against the current filters directly (the effect re-runs every
467
+ // render, so this closure is always fresh) and only touch state on a real
468
+ // change — resetting pagination OUTSIDE any updater, never nested inside
469
+ // another setState's updater.
470
+ if (JSON.stringify(dynamicFilters) !== JSON.stringify(filters)) {
471
+ setDynamicFilters(filters)
454
472
  setPagination((p: PaginationState) => ({ ...p, pageIndex: 0 }))
455
- return filters
456
- })
473
+ }
457
474
  const search = params.get('search')
458
- if (search !== null) setGlobalFilter(search)
475
+ if (search !== null && search !== globalFilter) setGlobalFilter(search)
459
476
  })
460
477
 
461
478
  const prefetchOptions = useCallback(async (endpoints: string[]) => {
package/src/index.ts CHANGED
@@ -236,6 +236,7 @@ export {
236
236
  } from './collection-cell'
237
237
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid'
238
238
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record'
239
+ export { normalizeRefFieldsForSubmit } from './dialogs/normalize-submit'
239
240
  export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record'
240
241
  export { CreateRecordDialog } from './dialogs/create-record-dialog'
241
242
  export { ViewRecordDialog } from './dialogs/view-record-dialog'