@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.
- package/dist/backend/CrudForm.js +177 -93
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/injection/InjectionSpot.js +13 -4
- package/dist/backend/injection/InjectionSpot.js.map +2 -2
- package/dist/backend/injection/useRegisteredComponent.js +48 -36
- package/dist/backend/injection/useRegisteredComponent.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/CrudForm.tsx +216 -103
- package/src/backend/__tests__/CrudForm.asyncInjectionTransform.test.tsx +555 -0
- package/src/backend/injection/InjectionSpot.tsx +13 -5
- package/src/backend/injection/__tests__/InjectionSpot.features.test.tsx +26 -0
- package/src/backend/injection/__tests__/useRegisteredComponent.remount.test.tsx +96 -0
- package/src/backend/injection/useRegisteredComponent.tsx +85 -38
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -777,6 +777,21 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
777
777
|
() => ({ ...(initialValues ?? {}) } as CrudFormValues<TValues>)
|
|
778
778
|
)
|
|
779
779
|
const valuesRef = React.useRef(values)
|
|
780
|
+
const pendingFieldChangeEventsRef = React.useRef<Array<{
|
|
781
|
+
eventId: number
|
|
782
|
+
fieldId: string
|
|
783
|
+
fieldValue: unknown
|
|
784
|
+
fieldRevision: number
|
|
785
|
+
fieldRevisionsAtEnqueue: Map<string, number>
|
|
786
|
+
formGeneration: number
|
|
787
|
+
}>>([])
|
|
788
|
+
const nextFieldChangeEventIdRef = React.useRef(0)
|
|
789
|
+
const fieldChangeRevisionByFieldRef = React.useRef(new Map<string, number>())
|
|
790
|
+
const fieldChangeFormGenerationRef = React.useRef(0)
|
|
791
|
+
const fieldChangeFormSnapshotRef = React.useRef<string | undefined>(undefined)
|
|
792
|
+
const fieldChangeDispatcherMountedRef = React.useRef(false)
|
|
793
|
+
const activeFieldChangeDispatchRef = React.useRef<{ eventId: number; formGeneration: number } | null>(null)
|
|
794
|
+
const [fieldChangeDispatchVersion, setFieldChangeDispatchVersion] = React.useState(0)
|
|
780
795
|
const [errors, setErrors] = React.useState<Record<string, string>>({})
|
|
781
796
|
const [pending, setPending] = React.useState(false)
|
|
782
797
|
// Synchronous guard against re-entrant submit/delete invocations (e.g. rapid
|
|
@@ -880,9 +895,32 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
880
895
|
React.useEffect(() => {
|
|
881
896
|
injectionContextRef.current = injectionContext
|
|
882
897
|
}, [injectionContext])
|
|
883
|
-
React.
|
|
898
|
+
React.useLayoutEffect(() => {
|
|
884
899
|
valuesRef.current = values
|
|
885
900
|
}, [values])
|
|
901
|
+
const fieldChangeFormSnapshot = React.useMemo(
|
|
902
|
+
() => createDirtySnapshot((initialValues ?? {}) as Record<string, unknown>),
|
|
903
|
+
[initialValues],
|
|
904
|
+
)
|
|
905
|
+
React.useLayoutEffect(() => {
|
|
906
|
+
const fieldChangeRevisions = fieldChangeRevisionByFieldRef.current
|
|
907
|
+
fieldChangeDispatcherMountedRef.current = true
|
|
908
|
+
return () => {
|
|
909
|
+
fieldChangeDispatcherMountedRef.current = false
|
|
910
|
+
fieldChangeFormGenerationRef.current += 1
|
|
911
|
+
pendingFieldChangeEventsRef.current = []
|
|
912
|
+
fieldChangeRevisions.clear()
|
|
913
|
+
activeFieldChangeDispatchRef.current = null
|
|
914
|
+
}
|
|
915
|
+
}, [])
|
|
916
|
+
React.useLayoutEffect(() => {
|
|
917
|
+
if (fieldChangeFormSnapshotRef.current === fieldChangeFormSnapshot) return
|
|
918
|
+
fieldChangeFormSnapshotRef.current = fieldChangeFormSnapshot
|
|
919
|
+
fieldChangeFormGenerationRef.current += 1
|
|
920
|
+
pendingFieldChangeEventsRef.current = []
|
|
921
|
+
fieldChangeRevisionByFieldRef.current.clear()
|
|
922
|
+
activeFieldChangeDispatchRef.current = null
|
|
923
|
+
}, [fieldChangeFormSnapshot])
|
|
886
924
|
|
|
887
925
|
const isDirtyRef = React.useRef(false)
|
|
888
926
|
const navigationPromptBypassRef = React.useRef(false)
|
|
@@ -957,6 +995,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
957
995
|
|
|
958
996
|
const clearDirtyState = React.useCallback((snapshotSource?: Record<string, unknown>) => {
|
|
959
997
|
const source = snapshotSource ?? (valuesRef.current as Record<string, unknown>)
|
|
998
|
+
pendingDirtyBaselineCommitRef.current = null
|
|
960
999
|
dirtyBaselineSnapshotRef.current = createDirtySnapshot(source)
|
|
961
1000
|
dirtyBaselineValuesRef.current = { ...source }
|
|
962
1001
|
userEditedFieldIdsRef.current.clear()
|
|
@@ -2315,29 +2354,32 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2315
2354
|
}, [])
|
|
2316
2355
|
|
|
2317
2356
|
const setValue = React.useCallback((id: string, nextValue: unknown) => {
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
updateEditedFieldMarker(id, nextValue)
|
|
2323
|
-
}
|
|
2324
|
-
setValues((prev) => {
|
|
2325
|
-
if (Object.is(prev[id], nextValue)) return prev
|
|
2326
|
-
const baselineSource = dirtyBaselineValuesRef.current ?? (prev as Record<string, unknown>)
|
|
2357
|
+
const currentValues = valuesRef.current
|
|
2358
|
+
if (!Object.is(currentValues[id], nextValue)) {
|
|
2359
|
+
pendingDirtyBaselineCommitRef.current = null
|
|
2360
|
+
const baselineSource = dirtyBaselineValuesRef.current ?? (currentValues as Record<string, unknown>)
|
|
2327
2361
|
updateEditedFieldMarker(id, nextValue, baselineSource)
|
|
2328
|
-
nextData = { ...prev, [id]: nextValue } as CrudFormValues<TValues>
|
|
2329
|
-
valuesRef.current = nextData
|
|
2330
2362
|
if (!(embedded && !trackDirtyWhenEmbedded)) {
|
|
2331
|
-
|
|
2332
|
-
dirtyBaselineSnapshotRef.current = baseline
|
|
2363
|
+
dirtyBaselineSnapshotRef.current ??= createDirtySnapshot(currentValues as Record<string, unknown>)
|
|
2333
2364
|
dirtyBaselineValuesRef.current = baselineSource
|
|
2334
|
-
nextDirty = createDirtySnapshot(nextData as Record<string, unknown>) !== baseline
|
|
2335
2365
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2366
|
+
if (extendedInjectionEventsEnabled) {
|
|
2367
|
+
const fieldRevision = (fieldChangeRevisionByFieldRef.current.get(id) ?? 0) + 1
|
|
2368
|
+
fieldChangeRevisionByFieldRef.current.set(id, fieldRevision)
|
|
2369
|
+
nextFieldChangeEventIdRef.current += 1
|
|
2370
|
+
pendingFieldChangeEventsRef.current.push({
|
|
2371
|
+
eventId: nextFieldChangeEventIdRef.current,
|
|
2372
|
+
fieldId: id,
|
|
2373
|
+
fieldValue: nextValue,
|
|
2374
|
+
fieldRevision,
|
|
2375
|
+
fieldRevisionsAtEnqueue: new Map(fieldChangeRevisionByFieldRef.current),
|
|
2376
|
+
formGeneration: fieldChangeFormGenerationRef.current,
|
|
2377
|
+
})
|
|
2378
|
+
setFieldChangeDispatchVersion((version) => version + 1)
|
|
2379
|
+
}
|
|
2380
|
+
setValues((prev) => Object.is(prev[id], nextValue)
|
|
2381
|
+
? prev
|
|
2382
|
+
: ({ ...prev, [id]: nextValue } as CrudFormValues<TValues>))
|
|
2341
2383
|
}
|
|
2342
2384
|
const clearedMessages: string[] = []
|
|
2343
2385
|
setErrors((prev) => {
|
|
@@ -2373,19 +2415,50 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2373
2415
|
return prev
|
|
2374
2416
|
})
|
|
2375
2417
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2418
|
+
}, [embedded, extendedInjectionEventsEnabled, t, trackDirtyWhenEmbedded, translateValidationMessage, updateEditedFieldMarker])
|
|
2419
|
+
|
|
2420
|
+
React.useEffect(() => {
|
|
2421
|
+
if (!extendedInjectionEventsEnabled) {
|
|
2422
|
+
pendingFieldChangeEventsRef.current = []
|
|
2423
|
+
return
|
|
2424
|
+
}
|
|
2425
|
+
if (!fieldChangeDispatcherMountedRef.current || activeFieldChangeDispatchRef.current) return
|
|
2426
|
+
let pendingEvent = pendingFieldChangeEventsRef.current.shift()
|
|
2427
|
+
while (pendingEvent && pendingEvent.formGeneration !== fieldChangeFormGenerationRef.current) {
|
|
2428
|
+
pendingEvent = pendingFieldChangeEventsRef.current.shift()
|
|
2429
|
+
}
|
|
2430
|
+
if (!pendingEvent) return
|
|
2431
|
+
const dispatchToken = {
|
|
2432
|
+
eventId: pendingEvent.eventId,
|
|
2433
|
+
formGeneration: pendingEvent.formGeneration,
|
|
2434
|
+
}
|
|
2435
|
+
activeFieldChangeDispatchRef.current = dispatchToken
|
|
2436
|
+
void triggerInjectionEvent('onFieldChange', values as TValues, injectionContextRef.current, {
|
|
2437
|
+
fieldId: pendingEvent.fieldId,
|
|
2438
|
+
fieldValue: pendingEvent.fieldValue,
|
|
2380
2439
|
}).then((result) => {
|
|
2440
|
+
const isCurrentEvent =
|
|
2441
|
+
fieldChangeDispatcherMountedRef.current &&
|
|
2442
|
+
fieldChangeFormGenerationRef.current === pendingEvent.formGeneration &&
|
|
2443
|
+
fieldChangeRevisionByFieldRef.current.get(pendingEvent.fieldId) === pendingEvent.fieldRevision
|
|
2444
|
+
if (!isCurrentEvent) return
|
|
2381
2445
|
if (!result.ok) return
|
|
2382
2446
|
const change = result.fieldChange
|
|
2383
2447
|
if (!change) return
|
|
2384
|
-
const
|
|
2448
|
+
const candidateUpdates: Record<string, unknown> = { ...(change.sideEffects ?? {}) }
|
|
2385
2449
|
if (change.value !== undefined) {
|
|
2386
|
-
|
|
2450
|
+
candidateUpdates[pendingEvent.fieldId] = change.value
|
|
2451
|
+
}
|
|
2452
|
+
const updates: Record<string, unknown> = {}
|
|
2453
|
+
for (const [fieldId, value] of Object.entries(candidateUpdates)) {
|
|
2454
|
+
if (
|
|
2455
|
+
fieldChangeRevisionByFieldRef.current.get(fieldId) !==
|
|
2456
|
+
pendingEvent.fieldRevisionsAtEnqueue.get(fieldId)
|
|
2457
|
+
) continue
|
|
2458
|
+
updates[fieldId] = value
|
|
2387
2459
|
}
|
|
2388
2460
|
if (Object.keys(updates).length > 0) {
|
|
2461
|
+
pendingDirtyBaselineCommitRef.current = null
|
|
2389
2462
|
setValues((prev) => {
|
|
2390
2463
|
let changed = false
|
|
2391
2464
|
const next = { ...prev } as Record<string, unknown>
|
|
@@ -2402,8 +2475,22 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2402
2475
|
}
|
|
2403
2476
|
}).catch((err) => {
|
|
2404
2477
|
logger.error('Error in onFieldChange', { err })
|
|
2478
|
+
}).finally(() => {
|
|
2479
|
+
const activeDispatch = activeFieldChangeDispatchRef.current
|
|
2480
|
+
if (
|
|
2481
|
+
activeDispatch?.eventId === dispatchToken.eventId &&
|
|
2482
|
+
activeDispatch.formGeneration === dispatchToken.formGeneration
|
|
2483
|
+
) {
|
|
2484
|
+
activeFieldChangeDispatchRef.current = null
|
|
2485
|
+
}
|
|
2486
|
+
if (
|
|
2487
|
+
fieldChangeDispatcherMountedRef.current &&
|
|
2488
|
+
fieldChangeFormGenerationRef.current === dispatchToken.formGeneration
|
|
2489
|
+
) {
|
|
2490
|
+
setFieldChangeDispatchVersion((version) => version + 1)
|
|
2491
|
+
}
|
|
2405
2492
|
})
|
|
2406
|
-
}, [
|
|
2493
|
+
}, [extendedInjectionEventsEnabled, fieldChangeDispatchVersion, triggerInjectionEvent, values])
|
|
2407
2494
|
|
|
2408
2495
|
const onBlurRequest = React.useCallback((fieldId: string) => {
|
|
2409
2496
|
void validateFieldOnBlur(fieldId)
|
|
@@ -2446,68 +2533,83 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2446
2533
|
)
|
|
2447
2534
|
|
|
2448
2535
|
const appliedInitialValuesSnapshotRef = React.useRef<string | undefined>(undefined)
|
|
2536
|
+
const initialValuesTransformMountedRef = React.useRef(true)
|
|
2449
2537
|
const dirtyBaselineSnapshotRef = React.useRef<string | undefined>(undefined)
|
|
2450
2538
|
const dirtyBaselineValuesRef = React.useRef<Record<string, unknown> | undefined>(undefined)
|
|
2539
|
+
const pendingDirtyBaselineCommitRef = React.useRef<{
|
|
2540
|
+
guardSnapshot?: string
|
|
2541
|
+
valuesSnapshot: string
|
|
2542
|
+
values: Record<string, unknown>
|
|
2543
|
+
} | null>(null)
|
|
2451
2544
|
const userEditedFieldIdsRef = React.useRef<Set<string>>(new Set())
|
|
2452
2545
|
const everEditedFieldIdsRef = React.useRef<Set<string>>(new Set())
|
|
2546
|
+
React.useLayoutEffect(() => {
|
|
2547
|
+
initialValuesTransformMountedRef.current = true
|
|
2548
|
+
return () => {
|
|
2549
|
+
initialValuesTransformMountedRef.current = false
|
|
2550
|
+
}
|
|
2551
|
+
}, [])
|
|
2552
|
+
React.useLayoutEffect(() => {
|
|
2553
|
+
const pendingBaseline = pendingDirtyBaselineCommitRef.current
|
|
2554
|
+
if (!pendingBaseline) return
|
|
2555
|
+
pendingDirtyBaselineCommitRef.current = null
|
|
2556
|
+
if (
|
|
2557
|
+
pendingBaseline.guardSnapshot !== undefined &&
|
|
2558
|
+
appliedInitialValuesSnapshotRef.current !== pendingBaseline.guardSnapshot
|
|
2559
|
+
) return
|
|
2560
|
+
const committedValues = values as Record<string, unknown>
|
|
2561
|
+
if (createDirtySnapshot(committedValues) !== pendingBaseline.valuesSnapshot) return
|
|
2562
|
+
dirtyBaselineSnapshotRef.current = pendingBaseline.valuesSnapshot
|
|
2563
|
+
dirtyBaselineValuesRef.current = { ...pendingBaseline.values }
|
|
2564
|
+
}, [values])
|
|
2453
2565
|
React.useLayoutEffect(() => {
|
|
2454
2566
|
if (!initialValues) return
|
|
2455
2567
|
const snapshot = JSON.stringify({
|
|
2456
2568
|
initialValues,
|
|
2457
2569
|
injectedFieldIds: injectedFieldDefinitions.map((definition) => definition.id),
|
|
2570
|
+
injectionWidgetIds: injectionWidgets.map((widget) => `${widget.moduleId}:${widget.widgetId}:${widget.key}`),
|
|
2458
2571
|
customFieldMappings: cfDefinitions.map((definition) => definition.key),
|
|
2459
2572
|
dotPathBaseFieldIds: Array.from(dotPathBaseFieldIds),
|
|
2460
2573
|
})
|
|
2461
2574
|
if (appliedInitialValuesSnapshotRef.current === snapshot) return
|
|
2462
2575
|
appliedInitialValuesSnapshotRef.current = snapshot
|
|
2463
2576
|
const initialRecord = initialValues as Record<string, unknown>
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
if (editedFieldIds.has(key)) continue
|
|
2483
|
-
mergedRecord[key] = value
|
|
2484
|
-
}
|
|
2485
|
-
for (const definition of injectedFieldDefinitions) {
|
|
2486
|
-
if (merged[definition.id] !== undefined) continue
|
|
2487
|
-
const extracted = readByDotPath(initialRecord, definition.id)
|
|
2488
|
-
if (extracted !== undefined) {
|
|
2489
|
-
;(merged as Record<string, unknown>)[definition.id] = extracted
|
|
2490
|
-
}
|
|
2577
|
+
const currentValues = valuesRef.current
|
|
2578
|
+
const priorBaseline = dirtyBaselineSnapshotRef.current
|
|
2579
|
+
const editedFieldIds = userEditedFieldIdsRef.current
|
|
2580
|
+
const hadUnsavedEdits =
|
|
2581
|
+
editedFieldIds.size > 0 ||
|
|
2582
|
+
priorBaseline !== undefined &&
|
|
2583
|
+
createDirtySnapshot(currentValues as Record<string, unknown>) !== priorBaseline
|
|
2584
|
+
const mergedValues = { ...currentValues } as CrudFormValues<TValues>
|
|
2585
|
+
const mergedRecord = mergedValues as Record<string, unknown>
|
|
2586
|
+
for (const [key, value] of Object.entries(initialValues as Record<string, unknown>)) {
|
|
2587
|
+
if (editedFieldIds.has(key)) continue
|
|
2588
|
+
mergedRecord[key] = value
|
|
2589
|
+
}
|
|
2590
|
+
for (const definition of injectedFieldDefinitions) {
|
|
2591
|
+
if (mergedValues[definition.id] !== undefined) continue
|
|
2592
|
+
const extracted = readByDotPath(initialRecord, definition.id)
|
|
2593
|
+
if (extracted !== undefined) {
|
|
2594
|
+
mergedRecord[definition.id] = extracted
|
|
2491
2595
|
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2596
|
+
}
|
|
2597
|
+
for (const definition of cfDefinitions) {
|
|
2598
|
+
const targetId = customEntity ? definition.key : `cf_${definition.key}`
|
|
2599
|
+
if (!targetId || mergedValues[targetId] !== undefined) continue
|
|
2600
|
+
const extracted = readInitialCustomFieldValue(initialRecord, definition.key)
|
|
2601
|
+
if (extracted !== undefined) {
|
|
2602
|
+
mergedRecord[targetId] = extracted
|
|
2499
2603
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2604
|
+
}
|
|
2605
|
+
for (const fieldId of dotPathBaseFieldIds) {
|
|
2606
|
+
if (mergedValues[fieldId] !== undefined) continue
|
|
2607
|
+
const extracted = readByDotPath(initialRecord, fieldId)
|
|
2608
|
+
if (extracted !== undefined) {
|
|
2609
|
+
mergedRecord[fieldId] = extracted
|
|
2506
2610
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
})
|
|
2510
|
-
if (mergedValues && !hadUnsavedEdits) {
|
|
2611
|
+
}
|
|
2612
|
+
if (!hadUnsavedEdits) {
|
|
2511
2613
|
// Do not absorb an in-progress edit into the pristine baseline. This effect
|
|
2512
2614
|
// re-runs whenever the snapshot changes — which includes custom-field
|
|
2513
2615
|
// definitions and injected fields loading ASYNCHRONOUSLY after mount. If the
|
|
@@ -2518,11 +2620,17 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2518
2620
|
// only when there are no pending edits; while dirty, keep the load-time
|
|
2519
2621
|
// baseline so the edit stays dirty until save/discard. Root cause of the flaky
|
|
2520
2622
|
// optimistic-lock stale-edit conflicts (#2055 / TC-LOCK-OSS-015 / TC-LOCK-OSS-029).
|
|
2521
|
-
|
|
2522
|
-
|
|
2623
|
+
pendingDirtyBaselineCommitRef.current = {
|
|
2624
|
+
guardSnapshot: snapshot,
|
|
2625
|
+
valuesSnapshot: createDirtySnapshot(mergedValues as Record<string, unknown>),
|
|
2626
|
+
values: { ...(mergedValues as Record<string, unknown>) },
|
|
2627
|
+
}
|
|
2628
|
+
} else {
|
|
2629
|
+
pendingDirtyBaselineCommitRef.current = null
|
|
2523
2630
|
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2631
|
+
setValues(mergedValues)
|
|
2632
|
+
if (!extendedInjectionEventsEnabled || hadUnsavedEdits) return
|
|
2633
|
+
const transformSourceSnapshot = createDirtySnapshot(mergedValues as Record<string, unknown>)
|
|
2526
2634
|
const run = async () => {
|
|
2527
2635
|
try {
|
|
2528
2636
|
const result = await triggerInjectionEvent(
|
|
@@ -2531,21 +2639,27 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2531
2639
|
injectionContextRef.current,
|
|
2532
2640
|
)
|
|
2533
2641
|
const transformed = result.data
|
|
2534
|
-
if (
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2642
|
+
if (
|
|
2643
|
+
!initialValuesTransformMountedRef.current ||
|
|
2644
|
+
appliedInitialValuesSnapshotRef.current !== snapshot ||
|
|
2645
|
+
!transformed
|
|
2646
|
+
) return
|
|
2647
|
+
if (createDirtySnapshot(valuesRef.current as Record<string, unknown>) !== transformSourceSnapshot) return
|
|
2648
|
+
const transformedValues = transformed as CrudFormValues<TValues>
|
|
2649
|
+
pendingDirtyBaselineCommitRef.current = {
|
|
2650
|
+
guardSnapshot: snapshot,
|
|
2651
|
+
valuesSnapshot: createDirtySnapshot(transformedValues as Record<string, unknown>),
|
|
2652
|
+
values: { ...(transformedValues as Record<string, unknown>) },
|
|
2653
|
+
}
|
|
2654
|
+
setValues((current) => {
|
|
2655
|
+
if (createDirtySnapshot(current as Record<string, unknown>) !== transformSourceSnapshot) return current
|
|
2656
|
+
return transformedValues
|
|
2657
|
+
})
|
|
2541
2658
|
} catch (err) {
|
|
2542
2659
|
logger.error('Error in transformDisplayData', { err })
|
|
2543
2660
|
}
|
|
2544
2661
|
}
|
|
2545
2662
|
void run()
|
|
2546
|
-
return () => {
|
|
2547
|
-
cancelled = true
|
|
2548
|
-
}
|
|
2549
2663
|
}, [
|
|
2550
2664
|
cfDefinitions,
|
|
2551
2665
|
customEntity,
|
|
@@ -2553,6 +2667,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2553
2667
|
extendedInjectionEventsEnabled,
|
|
2554
2668
|
initialValues,
|
|
2555
2669
|
injectedFieldDefinitions,
|
|
2670
|
+
injectionWidgets,
|
|
2556
2671
|
triggerInjectionEvent,
|
|
2557
2672
|
])
|
|
2558
2673
|
|
|
@@ -2589,26 +2704,24 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
2589
2704
|
|
|
2590
2705
|
if (Object.keys(defaults).length === 0) return
|
|
2591
2706
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
if (!applied) return prev
|
|
2603
|
-
mergedValues = merged
|
|
2604
|
-
return mergedValues
|
|
2605
|
-
})
|
|
2707
|
+
const currentValues = valuesRef.current
|
|
2708
|
+
const mergedValues = { ...currentValues } as CrudFormValues<TValues>
|
|
2709
|
+
let applied = false
|
|
2710
|
+
for (const [fieldId, defaultVal] of Object.entries(defaults)) {
|
|
2711
|
+
// Skip if a value already exists (from initialValues or user input)
|
|
2712
|
+
if (mergedValues[fieldId] !== undefined) continue
|
|
2713
|
+
;(mergedValues as Record<string, unknown>)[fieldId] = defaultVal
|
|
2714
|
+
applied = true
|
|
2715
|
+
}
|
|
2716
|
+
if (!applied) return
|
|
2606
2717
|
|
|
2607
|
-
// Update the dirty baseline
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2718
|
+
// Update the dirty baseline only after the defaults commit so an interrupted
|
|
2719
|
+
// render cannot make the navigation guard observe values the user never saw.
|
|
2720
|
+
pendingDirtyBaselineCommitRef.current = {
|
|
2721
|
+
valuesSnapshot: createDirtySnapshot(mergedValues as Record<string, unknown>),
|
|
2722
|
+
values: { ...(mergedValues as Record<string, unknown>) },
|
|
2611
2723
|
}
|
|
2724
|
+
setValues(mergedValues)
|
|
2612
2725
|
}, [isLoading, initialValuesHasId, cfDefinitions, customEntity])
|
|
2613
2726
|
|
|
2614
2727
|
const markFormAsClean = React.useCallback((snapshotSource?: Record<string, unknown>) => {
|