@asteby/metacore-runtime-react 23.11.0 → 23.11.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,17 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 23.11.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 990bb63: DynamicForm oculta los campos readonly (estado de conexión, valores escritos por sync): el servidor los posee, el form no los captura ni los envía en el submit.
8
+
9
+ ## 23.11.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 6e3cf4f: DynamicTable: el sentinel de infinite scroll se detiene cuando el backend devuelve una página corta/vacía, aunque meta.total reporte más filas de las que la lista entrega (drift count vs list). Elimina el skeleton accent parpadeando bajo las filas.
14
+
3
15
  ## 23.11.0
4
16
 
5
17
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;AAO9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,+BAoFlB"}
1
+ {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;AAO9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,+BA8FlB"}
@@ -19,22 +19,28 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
19
19
  const [values, setValues] = useState({});
20
20
  const [errors, setErrors] = useState({});
21
21
  const [submitting, setSubmitting] = useState(false);
22
- const schema = useMemo(() => buildZodSchema(fields), [fields]);
22
+ // Readonly fields are system-owned (connection state, sync-written values):
23
+ // the server writes them, so the form never captures them — hidden outright
24
+ // instead of rendered disabled, matching the metadata contract
25
+ // (ColumnDef.Readonly). They are also excluded from defaults/submit so a
26
+ // create never posts e.g. `status: ""` over the column default.
27
+ const editableFields = useMemo(() => fields.filter((f) => !f.readonly), [fields]);
28
+ const schema = useMemo(() => buildZodSchema(editableFields), [editableFields]);
23
29
  // Line-items fields carrying a balance rule gate submit: an unbalanced entry
24
30
  // (Σdebit ≠ Σcredit, or all-zero when require_nonzero) can't be saved. This
25
31
  // is fully declarative — `evaluateBalance` returns undefined for fields with
26
32
  // no rule, so non-balanced forms are unaffected.
27
33
  const balanceBlocked = useMemo(() => {
28
- for (const f of fields) {
34
+ for (const f of editableFields) {
29
35
  const state = evaluateBalance(f, values[f.key]);
30
36
  if (state && !state.balanced)
31
37
  return true;
32
38
  }
33
39
  return false;
34
- }, [fields, values]);
40
+ }, [editableFields, values]);
35
41
  useEffect(() => {
36
42
  const defaults = {};
37
- for (const f of fields) {
43
+ for (const f of editableFields) {
38
44
  if (isLineItemsField(f)) {
39
45
  defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? [];
40
46
  continue;
@@ -43,7 +49,7 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
43
49
  }
44
50
  setValues(defaults);
45
51
  setErrors({});
46
- }, [fields, initialValues]);
52
+ }, [editableFields, initialValues]);
47
53
  const update = (k, v) => setValues((prev) => ({ ...prev, [k]: v }));
48
54
  const handleSubmit = async (e) => {
49
55
  e.preventDefault();
@@ -73,7 +79,7 @@ export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitL
73
79
  // line-items grids (and textareas) span the full width so the row table /
74
80
  // memo gets room. Mirrors the pro look of the federated journal modal but
75
81
  // stays fully declarative — driven only by field shape.
76
- return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: fields.map((field) => (_jsx(FieldRow, { field: field, value: values[field.key], onChange: (v) => update(field.key, v), values: values, error: errors[field.key], initialValues: initialValues }, field.key))) }), _jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })), _jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })] })] }));
82
+ return (_jsxs("form", { onSubmit: handleSubmit, className: "grid gap-4", children: [_jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: editableFields.map((field) => (_jsx(FieldRow, { field: field, value: values[field.key], onChange: (v) => update(field.key, v), values: values, error: errors[field.key], initialValues: initialValues }, field.key))) }), _jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: submitting || disabled, children: cancelLabel })), _jsx(Button, { type: "submit", disabled: submitting || disabled || balanceBlocked, children: submitLabel })] })] }));
77
83
  }
78
84
  // One form field row: label + renderer + inline error. Encapsulated as its own
79
85
  // component so a STATIC enum select whose options are all gated out by a sibling
@@ -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,+BA8iCnB"}
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"}
@@ -50,6 +50,10 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
50
50
  // Infinite-scroll: a top-up page is in flight (distinct from the initial
51
51
  // page load so only a small bottom spinner shows, not the whole-table one).
52
52
  const [loadingMore, setLoadingMore] = useState(false);
53
+ // True once the backend returned a short/empty page: no more rows exist
54
+ // even if meta.total says otherwise (count/list drift would otherwise
55
+ // keep the sentinel re-firing forever). Reset by any page-1 fetch.
56
+ const [infExhausted, setInfExhausted] = useState(false);
53
57
  const infPageRef = useRef(1);
54
58
  const [optionsMap, setOptionsMap] = useState(new Map());
55
59
  const [exportOpen, setExportOpen] = useState(false);
@@ -467,6 +471,11 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
467
471
  setData((prev) => (append ? dedupeById(prev, rows) : rows));
468
472
  if (res.data.meta)
469
473
  setRowCount(res.data.meta.total);
474
+ // A short page means the backend has no more rows, even if
475
+ // meta.total disagrees with the visible count (count query
476
+ // vs list query drift, dedupe). Without this the sentinel
477
+ // re-fires forever on empty pages.
478
+ setInfExhausted(rows.length < infPageSize);
470
479
  }
471
480
  }
472
481
  catch (error) {
@@ -483,19 +492,19 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
483
492
  // the filters/search AND the sort (both live in buildFilterParams).
484
493
  const filterSignature = useMemo(() => JSON.stringify(buildFilterParams()), [buildFilterParams]);
485
494
  const loadNextPage = useCallback(() => {
486
- if (loadingMore || loadingData)
495
+ if (loadingMore || loadingData || infExhausted)
487
496
  return;
488
497
  if (data.length >= rowCount)
489
498
  return;
490
499
  infPageRef.current += 1;
491
500
  void fetchPage(infPageRef.current, true);
492
- }, [loadingMore, loadingData, data.length, rowCount, fetchPage]);
501
+ }, [loadingMore, loadingData, infExhausted, data.length, rowCount, fetchPage]);
493
502
  // Infinite-scroll sentinels. There are two scroll containers (desktop
494
503
  // table + mobile card list) but only one is laid out at a time — the CSS
495
504
  // `hidden`/`sm:hidden` container has no box, so its observer never fires.
496
505
  // Each sentinel drives the SAME `loadNextPage`; its internal guards + the
497
506
  // `disabled` flag keep concurrent/exhausted fetches from doubling up.
498
- const infScrollDisabled = !infiniteScroll || loadingMore || loadingData || data.length >= rowCount;
507
+ const infScrollDisabled = !infiniteScroll || loadingMore || loadingData || infExhausted || data.length >= rowCount;
499
508
  const { rootRef: infDesktopRoot, sentinelRef: infDesktopSentinel } = useInfiniteScrollSentinel({ onLoadMore: loadNextPage, disabled: infScrollDisabled });
500
509
  const { rootRef: infMobileRoot, sentinelRef: infMobileSentinel } = useInfiniteScrollSentinel({ onLoadMore: loadNextPage, disabled: infScrollDisabled });
501
510
  const initialFetchDone = useRef(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "23.11.0",
3
+ "version": "23.11.2",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -58,23 +58,33 @@ export function DynamicForm({
58
58
  const [errors, setErrors] = useState<Record<string, string>>({})
59
59
  const [submitting, setSubmitting] = useState(false)
60
60
 
61
- const schema = useMemo(() => buildZodSchema(fields), [fields])
61
+ // Readonly fields are system-owned (connection state, sync-written values):
62
+ // the server writes them, so the form never captures them — hidden outright
63
+ // instead of rendered disabled, matching the metadata contract
64
+ // (ColumnDef.Readonly). They are also excluded from defaults/submit so a
65
+ // create never posts e.g. `status: ""` over the column default.
66
+ const editableFields = useMemo(
67
+ () => fields.filter((f) => !(f as { readonly?: boolean }).readonly),
68
+ [fields],
69
+ )
70
+
71
+ const schema = useMemo(() => buildZodSchema(editableFields), [editableFields])
62
72
 
63
73
  // Line-items fields carrying a balance rule gate submit: an unbalanced entry
64
74
  // (Σdebit ≠ Σcredit, or all-zero when require_nonzero) can't be saved. This
65
75
  // is fully declarative — `evaluateBalance` returns undefined for fields with
66
76
  // no rule, so non-balanced forms are unaffected.
67
77
  const balanceBlocked = useMemo(() => {
68
- for (const f of fields) {
78
+ for (const f of editableFields) {
69
79
  const state = evaluateBalance(f, values[f.key])
70
80
  if (state && !state.balanced) return true
71
81
  }
72
82
  return false
73
- }, [fields, values])
83
+ }, [editableFields, values])
74
84
 
75
85
  useEffect(() => {
76
86
  const defaults: Record<string, any> = {}
77
- for (const f of fields) {
87
+ for (const f of editableFields) {
78
88
  if (isLineItemsField(f)) {
79
89
  defaults[f.key] = initialValues?.[f.key] ?? f.defaultValue ?? []
80
90
  continue
@@ -83,7 +93,7 @@ export function DynamicForm({
83
93
  }
84
94
  setValues(defaults)
85
95
  setErrors({})
86
- }, [fields, initialValues])
96
+ }, [editableFields, initialValues])
87
97
 
88
98
  const update = (k: string, v: any) =>
89
99
  setValues((prev: Record<string, any>) => ({ ...prev, [k]: v }))
@@ -113,7 +123,7 @@ export function DynamicForm({
113
123
  return (
114
124
  <form onSubmit={handleSubmit} className="grid gap-4">
115
125
  <div className="grid gap-4 sm:grid-cols-2">
116
- {fields.map((field) => (
126
+ {editableFields.map((field) => (
117
127
  <FieldRow
118
128
  key={field.key}
119
129
  field={field}
@@ -157,6 +157,10 @@ export function DynamicTable({
157
157
  // Infinite-scroll: a top-up page is in flight (distinct from the initial
158
158
  // page load so only a small bottom spinner shows, not the whole-table one).
159
159
  const [loadingMore, setLoadingMore] = useState(false)
160
+ // True once the backend returned a short/empty page: no more rows exist
161
+ // even if meta.total says otherwise (count/list drift would otherwise
162
+ // keep the sentinel re-firing forever). Reset by any page-1 fetch.
163
+ const [infExhausted, setInfExhausted] = useState(false)
160
164
  const infPageRef = useRef(1)
161
165
  const [optionsMap, setOptionsMap] = useState<Map<string, any[]>>(new Map())
162
166
 
@@ -546,6 +550,11 @@ export function DynamicTable({
546
550
  const rows = res.data.data || []
547
551
  setData((prev) => (append ? dedupeById(prev, rows) : rows))
548
552
  if (res.data.meta) setRowCount(res.data.meta.total)
553
+ // A short page means the backend has no more rows, even if
554
+ // meta.total disagrees with the visible count (count query
555
+ // vs list query drift, dedupe). Without this the sentinel
556
+ // re-fires forever on empty pages.
557
+ setInfExhausted(rows.length < infPageSize)
549
558
  }
550
559
  } catch (error) {
551
560
  console.error('Error al cargar los datos', error)
@@ -565,11 +574,11 @@ export function DynamicTable({
565
574
  )
566
575
 
567
576
  const loadNextPage = useCallback(() => {
568
- if (loadingMore || loadingData) return
577
+ if (loadingMore || loadingData || infExhausted) return
569
578
  if (data.length >= rowCount) return
570
579
  infPageRef.current += 1
571
580
  void fetchPage(infPageRef.current, true)
572
- }, [loadingMore, loadingData, data.length, rowCount, fetchPage])
581
+ }, [loadingMore, loadingData, infExhausted, data.length, rowCount, fetchPage])
573
582
 
574
583
  // Infinite-scroll sentinels. There are two scroll containers (desktop
575
584
  // table + mobile card list) but only one is laid out at a time — the CSS
@@ -577,7 +586,7 @@ export function DynamicTable({
577
586
  // Each sentinel drives the SAME `loadNextPage`; its internal guards + the
578
587
  // `disabled` flag keep concurrent/exhausted fetches from doubling up.
579
588
  const infScrollDisabled =
580
- !infiniteScroll || loadingMore || loadingData || data.length >= rowCount
589
+ !infiniteScroll || loadingMore || loadingData || infExhausted || data.length >= rowCount
581
590
  const { rootRef: infDesktopRoot, sentinelRef: infDesktopSentinel } =
582
591
  useInfiniteScrollSentinel({ onLoadMore: loadNextPage, disabled: infScrollDisabled })
583
592
  const { rootRef: infMobileRoot, sentinelRef: infMobileSentinel } =