@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 +24 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +15 -7
- package/dist/dialogs/normalize-submit.d.ts +17 -0
- package/dist/dialogs/normalize-submit.d.ts.map +1 -0
- package/dist/dialogs/normalize-submit.js +36 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +26 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +3 -3
- package/src/__tests__/normalize-ref-fields.test.ts +63 -0
- package/src/dialogs/dynamic-record.tsx +16 -7
- package/src/dialogs/normalize-submit.ts +49 -0
- package/src/dynamic-table.tsx +26 -9
- package/src/index.ts +1 -0
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":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
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(
|
|
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),
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,+
|
|
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"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -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(
|
|
337
|
+
if (canonicalSearch(now) === canonicalSearch(lastSelfSearch.current ?? ''))
|
|
324
338
|
return;
|
|
325
|
-
lastSelfSearch.current =
|
|
326
|
-
const params = new URLSearchParams(
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -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.
|
|
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-
|
|
71
|
-
"@asteby/metacore-
|
|
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(
|
|
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),
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -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(
|
|
441
|
-
lastSelfSearch.current =
|
|
442
|
-
const params = new URLSearchParams(
|
|
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
|
-
|
|
453
|
-
|
|
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
|
-
|
|
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'
|