@open-mercato/ui 0.6.8-develop.6930.1.1e5976efc3 → 0.6.8-develop.6940.1.177ea30c6e

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.
@@ -423,6 +423,14 @@ function CrudForm({
423
423
  () => ({ ...initialValues ?? {} })
424
424
  );
425
425
  const valuesRef = React.useRef(values);
426
+ const pendingFieldChangeEventsRef = React.useRef([]);
427
+ const nextFieldChangeEventIdRef = React.useRef(0);
428
+ const fieldChangeRevisionByFieldRef = React.useRef(/* @__PURE__ */ new Map());
429
+ const fieldChangeFormGenerationRef = React.useRef(0);
430
+ const fieldChangeFormSnapshotRef = React.useRef(void 0);
431
+ const fieldChangeDispatcherMountedRef = React.useRef(false);
432
+ const activeFieldChangeDispatchRef = React.useRef(null);
433
+ const [fieldChangeDispatchVersion, setFieldChangeDispatchVersion] = React.useState(0);
426
434
  const [errors, setErrors] = React.useState({});
427
435
  const [pending, setPending] = React.useState(false);
428
436
  const submittingRef = React.useRef(false);
@@ -496,9 +504,32 @@ function CrudForm({
496
504
  React.useEffect(() => {
497
505
  injectionContextRef.current = injectionContext;
498
506
  }, [injectionContext]);
499
- React.useEffect(() => {
507
+ React.useLayoutEffect(() => {
500
508
  valuesRef.current = values;
501
509
  }, [values]);
510
+ const fieldChangeFormSnapshot = React.useMemo(
511
+ () => createDirtySnapshot(initialValues ?? {}),
512
+ [initialValues]
513
+ );
514
+ React.useLayoutEffect(() => {
515
+ const fieldChangeRevisions = fieldChangeRevisionByFieldRef.current;
516
+ fieldChangeDispatcherMountedRef.current = true;
517
+ return () => {
518
+ fieldChangeDispatcherMountedRef.current = false;
519
+ fieldChangeFormGenerationRef.current += 1;
520
+ pendingFieldChangeEventsRef.current = [];
521
+ fieldChangeRevisions.clear();
522
+ activeFieldChangeDispatchRef.current = null;
523
+ };
524
+ }, []);
525
+ React.useLayoutEffect(() => {
526
+ if (fieldChangeFormSnapshotRef.current === fieldChangeFormSnapshot) return;
527
+ fieldChangeFormSnapshotRef.current = fieldChangeFormSnapshot;
528
+ fieldChangeFormGenerationRef.current += 1;
529
+ pendingFieldChangeEventsRef.current = [];
530
+ fieldChangeRevisionByFieldRef.current.clear();
531
+ activeFieldChangeDispatchRef.current = null;
532
+ }, [fieldChangeFormSnapshot]);
502
533
  const isDirtyRef = React.useRef(false);
503
534
  const navigationPromptBypassRef = React.useRef(false);
504
535
  const navigationConfirmPendingRef = React.useRef(false);
@@ -563,6 +594,7 @@ function CrudForm({
563
594
  }, [clearSubmitNavigationBypass]);
564
595
  const clearDirtyState = React.useCallback((snapshotSource) => {
565
596
  const source = snapshotSource ?? valuesRef.current;
597
+ pendingDirtyBaselineCommitRef.current = null;
566
598
  dirtyBaselineSnapshotRef.current = createDirtySnapshot(source);
567
599
  dirtyBaselineValuesRef.current = { ...source };
568
600
  userEditedFieldIdsRef.current.clear();
@@ -1713,29 +1745,30 @@ function CrudForm({
1713
1745
  everEditedFieldIdsRef.current.add(id);
1714
1746
  }, []);
1715
1747
  const setValue = React.useCallback((id, nextValue) => {
1716
- let nextData = null;
1717
- let nextDirty = null;
1718
- const currentValue = valuesRef.current[id];
1719
- if (!Object.is(currentValue, nextValue)) {
1720
- updateEditedFieldMarker(id, nextValue);
1721
- }
1722
- setValues((prev) => {
1723
- if (Object.is(prev[id], nextValue)) return prev;
1724
- const baselineSource = dirtyBaselineValuesRef.current ?? prev;
1748
+ const currentValues = valuesRef.current;
1749
+ if (!Object.is(currentValues[id], nextValue)) {
1750
+ pendingDirtyBaselineCommitRef.current = null;
1751
+ const baselineSource = dirtyBaselineValuesRef.current ?? currentValues;
1725
1752
  updateEditedFieldMarker(id, nextValue, baselineSource);
1726
- nextData = { ...prev, [id]: nextValue };
1727
- valuesRef.current = nextData;
1728
1753
  if (!(embedded && !trackDirtyWhenEmbedded)) {
1729
- const baseline = dirtyBaselineSnapshotRef.current ?? createDirtySnapshot(prev);
1730
- dirtyBaselineSnapshotRef.current = baseline;
1754
+ dirtyBaselineSnapshotRef.current ??= createDirtySnapshot(currentValues);
1731
1755
  dirtyBaselineValuesRef.current = baselineSource;
1732
- nextDirty = createDirtySnapshot(nextData) !== baseline;
1733
1756
  }
1734
- return nextData;
1735
- });
1736
- if (nextDirty !== null) {
1737
- isDirtyRef.current = nextDirty;
1738
- setHasUnsavedChanges(nextDirty);
1757
+ if (extendedInjectionEventsEnabled) {
1758
+ const fieldRevision = (fieldChangeRevisionByFieldRef.current.get(id) ?? 0) + 1;
1759
+ fieldChangeRevisionByFieldRef.current.set(id, fieldRevision);
1760
+ nextFieldChangeEventIdRef.current += 1;
1761
+ pendingFieldChangeEventsRef.current.push({
1762
+ eventId: nextFieldChangeEventIdRef.current,
1763
+ fieldId: id,
1764
+ fieldValue: nextValue,
1765
+ fieldRevision,
1766
+ fieldRevisionsAtEnqueue: new Map(fieldChangeRevisionByFieldRef.current),
1767
+ formGeneration: fieldChangeFormGenerationRef.current
1768
+ });
1769
+ setFieldChangeDispatchVersion((version) => version + 1);
1770
+ }
1771
+ setValues((prev) => Object.is(prev[id], nextValue) ? prev : { ...prev, [id]: nextValue });
1739
1772
  }
1740
1773
  const clearedMessages = [];
1741
1774
  setErrors((prev) => {
@@ -1766,19 +1799,43 @@ function CrudForm({
1766
1799
  return prev;
1767
1800
  });
1768
1801
  }
1769
- if (!nextData || !extendedInjectionEventsEnabled) return;
1770
- void triggerInjectionEvent("onFieldChange", nextData, injectionContextRef.current, {
1771
- fieldId: id,
1772
- fieldValue: nextValue
1802
+ }, [embedded, extendedInjectionEventsEnabled, t, trackDirtyWhenEmbedded, translateValidationMessage, updateEditedFieldMarker]);
1803
+ React.useEffect(() => {
1804
+ if (!extendedInjectionEventsEnabled) {
1805
+ pendingFieldChangeEventsRef.current = [];
1806
+ return;
1807
+ }
1808
+ if (!fieldChangeDispatcherMountedRef.current || activeFieldChangeDispatchRef.current) return;
1809
+ let pendingEvent = pendingFieldChangeEventsRef.current.shift();
1810
+ while (pendingEvent && pendingEvent.formGeneration !== fieldChangeFormGenerationRef.current) {
1811
+ pendingEvent = pendingFieldChangeEventsRef.current.shift();
1812
+ }
1813
+ if (!pendingEvent) return;
1814
+ const dispatchToken = {
1815
+ eventId: pendingEvent.eventId,
1816
+ formGeneration: pendingEvent.formGeneration
1817
+ };
1818
+ activeFieldChangeDispatchRef.current = dispatchToken;
1819
+ void triggerInjectionEvent("onFieldChange", values, injectionContextRef.current, {
1820
+ fieldId: pendingEvent.fieldId,
1821
+ fieldValue: pendingEvent.fieldValue
1773
1822
  }).then((result) => {
1823
+ const isCurrentEvent = fieldChangeDispatcherMountedRef.current && fieldChangeFormGenerationRef.current === pendingEvent.formGeneration && fieldChangeRevisionByFieldRef.current.get(pendingEvent.fieldId) === pendingEvent.fieldRevision;
1824
+ if (!isCurrentEvent) return;
1774
1825
  if (!result.ok) return;
1775
1826
  const change = result.fieldChange;
1776
1827
  if (!change) return;
1777
- const updates = { ...change.sideEffects ?? {} };
1828
+ const candidateUpdates = { ...change.sideEffects ?? {} };
1778
1829
  if (change.value !== void 0) {
1779
- updates[id] = change.value;
1830
+ candidateUpdates[pendingEvent.fieldId] = change.value;
1831
+ }
1832
+ const updates = {};
1833
+ for (const [fieldId, value] of Object.entries(candidateUpdates)) {
1834
+ if (fieldChangeRevisionByFieldRef.current.get(fieldId) !== pendingEvent.fieldRevisionsAtEnqueue.get(fieldId)) continue;
1835
+ updates[fieldId] = value;
1780
1836
  }
1781
1837
  if (Object.keys(updates).length > 0) {
1838
+ pendingDirtyBaselineCommitRef.current = null;
1782
1839
  setValues((prev) => {
1783
1840
  let changed = false;
1784
1841
  const next = { ...prev };
@@ -1795,8 +1852,16 @@ function CrudForm({
1795
1852
  }
1796
1853
  }).catch((err) => {
1797
1854
  logger.error("Error in onFieldChange", { err });
1855
+ }).finally(() => {
1856
+ const activeDispatch = activeFieldChangeDispatchRef.current;
1857
+ if (activeDispatch?.eventId === dispatchToken.eventId && activeDispatch.formGeneration === dispatchToken.formGeneration) {
1858
+ activeFieldChangeDispatchRef.current = null;
1859
+ }
1860
+ if (fieldChangeDispatcherMountedRef.current && fieldChangeFormGenerationRef.current === dispatchToken.formGeneration) {
1861
+ setFieldChangeDispatchVersion((version) => version + 1);
1862
+ }
1798
1863
  });
1799
- }, [embedded, extendedInjectionEventsEnabled, flash, t, trackDirtyWhenEmbedded, translateValidationMessage, triggerInjectionEvent, updateEditedFieldMarker]);
1864
+ }, [extendedInjectionEventsEnabled, fieldChangeDispatchVersion, triggerInjectionEvent, values]);
1800
1865
  const onBlurRequest = React.useCallback((fieldId) => {
1801
1866
  void validateFieldOnBlur(fieldId);
1802
1867
  }, [validateFieldOnBlur]);
@@ -1825,67 +1890,84 @@ function CrudForm({
1825
1890
  [buildCustomFieldsManageHref, customFieldsManageMode]
1826
1891
  );
1827
1892
  const appliedInitialValuesSnapshotRef = React.useRef(void 0);
1893
+ const initialValuesTransformMountedRef = React.useRef(true);
1828
1894
  const dirtyBaselineSnapshotRef = React.useRef(void 0);
1829
1895
  const dirtyBaselineValuesRef = React.useRef(void 0);
1896
+ const pendingDirtyBaselineCommitRef = React.useRef(null);
1830
1897
  const userEditedFieldIdsRef = React.useRef(/* @__PURE__ */ new Set());
1831
1898
  const everEditedFieldIdsRef = React.useRef(/* @__PURE__ */ new Set());
1899
+ React.useLayoutEffect(() => {
1900
+ initialValuesTransformMountedRef.current = true;
1901
+ return () => {
1902
+ initialValuesTransformMountedRef.current = false;
1903
+ };
1904
+ }, []);
1905
+ React.useLayoutEffect(() => {
1906
+ const pendingBaseline = pendingDirtyBaselineCommitRef.current;
1907
+ if (!pendingBaseline) return;
1908
+ pendingDirtyBaselineCommitRef.current = null;
1909
+ if (pendingBaseline.guardSnapshot !== void 0 && appliedInitialValuesSnapshotRef.current !== pendingBaseline.guardSnapshot) return;
1910
+ const committedValues = values;
1911
+ if (createDirtySnapshot(committedValues) !== pendingBaseline.valuesSnapshot) return;
1912
+ dirtyBaselineSnapshotRef.current = pendingBaseline.valuesSnapshot;
1913
+ dirtyBaselineValuesRef.current = { ...pendingBaseline.values };
1914
+ }, [values]);
1832
1915
  React.useLayoutEffect(() => {
1833
1916
  if (!initialValues) return;
1834
1917
  const snapshot = JSON.stringify({
1835
1918
  initialValues,
1836
1919
  injectedFieldIds: injectedFieldDefinitions.map((definition) => definition.id),
1920
+ injectionWidgetIds: injectionWidgets.map((widget) => `${widget.moduleId}:${widget.widgetId}:${widget.key}`),
1837
1921
  customFieldMappings: cfDefinitions.map((definition) => definition.key),
1838
1922
  dotPathBaseFieldIds: Array.from(dotPathBaseFieldIds)
1839
1923
  });
1840
1924
  if (appliedInitialValuesSnapshotRef.current === snapshot) return;
1841
1925
  appliedInitialValuesSnapshotRef.current = snapshot;
1842
1926
  const initialRecord = initialValues;
1843
- let mergedValues = null;
1844
- let hadUnsavedEdits = false;
1845
- setValues((prev) => {
1846
- const priorBaseline = dirtyBaselineSnapshotRef.current;
1847
- const editedFieldIds = userEditedFieldIdsRef.current;
1848
- hadUnsavedEdits = editedFieldIds.size > 0 || priorBaseline !== void 0 && createDirtySnapshot(prev) !== priorBaseline;
1849
- const merged = { ...prev };
1850
- const mergedRecord = merged;
1851
- for (const [key, value] of Object.entries(initialValues)) {
1852
- if (editedFieldIds.has(key)) continue;
1853
- mergedRecord[key] = value;
1854
- }
1855
- for (const definition of injectedFieldDefinitions) {
1856
- if (merged[definition.id] !== void 0) continue;
1857
- const extracted = readByDotPath(initialRecord, definition.id);
1858
- if (extracted !== void 0) {
1859
- ;
1860
- merged[definition.id] = extracted;
1861
- }
1927
+ const currentValues = valuesRef.current;
1928
+ const priorBaseline = dirtyBaselineSnapshotRef.current;
1929
+ const editedFieldIds = userEditedFieldIdsRef.current;
1930
+ const hadUnsavedEdits = editedFieldIds.size > 0 || priorBaseline !== void 0 && createDirtySnapshot(currentValues) !== priorBaseline;
1931
+ const mergedValues = { ...currentValues };
1932
+ const mergedRecord = mergedValues;
1933
+ for (const [key, value] of Object.entries(initialValues)) {
1934
+ if (editedFieldIds.has(key)) continue;
1935
+ mergedRecord[key] = value;
1936
+ }
1937
+ for (const definition of injectedFieldDefinitions) {
1938
+ if (mergedValues[definition.id] !== void 0) continue;
1939
+ const extracted = readByDotPath(initialRecord, definition.id);
1940
+ if (extracted !== void 0) {
1941
+ mergedRecord[definition.id] = extracted;
1862
1942
  }
1863
- for (const definition of cfDefinitions) {
1864
- const targetId = customEntity ? definition.key : `cf_${definition.key}`;
1865
- if (!targetId || merged[targetId] !== void 0) continue;
1866
- const extracted = readInitialCustomFieldValue(initialRecord, definition.key);
1867
- if (extracted !== void 0) {
1868
- ;
1869
- merged[targetId] = extracted;
1870
- }
1943
+ }
1944
+ for (const definition of cfDefinitions) {
1945
+ const targetId = customEntity ? definition.key : `cf_${definition.key}`;
1946
+ if (!targetId || mergedValues[targetId] !== void 0) continue;
1947
+ const extracted = readInitialCustomFieldValue(initialRecord, definition.key);
1948
+ if (extracted !== void 0) {
1949
+ mergedRecord[targetId] = extracted;
1871
1950
  }
1872
- for (const fieldId of dotPathBaseFieldIds) {
1873
- if (merged[fieldId] !== void 0) continue;
1874
- const extracted = readByDotPath(initialRecord, fieldId);
1875
- if (extracted !== void 0) {
1876
- ;
1877
- merged[fieldId] = extracted;
1878
- }
1951
+ }
1952
+ for (const fieldId of dotPathBaseFieldIds) {
1953
+ if (mergedValues[fieldId] !== void 0) continue;
1954
+ const extracted = readByDotPath(initialRecord, fieldId);
1955
+ if (extracted !== void 0) {
1956
+ mergedRecord[fieldId] = extracted;
1879
1957
  }
1880
- mergedValues = merged;
1881
- return mergedValues;
1882
- });
1883
- if (mergedValues && !hadUnsavedEdits) {
1884
- dirtyBaselineSnapshotRef.current = createDirtySnapshot(mergedValues);
1885
- dirtyBaselineValuesRef.current = { ...mergedValues };
1886
1958
  }
1887
- if (!extendedInjectionEventsEnabled || !mergedValues) return;
1888
- let cancelled = false;
1959
+ if (!hadUnsavedEdits) {
1960
+ pendingDirtyBaselineCommitRef.current = {
1961
+ guardSnapshot: snapshot,
1962
+ valuesSnapshot: createDirtySnapshot(mergedValues),
1963
+ values: { ...mergedValues }
1964
+ };
1965
+ } else {
1966
+ pendingDirtyBaselineCommitRef.current = null;
1967
+ }
1968
+ setValues(mergedValues);
1969
+ if (!extendedInjectionEventsEnabled || hadUnsavedEdits) return;
1970
+ const transformSourceSnapshot = createDirtySnapshot(mergedValues);
1889
1971
  const run = async () => {
1890
1972
  try {
1891
1973
  const result = await triggerInjectionEvent(
@@ -1894,19 +1976,23 @@ function CrudForm({
1894
1976
  injectionContextRef.current
1895
1977
  );
1896
1978
  const transformed = result.data;
1897
- if (cancelled || !transformed) return;
1898
- if (isDirtyRef.current) return;
1899
- dirtyBaselineSnapshotRef.current = createDirtySnapshot(transformed);
1900
- dirtyBaselineValuesRef.current = { ...transformed };
1901
- setValues(transformed);
1979
+ if (!initialValuesTransformMountedRef.current || appliedInitialValuesSnapshotRef.current !== snapshot || !transformed) return;
1980
+ if (createDirtySnapshot(valuesRef.current) !== transformSourceSnapshot) return;
1981
+ const transformedValues = transformed;
1982
+ pendingDirtyBaselineCommitRef.current = {
1983
+ guardSnapshot: snapshot,
1984
+ valuesSnapshot: createDirtySnapshot(transformedValues),
1985
+ values: { ...transformedValues }
1986
+ };
1987
+ setValues((current) => {
1988
+ if (createDirtySnapshot(current) !== transformSourceSnapshot) return current;
1989
+ return transformedValues;
1990
+ });
1902
1991
  } catch (err) {
1903
1992
  logger.error("Error in transformDisplayData", { err });
1904
1993
  }
1905
1994
  };
1906
1995
  void run();
1907
- return () => {
1908
- cancelled = true;
1909
- };
1910
1996
  }, [
1911
1997
  cfDefinitions,
1912
1998
  customEntity,
@@ -1914,6 +2000,7 @@ function CrudForm({
1914
2000
  extendedInjectionEventsEnabled,
1915
2001
  initialValues,
1916
2002
  injectedFieldDefinitions,
2003
+ injectionWidgets,
1917
2004
  triggerInjectionEvent
1918
2005
  ]);
1919
2006
  const cfDefaultsAppliedRef = React.useRef(false);
@@ -1936,23 +2023,20 @@ function CrudForm({
1936
2023
  defaults[fieldId] = def.defaultValue;
1937
2024
  }
1938
2025
  if (Object.keys(defaults).length === 0) return;
1939
- let mergedValues = null;
1940
- setValues((prev) => {
1941
- const merged = { ...prev };
1942
- let applied = false;
1943
- for (const [fieldId, defaultVal] of Object.entries(defaults)) {
1944
- if (merged[fieldId] !== void 0) continue;
1945
- merged[fieldId] = defaultVal;
1946
- applied = true;
1947
- }
1948
- if (!applied) return prev;
1949
- mergedValues = merged;
1950
- return mergedValues;
1951
- });
1952
- if (mergedValues) {
1953
- dirtyBaselineSnapshotRef.current = createDirtySnapshot(mergedValues);
1954
- dirtyBaselineValuesRef.current = { ...mergedValues };
2026
+ const currentValues = valuesRef.current;
2027
+ const mergedValues = { ...currentValues };
2028
+ let applied = false;
2029
+ for (const [fieldId, defaultVal] of Object.entries(defaults)) {
2030
+ if (mergedValues[fieldId] !== void 0) continue;
2031
+ mergedValues[fieldId] = defaultVal;
2032
+ applied = true;
1955
2033
  }
2034
+ if (!applied) return;
2035
+ pendingDirtyBaselineCommitRef.current = {
2036
+ valuesSnapshot: createDirtySnapshot(mergedValues),
2037
+ values: { ...mergedValues }
2038
+ };
2039
+ setValues(mergedValues);
1956
2040
  }, [isLoading, initialValuesHasId, cfDefinitions, customEntity]);
1957
2041
  const markFormAsClean = React.useCallback((snapshotSource) => {
1958
2042
  clearDirtyState(snapshotSource);