@open-mercato/enterprise 0.10.1-develop.7628.1.db711688d7 → 0.10.1-develop.7635.1.03de276351

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.
@@ -1,2 +1,2 @@
1
- [build:enterprise] found 696 entry points
1
+ [build:enterprise] found 697 entry points
2
2
  [build:enterprise] built successfully
@@ -366,6 +366,11 @@ function RecordLockingWidget({
366
366
  const [isPrimaryInstance, setIsPrimaryInstance] = React.useState(true);
367
367
  const releasePayloadRef = React.useRef(null);
368
368
  const keepMineRetryVersionRef = React.useRef(0);
369
+ const hasAnotherOwnerForForm = React.useCallback(() => {
370
+ if (!ownerKey) return false;
371
+ const owner = getRecordLockOwnerMap().get(ownerKey);
372
+ return Boolean(owner && owner.instanceId !== instanceId && owner.formId === formId);
373
+ }, [formId, instanceId, ownerKey]);
369
374
  React.useEffect(() => {
370
375
  if (!ownerKey) {
371
376
  setIsPrimaryInstance(true);
@@ -383,17 +388,18 @@ function RecordLockingWidget({
383
388
  const claimOwnership = () => {
384
389
  const current = owners.get(ownerKey);
385
390
  if (!current) {
386
- owners.set(ownerKey, { instanceId, priority: ownerPriority });
391
+ owners.set(ownerKey, { instanceId, priority: ownerPriority, formId });
387
392
  setIsPrimaryInstance(true);
388
393
  notifyOwnersChanged();
389
394
  return;
390
395
  }
391
396
  if (current.instanceId === instanceId) {
397
+ owners.set(ownerKey, { instanceId, priority: ownerPriority, formId });
392
398
  setIsPrimaryInstance(true);
393
399
  return;
394
400
  }
395
401
  if (ownerPriority > current.priority) {
396
- owners.set(ownerKey, { instanceId, priority: ownerPriority });
402
+ owners.set(ownerKey, { instanceId, priority: ownerPriority, formId });
397
403
  setIsPrimaryInstance(true);
398
404
  notifyOwnersChanged();
399
405
  return;
@@ -415,9 +421,10 @@ function RecordLockingWidget({
415
421
  notifyOwnersChanged();
416
422
  }
417
423
  };
418
- }, [instanceId, ownerKey, ownerPriority]);
424
+ }, [formId, instanceId, ownerKey, ownerPriority]);
419
425
  React.useEffect(() => {
420
426
  if (isPrimaryInstance) return;
427
+ if (hasAnotherOwnerForForm()) return;
421
428
  const current = getRecordLockFormState(formId);
422
429
  if (!current?.lock?.token || !current.resourceKind || !current.resourceId) {
423
430
  clearRecordLockFormState(formId);
@@ -432,7 +439,7 @@ function RecordLockingWidget({
432
439
  logger.warn("Failed to release lock while demoting owner", { err: error });
433
440
  });
434
441
  clearRecordLockFormState(formId);
435
- }, [formId, isPrimaryInstance]);
442
+ }, [formId, hasAnotherOwnerForForm, isPrimaryInstance]);
436
443
  React.useEffect(() => {
437
444
  setMounted(true);
438
445
  }, []);
@@ -470,13 +477,16 @@ function RecordLockingWidget({
470
477
  if (!resourceKind || !resourceId) return;
471
478
  let active = true;
472
479
  const acquire = async () => {
480
+ const requestedState = getRecordLockFormState(formId);
473
481
  const call = await apiCall("/api/record_locks/acquire", {
474
482
  method: "POST",
475
483
  headers: { "content-type": "application/json" },
476
484
  body: JSON.stringify({ resourceKind, resourceId })
477
485
  });
478
486
  const payload = call.result ?? {};
479
- if (!active) return;
487
+ const currentState = getRecordLockFormState(formId);
488
+ if (!active || !currentState || currentState.recordDeleted === true || currentState.resourceKind !== resourceKind || currentState.resourceId !== resourceId || (currentState.lock?.token ?? null) !== (requestedState?.lock?.token ?? null)) return;
489
+ const latestActionLogId = currentState.latestActionLogId !== void 0 ? currentState.latestActionLogId : payload.latestActionLogId ?? null;
480
490
  if (!call.ok) {
481
491
  const defaultMessage = call.status === 403 ? t("api.errors.forbidden", "Forbidden") : t("record_locks.errors.acquire_failed", "Failed to load record lock status.");
482
492
  const message = typeof payload.error === "string" && payload.error.trim().length ? payload.error : defaultMessage;
@@ -489,7 +499,7 @@ function RecordLockingWidget({
489
499
  lock: payload.lock ?? null,
490
500
  currentUserId: payload.currentUserId ?? null,
491
501
  heartbeatSeconds: payload.heartbeatSeconds ?? 15,
492
- latestActionLogId: payload.latestActionLogId ?? null,
502
+ latestActionLogId,
493
503
  allowForceUnlock: payload.allowForceUnlock ?? false
494
504
  });
495
505
  return;
@@ -502,7 +512,7 @@ function RecordLockingWidget({
502
512
  lock: payload.lock ?? null,
503
513
  currentUserId: payload.currentUserId ?? null,
504
514
  heartbeatSeconds: payload.heartbeatSeconds ?? 15,
505
- latestActionLogId: payload.latestActionLogId ?? null,
515
+ latestActionLogId,
506
516
  allowForceUnlock: payload.allowForceUnlock ?? false
507
517
  });
508
518
  };
@@ -594,7 +604,11 @@ function RecordLockingWidget({
594
604
  const hasUnresolvedConflict = Boolean(state?.conflict) && !(state?.pendingResolutionArmed === true && typeof state?.pendingResolution === "string" && state.pendingResolution !== "normal");
595
605
  if (hasUnresolvedConflict) return;
596
606
  if (!state?.resourceKind || !state?.resourceId) return;
607
+ if (state.recordDeleted === true) return;
608
+ let active = true;
597
609
  const refreshPresence = async () => {
610
+ const requestedState = getRecordLockFormState(formId);
611
+ if (!active || !requestedState || requestedState.recordDeleted === true || requestedState.resourceKind !== state.resourceKind || requestedState.resourceId !== state.resourceId) return;
598
612
  const call = await apiCall("/api/record_locks/acquire", {
599
613
  method: "POST",
600
614
  headers: { "content-type": "application/json" },
@@ -604,25 +618,22 @@ function RecordLockingWidget({
604
618
  })
605
619
  });
606
620
  const payload = call.result ?? {};
621
+ const currentState = getRecordLockFormState(formId);
622
+ if (!active || !currentState || currentState.recordDeleted === true || currentState.resourceKind !== requestedState.resourceKind || currentState.resourceId !== requestedState.resourceId || (currentState.lock?.token ?? null) !== (requestedState.lock?.token ?? null)) return;
623
+ const nextLatestActionLogId = currentState.latestActionLogId !== void 0 ? currentState.latestActionLogId : payload.latestActionLogId ?? null;
607
624
  if (!call.ok) {
608
- const currentState2 = getRecordLockFormState(formId);
609
625
  setRecordLockFormState(formId, {
610
626
  resourceKind: state.resourceKind,
611
627
  resourceId: state.resourceId,
612
628
  acquired: false,
613
629
  lock: payload.lock ?? null,
614
- currentUserId: payload.currentUserId ?? currentState2?.currentUserId ?? null,
615
- heartbeatSeconds: payload.heartbeatSeconds ?? currentState2?.heartbeatSeconds ?? 15,
616
- latestActionLogId: payload.latestActionLogId ?? currentState2?.latestActionLogId ?? null,
630
+ currentUserId: payload.currentUserId ?? currentState?.currentUserId ?? null,
631
+ heartbeatSeconds: payload.heartbeatSeconds ?? currentState?.heartbeatSeconds ?? 15,
632
+ latestActionLogId: nextLatestActionLogId,
617
633
  allowForceUnlock: payload.allowForceUnlock ?? false
618
634
  });
619
635
  return;
620
636
  }
621
- const currentState = getRecordLockFormState(formId);
622
- const previousToken = currentState?.lock?.token ?? null;
623
- const nextToken = payload.lock?.token ?? null;
624
- const isSameSession = Boolean(previousToken && nextToken && previousToken === nextToken);
625
- const nextLatestActionLogId = isSameSession ? currentState?.latestActionLogId ?? null : payload.latestActionLogId ?? currentState?.latestActionLogId ?? null;
626
637
  setRecordLockFormState(formId, {
627
638
  resourceKind: state.resourceKind,
628
639
  resourceId: state.resourceId,
@@ -674,6 +685,7 @@ function RecordLockingWidget({
674
685
  }, 3e4);
675
686
  void refreshPresence();
676
687
  return () => {
688
+ active = false;
677
689
  window.removeEventListener(RECORD_LOCKS_INCOMING_CHANGES_EVENT, onIncomingChanges);
678
690
  window.removeEventListener(RECORD_LOCKS_FORCE_RELEASED_EVENT, onForceReleased);
679
691
  window.removeEventListener("om:event", onBridgeReconnected);
@@ -687,6 +699,7 @@ function RecordLockingWidget({
687
699
  state?.conflict,
688
700
  state?.pendingResolution,
689
701
  state?.pendingResolutionArmed,
702
+ state?.recordDeleted,
690
703
  state?.resourceId,
691
704
  state?.resourceKind
692
705
  ]);
@@ -734,18 +747,20 @@ function RecordLockingWidget({
734
747
  };
735
748
  }, [isPrimaryInstance]);
736
749
  React.useEffect(() => {
737
- if (!isPrimaryInstance) return;
738
750
  return () => {
739
- const payload = releasePayloadRef.current;
740
- if (payload) {
751
+ if (hasAnotherOwnerForForm()) return;
752
+ const current = getRecordLockFormState(formId);
753
+ if (current?.lock?.token && current.resourceKind && current.resourceId) {
741
754
  void releaseLock({
742
- ...payload,
755
+ resourceKind: current.resourceKind,
756
+ resourceId: current.resourceId,
757
+ token: current.lock.token,
743
758
  reason: "unmount"
744
759
  });
745
760
  }
746
761
  clearRecordLockFormState(formId);
747
762
  };
748
- }, [formId, isPrimaryInstance]);
763
+ }, [formId, hasAnotherOwnerForForm]);
749
764
  React.useEffect(() => {
750
765
  if (!isPrimaryInstance) return;
751
766
  const onCrudSaveError = (event) => {
@@ -758,7 +773,7 @@ function RecordLockingWidget({
758
773
  pendingResolutionArmed: false
759
774
  };
760
775
  if (payload.lock !== void 0) {
761
- nextPatch.lock = payload.lock;
776
+ nextPatch.lock = resolveLockAfterValidate(getRecordLockFormState(formId)?.lock, payload.lock);
762
777
  }
763
778
  if (payload.latestActionLogId !== void 0) {
764
779
  nextPatch.latestActionLogId = payload.latestActionLogId;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/record_locks/widgets/injection/record-locking/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { BACKEND_MUTATION_ERROR_EVENT } from '@open-mercato/ui/backend/injection/mutationEvents'\nimport { registerRecordLockConflictHandler } from '@open-mercato/ui/backend/conflicts'\nimport { useSearchParams } from 'next/navigation'\nimport { Mail } from 'lucide-react'\nimport {\n RECORD_LOCKS_FORCE_RELEASED_EVENT,\n RECORD_LOCKS_INCOMING_CHANGES_EVENT,\n RECORD_LOCKS_LOCK_CONTENDED_EVENT,\n RECORD_LOCKS_RECORD_DELETED_EVENT,\n} from '@open-mercato/enterprise/modules/record_locks/notifications.handlers'\nimport {\n ChangedFieldsTable,\n type ChangeRow,\n} from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport {\n clearRecordLockFormState,\n getRecordLockFormState,\n setRecordLockFormState,\n subscribeRecordLockFormState,\n type RecordLockFormState,\n type RecordLockUiConflict,\n type RecordLockUiView,\n} from '@open-mercato/enterprise/modules/record_locks/lib/clientLockStore'\nimport { isUuid, resolveConflictId, runAcceptIncoming } from './conflictResolution'\nimport { isOptimisticLockFloorConflict } from '@open-mercato/enterprise/modules/record_locks/lib/optimisticLockFloor'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('record_locks').child({ component: 'record-locking-widget' })\n\ntype CrudInjectionContext = {\n formId?: string\n entityId?: string\n resourceKind?: string\n resourceId?: string\n recordId?: string\n path?: string\n query?: string\n kind?: string\n personId?: string\n companyId?: string\n dealId?: string\n retryLastMutation?: () => Promise<boolean | void> | boolean | void\n}\n\ntype RecordLockWidgetOwner = {\n instanceId: string\n priority: number\n}\n\nconst GLOBAL_RECORD_LOCK_OWNERS_KEY = '__openMercatoRecordLockWidgetOwners__'\n\nfunction getRecordLockOwnerMap(): Map<string, RecordLockWidgetOwner> {\n const store = globalThis as Record<string, unknown>\n const existing = store[GLOBAL_RECORD_LOCK_OWNERS_KEY]\n if (existing instanceof Map) return existing as Map<string, RecordLockWidgetOwner>\n const next = new Map<string, RecordLockWidgetOwner>()\n store[GLOBAL_RECORD_LOCK_OWNERS_KEY] = next\n return next\n}\n\ntype AcquireResponse = {\n ok?: boolean\n acquired?: boolean\n allowForceUnlock?: boolean\n heartbeatSeconds?: number\n latestActionLogId?: string | null\n lock?: RecordLockUiView | null\n currentUserId?: string\n error?: string\n code?: string\n}\n\ntype ValidateResponse = {\n ok: boolean\n status?: number\n code?: string\n latestActionLogId?: string | null\n lock?: RecordLockUiView | null\n conflict?: RecordLockUiConflict | null\n}\n\ntype CrudSaveErrorEventDetail = {\n contextId?: string\n formId?: string\n error?: unknown\n}\n\ntype RecordLockContendedEventDetail = {\n sourceEntityId?: string | null\n}\n\ntype RecordDeletedEventDetail = {\n resourceId?: string | null\n resourceKind?: string | null\n}\n\ntype RecordLockIncomingChangesEventDetail = {\n resourceId?: string | null\n resourceKind?: string | null\n}\n\ntype RecordLockForceReleasedEventDetail = {\n resourceId?: string | null\n resourceKind?: string | null\n}\n\nfunction isObjectRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object'\n}\n\nfunction readStringOrNull(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction extractErrorStatus(error: unknown): number | null {\n const queue: unknown[] = [error]\n const visited = new Set<unknown>()\n\n while (queue.length > 0) {\n const current = queue.shift()\n if (!current || visited.has(current)) continue\n visited.add(current)\n if (!isObjectRecord(current)) continue\n\n const status = current.status\n if (typeof status === 'number' && Number.isFinite(status)) return status\n if (typeof status === 'string') {\n const parsed = Number(status)\n if (Number.isFinite(parsed)) return parsed\n }\n\n const statusCode = current.statusCode\n if (typeof statusCode === 'number' && Number.isFinite(statusCode)) return statusCode\n if (typeof statusCode === 'string') {\n const parsed = Number(statusCode)\n if (Number.isFinite(parsed)) return parsed\n }\n\n const nested = ['body', 'response', 'data', 'details', 'error', 'cause']\n for (const key of nested) {\n const next = current[key]\n if (next && !visited.has(next)) queue.push(next)\n }\n }\n\n return null\n}\n\nfunction extractRecordLockConflictPayload(error: unknown): {\n conflict: RecordLockUiConflict\n lock?: RecordLockUiView | null\n latestActionLogId?: string | null\n} | null {\n const queue: unknown[] = [error]\n const visited = new Set<unknown>()\n\n while (queue.length > 0) {\n const current = queue.shift()\n if (!current || visited.has(current)) continue\n visited.add(current)\n if (!isObjectRecord(current)) continue\n\n const nested = ['body', 'response', 'data', 'details', 'error']\n for (const key of nested) {\n const next = current[key]\n if (next && !visited.has(next)) queue.push(next)\n }\n\n const code = typeof current.code === 'string' ? current.code : null\n const status = extractErrorStatus(current)\n const hasLockMarkers = (\n isObjectRecord(current.lock)\n || isObjectRecord(current.conflict)\n || typeof current.conflictId === 'string'\n || typeof current.resourceKind === 'string'\n || typeof current.resourceId === 'string'\n || Array.isArray(current.resolutionOptions)\n || typeof current.allowIncomingOverride === 'boolean'\n || typeof current.canOverrideIncoming === 'boolean'\n )\n const message = typeof current.message === 'string'\n ? current.message.toLowerCase()\n : typeof current.error === 'string'\n ? current.error.toLowerCase()\n : ''\n const looksLikeLockConflictMessage = (\n message.includes('record conflict')\n || message.includes('record_lock_conflict')\n || message.includes('conflict detected')\n )\n const isRecordLockConflict = (\n code === 'record_lock_conflict'\n || (status === 409 && (hasLockMarkers || looksLikeLockConflictMessage))\n )\n if (!isRecordLockConflict) continue\n if (!isObjectRecord(current.conflict)) {\n const lock = isObjectRecord(current.lock) ? (current.lock as RecordLockUiView) : undefined\n const fallbackConflictId = typeof current.conflictId === 'string' && isUuid(current.conflictId)\n ? current.conflictId\n : 'unresolved'\n const fallbackConflict: RecordLockUiConflict = {\n id: fallbackConflictId,\n resourceKind:\n (typeof current.resourceKind === 'string' && current.resourceKind.trim().length > 0\n ? current.resourceKind\n : lock?.resourceKind) ?? '',\n resourceId:\n (typeof current.resourceId === 'string' && current.resourceId.trim().length > 0\n ? current.resourceId\n : lock?.resourceId) ?? '',\n baseActionLogId:\n typeof current.baseActionLogId === 'string' || current.baseActionLogId === null\n ? current.baseActionLogId\n : lock?.baseActionLogId ?? null,\n incomingActionLogId:\n typeof current.incomingActionLogId === 'string' || current.incomingActionLogId === null\n ? current.incomingActionLogId\n : null,\n allowIncomingOverride: Boolean(current.allowIncomingOverride),\n canOverrideIncoming: Boolean(current.canOverrideIncoming),\n resolutionOptions: Array.isArray(current.resolutionOptions) && current.resolutionOptions.includes('accept_mine')\n ? ['accept_mine']\n : [],\n changes: [],\n }\n return {\n conflict: fallbackConflict,\n lock,\n latestActionLogId: typeof current.latestActionLogId === 'string' || current.latestActionLogId === null\n ? current.latestActionLogId\n : undefined,\n }\n }\n return {\n conflict: current.conflict as RecordLockUiConflict,\n lock: isObjectRecord(current.lock) ? (current.lock as RecordLockUiView) : undefined,\n latestActionLogId: typeof current.latestActionLogId === 'string' || current.latestActionLogId === null\n ? current.latestActionLogId\n : undefined,\n }\n }\n\n return null\n}\n\nfunction isRecordDeletedError(error: unknown): boolean {\n const queue: unknown[] = [error]\n const visited = new Set<unknown>()\n\n while (queue.length > 0) {\n const current = queue.shift()\n if (!current || visited.has(current)) continue\n visited.add(current)\n if (!isObjectRecord(current)) continue\n\n const status = extractErrorStatus(current)\n const code = typeof current.code === 'string' ? current.code.toLowerCase() : ''\n const message = typeof current.message === 'string'\n ? current.message.toLowerCase()\n : typeof current.error === 'string'\n ? current.error.toLowerCase()\n : ''\n\n const matchesCode = (\n code === 'record_not_found'\n || code === 'not_found'\n || code === 'record_deleted'\n )\n const matchesMessage = (\n message.includes('not found')\n || message.includes('was deleted')\n || message.includes('record deleted')\n )\n\n if (status === 404 || matchesCode || matchesMessage) {\n return true\n }\n\n const nested = ['body', 'response', 'data', 'details', 'error', 'cause']\n for (const key of nested) {\n const next = current[key]\n if (next && !visited.has(next)) queue.push(next)\n }\n }\n\n return false\n}\n\n/**\n * Action the record-lock widget takes when a CrudForm / guarded-mutation save\n * error event (`om:crud-save-error` / backend-mutation-error) fires. Extracted\n * as a pure function so the single-conflict-surface (S3) decision is unit-testable\n * without rendering the widget:\n *\n * - `ignore` \u2014 event is for a different form/record, there is no\n * actionable mounted record, OR the error is a plain OSS\n * `optimistic_lock_conflict` 409 already owned by the OSS\n * conflict bar via `surfaceRecordConflict` (opening the\n * dialog too would render TWO surfaces \u2014 #3504/#3505).\n * - `record-deleted` \u2014 the record was deleted by another user.\n * - `apply-conflict` \u2014 a genuine enterprise `record_lock_conflict` payload;\n * open the field-level merge dialog with it.\n * - `fallback-dialog` \u2014 an unrecognized 409 (neither record-lock nor OSS\n * optimistic-lock) for the mounted record; open the\n * degraded fallback dialog (legacy behavior).\n */\nexport type RecordLockSaveErrorDecision =\n | { action: 'ignore' }\n | { action: 'record-deleted' }\n | {\n action: 'apply-conflict'\n payload: {\n conflict: RecordLockUiConflict\n lock?: RecordLockUiView | null\n latestActionLogId?: string | null\n }\n }\n | { action: 'fallback-dialog' }\n\nexport function resolveRecordLockSaveErrorDecision(args: {\n error: unknown\n eventContextId: string | null | undefined\n formId: string\n currentState: { resourceKind?: string | null; resourceId?: string | null } | null | undefined\n}): RecordLockSaveErrorDecision {\n const { error, eventContextId, formId, currentState } = args\n const payload = extractRecordLockConflictPayload(error)\n const eventTargetsCurrentForm = !eventContextId || eventContextId === formId\n if (!eventTargetsCurrentForm) {\n if (!payload || !currentState?.resourceKind || !currentState?.resourceId) return { action: 'ignore' }\n const payloadResourceKind = payload.conflict.resourceKind?.trim() ?? ''\n const payloadResourceId = payload.conflict.resourceId?.trim() ?? ''\n if (!payloadResourceKind || !payloadResourceId) return { action: 'ignore' }\n if (payloadResourceKind !== currentState.resourceKind || payloadResourceId !== currentState.resourceId) {\n return { action: 'ignore' }\n }\n }\n if (!payload) {\n if (!currentState?.resourceKind || !currentState?.resourceId) return { action: 'ignore' }\n if (isRecordDeletedError(error)) return { action: 'record-deleted' }\n // A plain OSS `optimistic_lock_conflict` 409 (the shape `makeCrudRoute` /\n // `CrudForm` returns on a stale write) is already surfaced by the OSS conflict\n // bar via `surfaceRecordConflict`. Defer to it instead of opening the degraded\n // fallback merge dialog \u2014 otherwise both surfaces render at once (#3504) and the\n // dialog has no field diff and a no-op \"Accept incoming\" (#3505). The rich merge\n // dialog stays reserved for genuine `record_lock_conflict` payloads (handled\n // above) whose conflict carries field changes + resolution options. Delegates to\n // the shared `optimisticLockFloor` detector so this arbitration stays identical to\n // the conflict bar's ownership decision (single source of truth).\n if (isOptimisticLockFloorConflict(error)) return { action: 'ignore' }\n if (extractErrorStatus(error) === 409) return { action: 'fallback-dialog' }\n return { action: 'ignore' }\n }\n return { action: 'apply-conflict', payload }\n}\n\n/**\n * Merge the lock returned by `/api/record_locks/validate` into the widget's\n * current lock state. `/validate` intentionally strips the token from the\n * caller's OWN lock (`toLockView(..., includeToken=false)`), so a naive\n * `payloadLock ?? currentLock` overwrites a good token with `null`, flips\n * `mine` false, and flashes a spurious \"locked by another user\" banner for the\n * ~1s round-trip of every save (#5289). Preserve the current token when the\n * response describes the same *active* lock id; a genuinely different (foreign)\n * lock, or a same-id lock the server no longer reports as active, keeps its null\n * token so real contention banners and re-acquisition still work.\n */\nexport function resolveLockAfterValidate(\n currentLock: RecordLockUiView | null | undefined,\n payloadLock: RecordLockUiView | null | undefined,\n): RecordLockUiView | null {\n if (!payloadLock) return currentLock ?? null\n if (\n payloadLock.token == null\n && payloadLock.status === 'active'\n && currentLock\n && currentLock.id === payloadLock.id\n ) {\n return { ...payloadLock, token: currentLock.token }\n }\n return payloadLock\n}\n\nfunction clearIncomingChangesQueryFlag() {\n if (typeof window === 'undefined') return\n try {\n const url = new URL(window.location.href)\n if (url.searchParams.get('showIncomingChanges') !== '1') return\n url.searchParams.delete('showIncomingChanges')\n const nextUrl = `${url.pathname}${url.search}${url.hash}`\n window.history.replaceState(window.history.state, '', nextUrl)\n } catch {\n // ignore URL parse failures\n }\n}\n\nfunction clearLockContentionQueryFlag() {\n if (typeof window === 'undefined') return\n try {\n const url = new URL(window.location.href)\n if (url.searchParams.get('showLockContention') !== '1') return\n url.searchParams.delete('showLockContention')\n const nextUrl = `${url.pathname}${url.search}${url.hash}`\n window.history.replaceState(window.history.state, '', nextUrl)\n } catch {\n // ignore URL parse failures\n }\n}\n\nfunction submitCrudForm(formId: string): boolean {\n if (typeof document === 'undefined') return false\n const form = document.getElementById(formId)\n if (!(form instanceof HTMLFormElement)) return false\n form.requestSubmit()\n return true\n}\n\nfunction resolveResourceKind(context: CrudInjectionContext): string | null {\n if (context.resourceKind && context.resourceKind.trim()) return context.resourceKind\n if (context.kind === 'order') return 'sales.order'\n if (context.kind === 'quote') return 'sales.quote'\n if (context.personId) return 'customers.person'\n if (context.companyId) return 'customers.company'\n if (context.dealId) return 'customers.deal'\n const entityId = context.entityId\n if (entityId && entityId.includes(':')) {\n const [moduleId, rawEntity] = entityId.split(':')\n const entity = rawEntity ?? ''\n const normalizedModuleId = moduleId.trim()\n const normalizedEntity = entity.trim()\n if (normalizedModuleId && normalizedEntity) {\n const singularModuleId = normalizedModuleId.endsWith('s')\n ? normalizedModuleId.slice(0, -1)\n : normalizedModuleId\n\n const stripPrefixes = [\n `${normalizedModuleId}_`,\n `${singularModuleId}_`,\n ]\n\n let finalEntity = normalizedEntity\n for (const prefix of stripPrefixes) {\n if (finalEntity.startsWith(prefix)) {\n finalEntity = finalEntity.slice(prefix.length)\n break\n }\n }\n\n if (finalEntity) return `${normalizedModuleId}.${finalEntity}`\n }\n }\n\n const path = context.path ?? ''\n if (path.startsWith('/backend/customers/people/')) return 'customers.person'\n if (path.startsWith('/backend/customers/companies/')) return 'customers.company'\n if (path.startsWith('/backend/customers/deals/')) return 'customers.deal'\n if (path.startsWith('/backend/sales/orders/')) return 'sales.order'\n if (path.startsWith('/backend/sales/quotes/')) return 'sales.quote'\n if (path.startsWith('/backend/sales/documents/')) {\n const query = context.query ?? ''\n const params = new URLSearchParams(query)\n const kind = params.get('kind')\n if (kind === 'order') return 'sales.order'\n if (kind === 'quote') return 'sales.quote'\n }\n\n return null\n}\n\nfunction resolveResourceId(context: CrudInjectionContext, data: unknown): string | null {\n if (context.resourceId && context.resourceId.trim()) return context.resourceId\n if (context.recordId && context.recordId.trim()) return context.recordId\n if (context.personId && context.personId.trim()) return context.personId\n if (context.companyId && context.companyId.trim()) return context.companyId\n if (context.dealId && context.dealId.trim()) return context.dealId\n if (data && typeof data === 'object' && 'id' in data) {\n const id = (data as { id?: unknown }).id\n if (typeof id === 'string' && id.trim()) return id\n }\n if (data && typeof data === 'object') {\n const nestedPersonId = (data as { person?: { id?: unknown } }).person?.id\n if (typeof nestedPersonId === 'string' && nestedPersonId.trim()) return nestedPersonId\n const nestedCompanyId = (data as { company?: { id?: unknown } }).company?.id\n if (typeof nestedCompanyId === 'string' && nestedCompanyId.trim()) return nestedCompanyId\n const nestedDealId = (data as { deal?: { id?: unknown } }).deal?.id\n if (typeof nestedDealId === 'string' && nestedDealId.trim()) return nestedDealId\n }\n const path = context.path ?? ''\n const parts = path.split('/').filter((part) => part.length > 0)\n const candidates = [\n ['backend', 'customers', 'people'],\n ['backend', 'customers', 'companies'],\n ['backend', 'customers', 'deals'],\n ['backend', 'sales', 'orders'],\n ['backend', 'sales', 'quotes'],\n ['backend', 'sales', 'documents'],\n ] as const\n for (const prefix of candidates) {\n const matchesPrefix = prefix.every((segment, index) => parts[index] === segment)\n if (!matchesPrefix || parts.length <= prefix.length) continue\n const rawId = parts[prefix.length] ?? ''\n if (!rawId) continue\n try {\n const decoded = decodeURIComponent(rawId).trim()\n if (decoded.length > 0) return decoded\n } catch {\n const trimmed = rawId.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n}\n\nfunction resolveFormId(\n context: CrudInjectionContext,\n resourceKind: string | null,\n resourceId: string | null,\n): string {\n if (context.formId && context.formId.trim().length > 0) return context.formId\n if (resourceKind && resourceId) return `record-lock:${resourceKind}:${resourceId}`\n if (context.path && context.path.trim().length > 0) {\n const query = context.query?.trim()\n return `record-lock:${context.path}${query ? `?${query}` : ''}`\n }\n return 'record-lock:global'\n}\n\nasync function releaseLock(state: {\n resourceKind: string\n resourceId: string\n token?: string | null\n reason?: 'saved' | 'cancelled' | 'unmount'\n}) {\n await apiCall('/api/record_locks/release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: state.token ?? undefined,\n reason: state.reason ?? 'cancelled',\n }),\n })\n}\n\nfunction releaseLockWithKeepalive(state: {\n resourceKind: string\n resourceId: string\n token?: string | null\n reason?: 'saved' | 'cancelled' | 'unmount'\n}) {\n const payload = JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: state.token ?? undefined,\n reason: state.reason ?? 'unmount',\n })\n\n try {\n if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {\n const blob = new Blob([payload], { type: 'application/json' })\n const sent = navigator.sendBeacon('/api/record_locks/release', blob)\n if (sent) return\n }\n } catch {\n // ignore and fallback to fetch\n }\n\n void fetch('/api/record_locks/release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: payload,\n keepalive: true,\n credentials: 'include',\n }).catch((error) => {\n logger.warn('Failed to release lock with keepalive fallback', { err: error })\n })\n}\n\nexport default function RecordLockingWidget({\n context,\n data,\n}: InjectionWidgetComponentProps<CrudInjectionContext, Record<string, unknown>>) {\n const t = useT()\n const searchParams = useSearchParams()\n const resourceKind = React.useMemo(() => resolveResourceKind(context), [context])\n const resourceId = React.useMemo(() => resolveResourceId(context, data), [context, data])\n const formId = React.useMemo(\n () => resolveFormId(context, resourceKind, resourceId),\n [context, resourceId, resourceKind],\n )\n const [, forceRender] = React.useReducer((value) => value + 1, 0)\n const state = getRecordLockFormState(formId)\n const [mounted, setMounted] = React.useState(false)\n const [showIncomingChangesRequested, setShowIncomingChangesRequested] = React.useState(false)\n const [showLockContentionBanner, setShowLockContentionBanner] = React.useState(false)\n const [isConflictDialogOpen, setIsConflictDialogOpen] = React.useState(false)\n const instanceId = React.useMemo(\n () =>\n `record-lock-widget:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`,\n [],\n )\n const ownerPriority = context.formId ? 2 : 1\n const ownerKey = resourceKind && resourceId ? `${resourceKind}:${resourceId}` : null\n const [isPrimaryInstance, setIsPrimaryInstance] = React.useState(true)\n const releasePayloadRef = React.useRef<{\n resourceKind: string\n resourceId: string\n token: string\n } | null>(null)\n const keepMineRetryVersionRef = React.useRef(0)\n\n React.useEffect(() => {\n if (!ownerKey) {\n setIsPrimaryInstance(true)\n return\n }\n\n const owners = getRecordLockOwnerMap()\n const notifyOwnersChanged = () => {\n if (typeof window === 'undefined') return\n window.dispatchEvent(\n new CustomEvent('om:record-lock-owner-changed', {\n detail: { ownerKey },\n }),\n )\n }\n\n const claimOwnership = () => {\n const current = owners.get(ownerKey)\n if (!current) {\n owners.set(ownerKey, { instanceId, priority: ownerPriority })\n setIsPrimaryInstance(true)\n notifyOwnersChanged()\n return\n }\n if (current.instanceId === instanceId) {\n setIsPrimaryInstance(true)\n return\n }\n if (ownerPriority > current.priority) {\n owners.set(ownerKey, { instanceId, priority: ownerPriority })\n setIsPrimaryInstance(true)\n notifyOwnersChanged()\n return\n }\n setIsPrimaryInstance(false)\n }\n\n claimOwnership()\n const onOwnersChanged = () => claimOwnership()\n if (typeof window !== 'undefined') {\n window.addEventListener('om:record-lock-owner-changed', onOwnersChanged)\n }\n\n return () => {\n if (typeof window !== 'undefined') {\n window.removeEventListener('om:record-lock-owner-changed', onOwnersChanged)\n }\n const current = owners.get(ownerKey)\n if (current?.instanceId === instanceId) {\n owners.delete(ownerKey)\n notifyOwnersChanged()\n }\n }\n }, [instanceId, ownerKey, ownerPriority])\n\n React.useEffect(() => {\n if (isPrimaryInstance) return\n const current = getRecordLockFormState(formId)\n if (!current?.lock?.token || !current.resourceKind || !current.resourceId) {\n clearRecordLockFormState(formId)\n return\n }\n void releaseLock({\n resourceKind: current.resourceKind,\n resourceId: current.resourceId,\n token: current.lock.token,\n reason: 'cancelled',\n }).catch((error) => {\n logger.warn('Failed to release lock while demoting owner', { err: error })\n })\n clearRecordLockFormState(formId)\n }, [formId, isPrimaryInstance])\n\n React.useEffect(() => {\n setMounted(true)\n }, [])\n\n React.useEffect(() => {\n const showIncomingChanges = searchParams?.get('showIncomingChanges') === '1'\n const showLockContention = searchParams?.get('showLockContention') === '1'\n\n if (showIncomingChanges) {\n setShowIncomingChangesRequested(true)\n clearIncomingChangesQueryFlag()\n }\n\n if (showLockContention) {\n setShowLockContentionBanner(true)\n clearLockContentionQueryFlag()\n }\n }, [searchParams])\n\n React.useEffect(() => subscribeRecordLockFormState(formId, () => forceRender()), [formId])\n\n React.useEffect(() => {\n if (!state?.conflict) {\n setIsConflictDialogOpen(false)\n return\n }\n setIsConflictDialogOpen(true)\n }, [\n state?.conflict?.id,\n state?.conflict?.incomingActionLogId,\n state?.conflict?.baseActionLogId,\n ])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!resourceKind || !resourceId) return\n setRecordLockFormState(formId, { formId, resourceKind, resourceId })\n }, [formId, isPrimaryInstance, resourceId, resourceKind])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!resourceKind || !resourceId) return\n let active = true\n const acquire = async () => {\n const call = await apiCall<AcquireResponse>('/api/record_locks/acquire', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ resourceKind, resourceId }),\n })\n const payload = call.result ?? {}\n if (!active) return\n if (!call.ok) {\n const defaultMessage = call.status === 403\n ? t('api.errors.forbidden', 'Forbidden')\n : t('record_locks.errors.acquire_failed', 'Failed to load record lock status.')\n const message = typeof payload.error === 'string' && payload.error.trim().length\n ? payload.error\n : defaultMessage\n flash(message, 'error')\n setRecordLockFormState(formId, {\n formId,\n resourceKind,\n resourceId,\n acquired: false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? 15,\n latestActionLogId: payload.latestActionLogId ?? null,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n })\n return\n }\n setRecordLockFormState(formId, {\n formId,\n resourceKind,\n resourceId,\n acquired: payload.acquired ?? false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? 15,\n latestActionLogId: payload.latestActionLogId ?? null,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n })\n }\n void acquire()\n return () => {\n active = false\n }\n }, [formId, isPrimaryInstance, resourceId, resourceKind])\n\n const mine = Boolean(state?.lock?.token)\n const participants = React.useMemo(() => {\n if (!state?.lock) return []\n const fromPayload = Array.isArray(state.lock.participants) ? state.lock.participants : []\n if (fromPayload.length) return fromPayload\n return [{\n userId: state.lock.lockedByUserId,\n lockedByName: state.lock.lockedByName,\n lockedByEmail: state.lock.lockedByEmail,\n lockedByIp: state.lock.lockedByIp,\n lockedAt: state.lock.lockedAt,\n lastHeartbeatAt: state.lock.lastHeartbeatAt,\n expiresAt: state.lock.expiresAt,\n }]\n }, [state?.lock])\n const activeParticipantCount = state?.lock?.activeParticipantCount ?? participants.length\n const otherParticipants = React.useMemo(() => {\n if (!state?.currentUserId) return participants\n return participants.filter((participant) => participant.userId !== state.currentUserId)\n }, [participants, state?.currentUserId])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!mine || !state?.lock?.id) return\n\n const onContention = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? ((event as CustomEvent<unknown>).detail as RecordLockContendedEventDetail)\n : null\n if (!detail) return\n if (detail.sourceEntityId !== state.lock?.id) return\n setShowLockContentionBanner(true)\n }\n\n window.addEventListener(RECORD_LOCKS_LOCK_CONTENDED_EVENT, onContention)\n\n return () => {\n window.removeEventListener(RECORD_LOCKS_LOCK_CONTENDED_EVENT, onContention)\n }\n }, [isPrimaryInstance, mine, state?.lock?.id])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!state?.resourceKind || !state?.resourceId) return\n if (state.recordDeleted === true) return\n const onRecordDeleted = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? ((event as CustomEvent<unknown>).detail as RecordDeletedEventDetail)\n : null\n if (!detail) return\n if (detail.resourceId !== state.resourceId) return\n const kind = readStringOrNull(detail.resourceKind)\n if (kind && kind !== state.resourceKind) return\n setIsConflictDialogOpen(true)\n setRecordLockFormState(formId, {\n recordDeleted: true,\n acquired: false,\n lock: null,\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n }\n\n window.addEventListener(RECORD_LOCKS_RECORD_DELETED_EVENT, onRecordDeleted)\n\n return () => {\n window.removeEventListener(RECORD_LOCKS_RECORD_DELETED_EVENT, onRecordDeleted)\n }\n }, [formId, isPrimaryInstance, state?.recordDeleted, state?.resourceId, state?.resourceKind])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!state?.lock?.token || !state.resourceKind || !state.resourceId) return\n const heartbeatSeconds = (\n typeof state.heartbeatSeconds === 'number'\n && Number.isFinite(state.heartbeatSeconds)\n && state.heartbeatSeconds > 0\n )\n ? state.heartbeatSeconds\n : 10\n const intervalMs = Math.max(1_000, Math.round(heartbeatSeconds * 1000))\n const interval = window.setInterval(() => {\n void apiCall('/api/record_locks/heartbeat', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: state.lock?.token,\n }),\n })\n }, intervalMs)\n return () => window.clearInterval(interval)\n }, [isPrimaryInstance, state?.heartbeatSeconds, state?.lock?.token, state?.resourceId, state?.resourceKind])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n const hasUnresolvedConflict = Boolean(state?.conflict)\n && !(\n state?.pendingResolutionArmed === true\n && typeof state?.pendingResolution === 'string'\n && state.pendingResolution !== 'normal'\n )\n if (hasUnresolvedConflict) return\n if (!state?.resourceKind || !state?.resourceId) return\n const refreshPresence = async () => {\n const call = await apiCall<AcquireResponse>('/api/record_locks/acquire', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n }),\n })\n const payload = call.result ?? {}\n if (!call.ok) {\n const currentState = getRecordLockFormState(formId)\n setRecordLockFormState(formId, {\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n acquired: false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? currentState?.currentUserId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? currentState?.heartbeatSeconds ?? 15,\n latestActionLogId: payload.latestActionLogId ?? currentState?.latestActionLogId ?? null,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n })\n return\n }\n const currentState = getRecordLockFormState(formId)\n const previousToken = currentState?.lock?.token ?? null\n const nextToken = payload.lock?.token ?? null\n const isSameSession = Boolean(previousToken && nextToken && previousToken === nextToken)\n const nextLatestActionLogId = isSameSession\n ? (currentState?.latestActionLogId ?? null)\n : (payload.latestActionLogId ?? currentState?.latestActionLogId ?? null)\n\n setRecordLockFormState(formId, {\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n acquired: payload.acquired ?? false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? 15,\n latestActionLogId: nextLatestActionLogId,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n })\n }\n const onIncomingChanges = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? ((event as CustomEvent<unknown>).detail as RecordLockIncomingChangesEventDetail)\n : null\n if (!detail) return\n if (detail.resourceId !== state.resourceId) return\n const kind = readStringOrNull(detail.resourceKind)\n if (kind && kind !== state.resourceKind) return\n setShowIncomingChangesRequested(true)\n void refreshPresence()\n }\n const onForceReleased = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? ((event as CustomEvent<unknown>).detail as RecordLockForceReleasedEventDetail)\n : null\n if (!detail) return\n if (detail.resourceId !== state.resourceId) return\n const kind = readStringOrNull(detail.resourceKind)\n if (kind && kind !== state.resourceKind) return\n void refreshPresence()\n }\n const onBridgeReconnected = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? (event as CustomEvent<Record<string, unknown>>).detail\n : null\n if (detail?.id !== 'om:bridge:reconnected') return\n void refreshPresence()\n }\n const onFocus = () => {\n void refreshPresence()\n }\n const onVisibilityChange = () => {\n if (!document.hidden) {\n void refreshPresence()\n }\n }\n\n window.addEventListener(RECORD_LOCKS_INCOMING_CHANGES_EVENT, onIncomingChanges)\n window.addEventListener(RECORD_LOCKS_FORCE_RELEASED_EVENT, onForceReleased)\n window.addEventListener('om:event', onBridgeReconnected)\n window.addEventListener('focus', onFocus)\n document.addEventListener('visibilitychange', onVisibilityChange)\n const heartbeatInterval = window.setInterval(() => {\n void refreshPresence()\n }, 30_000)\n void refreshPresence()\n\n return () => {\n window.removeEventListener(RECORD_LOCKS_INCOMING_CHANGES_EVENT, onIncomingChanges)\n window.removeEventListener(RECORD_LOCKS_FORCE_RELEASED_EVENT, onForceReleased)\n window.removeEventListener('om:event', onBridgeReconnected)\n window.removeEventListener('focus', onFocus)\n document.removeEventListener('visibilitychange', onVisibilityChange)\n window.clearInterval(heartbeatInterval)\n }\n }, [\n formId,\n isPrimaryInstance,\n state?.conflict,\n state?.pendingResolution,\n state?.pendingResolutionArmed,\n state?.resourceId,\n state?.resourceKind,\n ])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!showIncomingChangesRequested) return\n if (!state?.resourceKind || !state?.resourceId || !state?.lock) return\n let cancelled = false\n const openIncomingChangesDialog = async () => {\n clearIncomingChangesQueryFlag()\n setShowIncomingChangesRequested(false)\n await validateBeforeSave({}, context)\n if (cancelled) return\n }\n void openIncomingChangesDialog()\n return () => {\n cancelled = true\n }\n }, [context, isPrimaryInstance, showIncomingChangesRequested, state?.lock, state?.resourceId, state?.resourceKind, t])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (\n state?.resourceKind\n && state?.resourceId\n && typeof state.lock?.token === 'string'\n && state.lock.token.trim().length > 0\n ) {\n releasePayloadRef.current = {\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: state.lock.token,\n }\n return\n }\n releasePayloadRef.current = null\n }, [isPrimaryInstance, state?.lock?.token, state?.resourceId, state?.resourceKind])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n const onPageHide = () => {\n const payload = releasePayloadRef.current\n if (!payload) return\n releaseLockWithKeepalive({\n ...payload,\n reason: 'unmount',\n })\n }\n window.addEventListener('pagehide', onPageHide)\n return () => {\n window.removeEventListener('pagehide', onPageHide)\n }\n }, [isPrimaryInstance])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n return () => {\n const payload = releasePayloadRef.current\n if (payload) {\n void releaseLock({\n ...payload,\n reason: 'unmount',\n })\n }\n clearRecordLockFormState(formId)\n }\n }, [formId, isPrimaryInstance])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n const onCrudSaveError = (event: Event) => {\n const applyConflictPayload = (payload: {\n conflict: RecordLockUiConflict\n lock?: RecordLockUiView | null\n latestActionLogId?: string | null\n }) => {\n setIsConflictDialogOpen(true)\n const nextPatch: Partial<RecordLockFormState> = {\n conflict: payload.conflict,\n pendingConflictId: payload.conflict.id,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n }\n if (payload.lock !== undefined) {\n nextPatch.lock = payload.lock\n }\n if (payload.latestActionLogId !== undefined) {\n nextPatch.latestActionLogId = payload.latestActionLogId\n }\n setRecordLockFormState(formId, {\n ...nextPatch,\n })\n }\n\n const buildFallbackConflict = (currentState: RecordLockFormState) => {\n const preferredConflictId = isUuid(currentState.pendingConflictId)\n ? currentState.pendingConflictId\n : isUuid(currentState.conflict?.id)\n ? currentState.conflict.id\n : 'unresolved'\n return ({\n conflict: {\n id: preferredConflictId,\n resourceKind: currentState.resourceKind ?? '',\n resourceId: currentState.resourceId ?? '',\n baseActionLogId: currentState.latestActionLogId ?? null,\n incomingActionLogId: null,\n allowIncomingOverride: false,\n canOverrideIncoming: false,\n resolutionOptions: [],\n changes: [],\n } as RecordLockUiConflict,\n lock: currentState.lock ?? undefined,\n latestActionLogId: currentState.latestActionLogId ?? null,\n })\n }\n\n const detail = (event as CustomEvent<CrudSaveErrorEventDetail>).detail\n if (!detail) return\n const eventContextId = detail.contextId ?? detail.formId\n const currentState = getRecordLockFormState(formId)\n const decision = resolveRecordLockSaveErrorDecision({\n error: detail.error,\n eventContextId,\n formId,\n currentState,\n })\n if (decision.action === 'ignore') return\n if (decision.action === 'record-deleted') {\n setIsConflictDialogOpen(true)\n setRecordLockFormState(formId, {\n recordDeleted: true,\n acquired: false,\n lock: null,\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n return\n }\n if (decision.action === 'fallback-dialog') {\n if (currentState) applyConflictPayload(buildFallbackConflict(currentState))\n return\n }\n\n applyConflictPayload(decision.payload)\n }\n\n window.addEventListener(BACKEND_MUTATION_ERROR_EVENT, onCrudSaveError)\n window.addEventListener('om:crud-save-error', onCrudSaveError)\n return () => {\n window.removeEventListener(BACKEND_MUTATION_ERROR_EVENT, onCrudSaveError)\n window.removeEventListener('om:crud-save-error', onCrudSaveError)\n }\n }, [formId, isPrimaryInstance])\n\n // Single conflict surface (S3): own the surface for record_lock_conflict 409s\n // routed through `surfaceRecordConflict` (command/raw-write paths that don't\n // emit the crud-save-error event). Opening the dialog here is idempotent with\n // the listener above; returning true tells the core helper to suppress the OSS\n // bar (so we never render both). Decline (false) for a different record so the\n // bar still renders there \u2014 a conflict is never swallowed.\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!resourceKind || !resourceId) return\n const unregister = registerRecordLockConflictHandler((_conflict, error) => {\n const payload = extractRecordLockConflictPayload(error)\n if (!payload) return false\n const payloadKind = payload.conflict.resourceKind?.trim() ?? ''\n const payloadId = payload.conflict.resourceId?.trim() ?? ''\n if (payloadKind && payloadId && (payloadKind !== resourceKind || payloadId !== resourceId)) return false\n setIsConflictDialogOpen(true)\n setRecordLockFormState(formId, {\n conflict: payload.conflict,\n pendingConflictId: payload.conflict.id,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n return true\n })\n return unregister\n }, [formId, isPrimaryInstance, resourceKind, resourceId])\n\n const handleTakeOver = React.useCallback(async () => {\n keepMineRetryVersionRef.current += 1\n if (!state?.resourceKind || !state?.resourceId) return\n const call = await apiCall<AcquireResponse>('/api/record_locks/force-release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n }),\n })\n if (!call.ok) {\n flash(t('record_locks.errors.force_release_failed', 'Failed to take over editing.'), 'error')\n return\n }\n const acquire = await apiCall<AcquireResponse>('/api/record_locks/acquire', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ resourceKind: state.resourceKind, resourceId: state.resourceId }),\n })\n if (!acquire.ok) {\n flash(t('record_locks.errors.force_release_failed', 'Failed to take over editing.'), 'error')\n return\n }\n const payload = acquire.result ?? {}\n setRecordLockFormState(formId, {\n acquired: payload.acquired ?? false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? null,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n latestActionLogId: payload.latestActionLogId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? 15,\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n }, [formId, state?.resourceId, state?.resourceKind, t])\n\n const handleAcceptIncoming = React.useCallback(async () => {\n keepMineRetryVersionRef.current += 1\n await runAcceptIncoming({\n conflict: state?.conflict,\n resourceKind: state?.resourceKind,\n resourceId: state?.resourceId,\n revalidateConflictId: async () => {\n const validation = await validateBeforeSave({}, context)\n return resolveConflictId(validation.conflict)\n },\n releaseIncoming: async (conflictId) => {\n await apiCallOrThrow('/api/record_locks/release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state?.resourceKind,\n resourceId: state?.resourceId,\n token: state?.lock?.token ?? undefined,\n reason: 'conflict_resolved',\n conflictId,\n resolution: 'accept_incoming',\n }),\n })\n },\n clearConflictState: () => {\n setRecordLockFormState(formId, {\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n },\n reload: () => {\n window.location.reload()\n },\n })\n }, [context, formId, state?.conflict, state?.lock?.token, state?.resourceId, state?.resourceKind])\n\n const handleKeepMine = React.useCallback(() => {\n if (!state?.conflict) return\n const applyAcceptMine = async () => {\n const keepMineRetryVersion = keepMineRetryVersionRef.current + 1\n keepMineRetryVersionRef.current = keepMineRetryVersion\n let conflictId: string | null = isUuid(state.conflict?.id) ? state.conflict.id : null\n if (!conflictId) {\n const validation = await validateBeforeSave({}, context)\n conflictId = isUuid(validation.conflict?.id) ? validation.conflict.id : null\n }\n if (!conflictId) {\n flash(\n t(\n 'record_locks.conflict.refresh_required',\n 'Could not confirm conflict details. Save again to refresh conflict data.',\n ),\n 'error',\n )\n return\n }\n setRecordLockFormState(formId, {\n pendingResolution: 'accept_mine',\n pendingConflictId: conflictId,\n pendingResolutionArmed: true,\n })\n window.setTimeout(async () => {\n if (keepMineRetryVersionRef.current !== keepMineRetryVersion) return\n const currentState = getRecordLockFormState(formId)\n if (currentState?.pendingResolution !== 'accept_mine') return\n const submitted = submitCrudForm(formId)\n if (submitted) return\n const retried = await Promise.resolve(context.retryLastMutation?.()).catch(() => false)\n if (!retried) {\n flash(\n t(\n 'record_locks.conflict.retry_save_after_accept_mine',\n 'Click save again to apply your changes.',\n ),\n 'info',\n )\n }\n }, 0)\n }\n void applyAcceptMine()\n }, [context, context.retryLastMutation, formId, state?.conflict, t])\n\n const handleKeepEditing = React.useCallback(() => {\n keepMineRetryVersionRef.current += 1\n setIsConflictDialogOpen(false)\n }, [])\n\n const noneLabel = t('audit_logs.common.none')\n const conflictChangeRows = React.useMemo<ChangeRow[]>(\n () => (state?.conflict?.changes ?? []).map((change) => ({\n field: change.field,\n from: change.incomingValue,\n to: change.mineValue,\n })),\n [state?.conflict?.changes],\n )\n const canKeepMyChanges = Boolean(\n state?.conflict?.allowIncomingOverride\n && state?.conflict?.canOverrideIncoming === true\n && state?.conflict?.resolutionOptions?.includes('accept_mine'),\n )\n const isRecordDeleted = state?.recordDeleted === true\n const showOverrideBlockedNotice = Boolean(\n state?.conflict?.allowIncomingOverride\n && !state?.conflict?.canOverrideIncoming,\n )\n const conflictDialog = (\n <Dialog open={Boolean(state?.conflict || isRecordDeleted) && isConflictDialogOpen} onOpenChange={(open) => {\n if (open) {\n setIsConflictDialogOpen(true)\n return\n }\n if (isRecordDeleted) {\n setIsConflictDialogOpen(true)\n return\n }\n setIsConflictDialogOpen(false)\n }}>\n <DialogContent data-testid=\"record-lock-conflict-dialog\">\n <DialogHeader>\n <DialogTitle>\n {isRecordDeleted\n ? t('record_locks.conflict.record_deleted_title', 'Record was deleted')\n : t('record_locks.conflict.title', 'Conflict detected')}\n </DialogTitle>\n </DialogHeader>\n <div className=\"space-y-3 text-sm\">\n <p className=\"text-muted-foreground\">\n {isRecordDeleted\n ? t(\n 'record_locks.conflict.record_deleted_description',\n 'This record was deleted by another user while you were editing. Saving is blocked to avoid unexpected results.',\n )\n : t('record_locks.conflict.description', 'The record was changed by another user after you started editing.')}\n </p>\n {!isRecordDeleted ? (\n <ChangedFieldsTable\n changeRows={conflictChangeRows}\n noneLabel={noneLabel}\n t={t}\n beforeLabel={t('record_locks.conflict.incoming_label', 'Incoming')}\n afterLabel={t('record_locks.conflict.current_label', 'Current')}\n />\n ) : null}\n {(state?.conflict?.changes?.length ?? 0) === 0 ? (\n !isRecordDeleted ? (\n <Alert status=\"information\">\n <AlertDescription>\n {t(\n 'record_locks.conflict.no_field_details',\n 'Field-level conflict details are unavailable for this record. Choose a resolution to continue.'\n )}\n </AlertDescription>\n </Alert>\n ) : null\n ) : null}\n {showOverrideBlockedNotice ? (\n <Alert status=\"warning\">\n <AlertDescription>\n {t(\n 'record_locks.conflict.override_blocked_notice',\n 'You cannot keep your version because you do not have permission to override incoming changes.',\n )}\n </AlertDescription>\n </Alert>\n ) : null}\n <div className=\"-mx-6 -mb-6 mt-4 border-t bg-background/95 px-6 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80\">\n <div className=\"flex flex-wrap justify-end gap-2\">\n {isRecordDeleted ? null : (\n <>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n void handleAcceptIncoming()\n }}\n >\n {t('record_locks.conflict.accept_incoming', 'Accept incoming')}\n </Button>\n {canKeepMyChanges ? (\n <Button\n type=\"button\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n void handleKeepMine()\n }}\n >\n {t('record_locks.conflict.accept_mine', 'Keep my changes')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n handleKeepEditing()\n }}\n >\n {t('record_locks.conflict.keep_editing', 'Keep editing')}\n </Button>\n </>\n )}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n\n const participantEmails = React.useMemo(() => {\n return otherParticipants\n .map((participant) => participant.lockedByEmail?.trim() ?? '')\n .filter((email, index, all) => email.length > 0 && all.indexOf(email) === index)\n .slice(0, 4)\n }, [otherParticipants])\n\n React.useEffect(() => {\n if (!showLockContentionBanner) return\n if (!mine) return\n if (activeParticipantCount > 1) return\n setShowLockContentionBanner(false)\n }, [activeParticipantCount, mine, showLockContentionBanner])\n\n const topBannerTarget = mounted ? document.getElementById('om-top-banners') : null\n\n if (!isPrimaryInstance) return null\n if (!state?.lock) return conflictDialog\n\n const defaultPresenceMessage = activeParticipantCount > 1\n ? `${activeParticipantCount} users are currently on this record.`\n : 'This record is currently locked by another user.'\n const bannerMessageRaw = !mine\n ? t('record_locks.banner.locked_by_other', 'This record is currently locked by another user.')\n : showLockContentionBanner\n ? t('record_locks.banner.contention_notice', 'Another user opened this record while you are editing it. Conflicts may occur on save.')\n : t('record_locks.banner.participants_notice', 'Multiple users are currently on this record.')\n const bannerMessage = typeof bannerMessageRaw === 'string' && bannerMessageRaw.trim().length > 0\n ? bannerMessageRaw\n : defaultPresenceMessage\n const shouldShowPresenceBanner = Boolean(\n showLockContentionBanner\n || activeParticipantCount > 1\n || !mine,\n )\n\n const lockBanner = shouldShowPresenceBanner ? (\n <div className=\"rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-900 shadow-sm\">\n <div className=\"font-medium\">\n {bannerMessage}\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-amber-900/90\">\n <span>{`${t('record_locks.banner.active_users_label', 'Active users')}: ${activeParticipantCount}`}</span>\n {participantEmails.map((email) => (\n <span\n key={email}\n className=\"inline-flex items-center gap-1 rounded-full border border-amber-300 bg-amber-100 px-2 py-0.5 text-overline font-medium text-amber-900\"\n >\n <Mail className=\"h-3 w-3\" />\n <span>{email}</span>\n </span>\n ))}\n </div>\n <div className=\"mt-2 flex gap-2\">\n {state.allowForceUnlock && !mine ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={handleTakeOver}\n className=\"border-amber-300 bg-amber-50 text-amber-900 hover:bg-amber-100 hover:text-amber-900\"\n >\n {t('record_locks.banner.take_over', 'Take over editing')}\n </Button>\n ) : null}\n {showLockContentionBanner ? (\n <div className=\"mt-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={() => setShowLockContentionBanner(false)}\n className=\"border-amber-300 bg-amber-50 text-amber-900 hover:bg-amber-100 hover:text-amber-900\"\n >\n {t('common.close', 'Close')}\n </Button>\n </div>\n ) : null}\n </div>\n </div>\n ) : null\n\n return (\n <>\n {lockBanner ? (topBannerTarget ? createPortal(lockBanner, topBannerTarget) : lockBanner) : null}\n {conflictDialog}\n </>\n )\n}\n\nexport async function validateBeforeSave(\n data: Record<string, unknown>,\n context: CrudInjectionContext,\n): Promise<ValidateResponse> {\n const contextResourceKind = resolveResourceKind(context)\n const contextResourceId = resolveResourceId(context, data)\n const formId = resolveFormId(context, contextResourceKind, contextResourceId)\n const state = getRecordLockFormState(formId)\n const resourceKind = state?.resourceKind || contextResourceKind\n const resourceId = state?.resourceId || contextResourceId\n if (!resourceKind || !resourceId) {\n return { ok: true }\n }\n const hasSelectedConflictResolution = Boolean(\n state?.pendingResolutionArmed === true\n && typeof state?.pendingResolution === 'string'\n && state.pendingResolution !== 'normal',\n )\n if (state?.conflict && !hasSelectedConflictResolution) {\n return {\n ok: false,\n status: 409,\n code: 'record_lock_conflict',\n lock: state.lock ?? null,\n conflict: state.conflict,\n latestActionLogId: state.latestActionLogId ?? null,\n }\n }\n const hasResolvableConflict = Boolean(state?.conflict?.id && isUuid(state.conflict.id))\n const requestedResolution = state?.pendingResolution ?? 'normal'\n const resolution = requestedResolution !== 'normal' && !hasResolvableConflict\n ? 'normal'\n : requestedResolution\n const rawConflictId = resolution === 'normal' || !hasResolvableConflict\n ? undefined\n : (state?.pendingConflictId ?? state?.conflict?.id ?? undefined)\n const conflictId = isUuid(rawConflictId) ? rawConflictId : undefined\n const call = await apiCall<ValidateResponse>('/api/record_locks/validate', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind,\n resourceId,\n method: 'PUT',\n token: state?.lock?.token ?? undefined,\n baseLogId: state?.latestActionLogId ?? state?.lock?.baseActionLogId ?? undefined,\n conflictId,\n resolution,\n mutationPayload: data,\n }),\n })\n const payload = call.result ?? { ok: false }\n if (payload.ok) {\n const nextResolution = resolution === 'normal' ? 'normal' : resolution\n const preserveConflictUntilSuccessfulSave = nextResolution !== 'normal'\n setRecordLockFormState(formId, {\n resourceKind,\n resourceId,\n latestActionLogId: payload.latestActionLogId ?? state?.latestActionLogId ?? null,\n lock: resolveLockAfterValidate(state?.lock, payload.lock),\n conflict: preserveConflictUntilSuccessfulSave ? (state?.conflict ?? null) : null,\n pendingConflictId: nextResolution === 'normal' ? null : (conflictId ?? state?.pendingConflictId ?? null),\n pendingResolution: nextResolution,\n pendingResolutionArmed: nextResolution === 'normal' ? false : Boolean(state?.pendingResolutionArmed),\n })\n return payload\n }\n setRecordLockFormState(formId, {\n resourceKind,\n resourceId,\n lock: resolveLockAfterValidate(state?.lock, payload.lock),\n conflict: payload.conflict ?? state?.conflict ?? null,\n pendingConflictId: payload.conflict?.id ?? conflictId ?? state?.pendingConflictId ?? null,\n pendingResolution: resolution === 'normal' ? 'normal' : resolution,\n pendingResolutionArmed: resolution === 'normal' ? false : Boolean(state?.pendingResolutionArmed),\n })\n return payload\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { BACKEND_MUTATION_ERROR_EVENT } from '@open-mercato/ui/backend/injection/mutationEvents'\nimport { registerRecordLockConflictHandler } from '@open-mercato/ui/backend/conflicts'\nimport { useSearchParams } from 'next/navigation'\nimport { Mail } from 'lucide-react'\nimport {\n RECORD_LOCKS_FORCE_RELEASED_EVENT,\n RECORD_LOCKS_INCOMING_CHANGES_EVENT,\n RECORD_LOCKS_LOCK_CONTENDED_EVENT,\n RECORD_LOCKS_RECORD_DELETED_EVENT,\n} from '@open-mercato/enterprise/modules/record_locks/notifications.handlers'\nimport {\n ChangedFieldsTable,\n type ChangeRow,\n} from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport {\n clearRecordLockFormState,\n getRecordLockFormState,\n setRecordLockFormState,\n subscribeRecordLockFormState,\n type RecordLockFormState,\n type RecordLockUiConflict,\n type RecordLockUiView,\n} from '@open-mercato/enterprise/modules/record_locks/lib/clientLockStore'\nimport { isUuid, resolveConflictId, runAcceptIncoming } from './conflictResolution'\nimport { isOptimisticLockFloorConflict } from '@open-mercato/enterprise/modules/record_locks/lib/optimisticLockFloor'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('record_locks').child({ component: 'record-locking-widget' })\n\ntype CrudInjectionContext = {\n formId?: string\n entityId?: string\n resourceKind?: string\n resourceId?: string\n recordId?: string\n path?: string\n query?: string\n kind?: string\n personId?: string\n companyId?: string\n dealId?: string\n retryLastMutation?: () => Promise<boolean | void> | boolean | void\n}\n\ntype RecordLockWidgetOwner = {\n instanceId: string\n priority: number\n formId: string\n}\n\nconst GLOBAL_RECORD_LOCK_OWNERS_KEY = '__openMercatoRecordLockWidgetOwners__'\n\nfunction getRecordLockOwnerMap(): Map<string, RecordLockWidgetOwner> {\n const store = globalThis as Record<string, unknown>\n const existing = store[GLOBAL_RECORD_LOCK_OWNERS_KEY]\n if (existing instanceof Map) return existing as Map<string, RecordLockWidgetOwner>\n const next = new Map<string, RecordLockWidgetOwner>()\n store[GLOBAL_RECORD_LOCK_OWNERS_KEY] = next\n return next\n}\n\ntype AcquireResponse = {\n ok?: boolean\n acquired?: boolean\n allowForceUnlock?: boolean\n heartbeatSeconds?: number\n latestActionLogId?: string | null\n lock?: RecordLockUiView | null\n currentUserId?: string\n error?: string\n code?: string\n}\n\ntype ValidateResponse = {\n ok: boolean\n status?: number\n code?: string\n latestActionLogId?: string | null\n lock?: RecordLockUiView | null\n conflict?: RecordLockUiConflict | null\n}\n\ntype CrudSaveErrorEventDetail = {\n contextId?: string\n formId?: string\n error?: unknown\n}\n\ntype RecordLockContendedEventDetail = {\n sourceEntityId?: string | null\n}\n\ntype RecordDeletedEventDetail = {\n resourceId?: string | null\n resourceKind?: string | null\n}\n\ntype RecordLockIncomingChangesEventDetail = {\n resourceId?: string | null\n resourceKind?: string | null\n}\n\ntype RecordLockForceReleasedEventDetail = {\n resourceId?: string | null\n resourceKind?: string | null\n}\n\nfunction isObjectRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object'\n}\n\nfunction readStringOrNull(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction extractErrorStatus(error: unknown): number | null {\n const queue: unknown[] = [error]\n const visited = new Set<unknown>()\n\n while (queue.length > 0) {\n const current = queue.shift()\n if (!current || visited.has(current)) continue\n visited.add(current)\n if (!isObjectRecord(current)) continue\n\n const status = current.status\n if (typeof status === 'number' && Number.isFinite(status)) return status\n if (typeof status === 'string') {\n const parsed = Number(status)\n if (Number.isFinite(parsed)) return parsed\n }\n\n const statusCode = current.statusCode\n if (typeof statusCode === 'number' && Number.isFinite(statusCode)) return statusCode\n if (typeof statusCode === 'string') {\n const parsed = Number(statusCode)\n if (Number.isFinite(parsed)) return parsed\n }\n\n const nested = ['body', 'response', 'data', 'details', 'error', 'cause']\n for (const key of nested) {\n const next = current[key]\n if (next && !visited.has(next)) queue.push(next)\n }\n }\n\n return null\n}\n\nfunction extractRecordLockConflictPayload(error: unknown): {\n conflict: RecordLockUiConflict\n lock?: RecordLockUiView | null\n latestActionLogId?: string | null\n} | null {\n const queue: unknown[] = [error]\n const visited = new Set<unknown>()\n\n while (queue.length > 0) {\n const current = queue.shift()\n if (!current || visited.has(current)) continue\n visited.add(current)\n if (!isObjectRecord(current)) continue\n\n const nested = ['body', 'response', 'data', 'details', 'error']\n for (const key of nested) {\n const next = current[key]\n if (next && !visited.has(next)) queue.push(next)\n }\n\n const code = typeof current.code === 'string' ? current.code : null\n const status = extractErrorStatus(current)\n const hasLockMarkers = (\n isObjectRecord(current.lock)\n || isObjectRecord(current.conflict)\n || typeof current.conflictId === 'string'\n || typeof current.resourceKind === 'string'\n || typeof current.resourceId === 'string'\n || Array.isArray(current.resolutionOptions)\n || typeof current.allowIncomingOverride === 'boolean'\n || typeof current.canOverrideIncoming === 'boolean'\n )\n const message = typeof current.message === 'string'\n ? current.message.toLowerCase()\n : typeof current.error === 'string'\n ? current.error.toLowerCase()\n : ''\n const looksLikeLockConflictMessage = (\n message.includes('record conflict')\n || message.includes('record_lock_conflict')\n || message.includes('conflict detected')\n )\n const isRecordLockConflict = (\n code === 'record_lock_conflict'\n || (status === 409 && (hasLockMarkers || looksLikeLockConflictMessage))\n )\n if (!isRecordLockConflict) continue\n if (!isObjectRecord(current.conflict)) {\n const lock = isObjectRecord(current.lock) ? (current.lock as RecordLockUiView) : undefined\n const fallbackConflictId = typeof current.conflictId === 'string' && isUuid(current.conflictId)\n ? current.conflictId\n : 'unresolved'\n const fallbackConflict: RecordLockUiConflict = {\n id: fallbackConflictId,\n resourceKind:\n (typeof current.resourceKind === 'string' && current.resourceKind.trim().length > 0\n ? current.resourceKind\n : lock?.resourceKind) ?? '',\n resourceId:\n (typeof current.resourceId === 'string' && current.resourceId.trim().length > 0\n ? current.resourceId\n : lock?.resourceId) ?? '',\n baseActionLogId:\n typeof current.baseActionLogId === 'string' || current.baseActionLogId === null\n ? current.baseActionLogId\n : lock?.baseActionLogId ?? null,\n incomingActionLogId:\n typeof current.incomingActionLogId === 'string' || current.incomingActionLogId === null\n ? current.incomingActionLogId\n : null,\n allowIncomingOverride: Boolean(current.allowIncomingOverride),\n canOverrideIncoming: Boolean(current.canOverrideIncoming),\n resolutionOptions: Array.isArray(current.resolutionOptions) && current.resolutionOptions.includes('accept_mine')\n ? ['accept_mine']\n : [],\n changes: [],\n }\n return {\n conflict: fallbackConflict,\n lock,\n latestActionLogId: typeof current.latestActionLogId === 'string' || current.latestActionLogId === null\n ? current.latestActionLogId\n : undefined,\n }\n }\n return {\n conflict: current.conflict as RecordLockUiConflict,\n lock: isObjectRecord(current.lock) ? (current.lock as RecordLockUiView) : undefined,\n latestActionLogId: typeof current.latestActionLogId === 'string' || current.latestActionLogId === null\n ? current.latestActionLogId\n : undefined,\n }\n }\n\n return null\n}\n\nfunction isRecordDeletedError(error: unknown): boolean {\n const queue: unknown[] = [error]\n const visited = new Set<unknown>()\n\n while (queue.length > 0) {\n const current = queue.shift()\n if (!current || visited.has(current)) continue\n visited.add(current)\n if (!isObjectRecord(current)) continue\n\n const status = extractErrorStatus(current)\n const code = typeof current.code === 'string' ? current.code.toLowerCase() : ''\n const message = typeof current.message === 'string'\n ? current.message.toLowerCase()\n : typeof current.error === 'string'\n ? current.error.toLowerCase()\n : ''\n\n const matchesCode = (\n code === 'record_not_found'\n || code === 'not_found'\n || code === 'record_deleted'\n )\n const matchesMessage = (\n message.includes('not found')\n || message.includes('was deleted')\n || message.includes('record deleted')\n )\n\n if (status === 404 || matchesCode || matchesMessage) {\n return true\n }\n\n const nested = ['body', 'response', 'data', 'details', 'error', 'cause']\n for (const key of nested) {\n const next = current[key]\n if (next && !visited.has(next)) queue.push(next)\n }\n }\n\n return false\n}\n\n/**\n * Action the record-lock widget takes when a CrudForm / guarded-mutation save\n * error event (`om:crud-save-error` / backend-mutation-error) fires. Extracted\n * as a pure function so the single-conflict-surface (S3) decision is unit-testable\n * without rendering the widget:\n *\n * - `ignore` \u2014 event is for a different form/record, there is no\n * actionable mounted record, OR the error is a plain OSS\n * `optimistic_lock_conflict` 409 already owned by the OSS\n * conflict bar via `surfaceRecordConflict` (opening the\n * dialog too would render TWO surfaces \u2014 #3504/#3505).\n * - `record-deleted` \u2014 the record was deleted by another user.\n * - `apply-conflict` \u2014 a genuine enterprise `record_lock_conflict` payload;\n * open the field-level merge dialog with it.\n * - `fallback-dialog` \u2014 an unrecognized 409 (neither record-lock nor OSS\n * optimistic-lock) for the mounted record; open the\n * degraded fallback dialog (legacy behavior).\n */\nexport type RecordLockSaveErrorDecision =\n | { action: 'ignore' }\n | { action: 'record-deleted' }\n | {\n action: 'apply-conflict'\n payload: {\n conflict: RecordLockUiConflict\n lock?: RecordLockUiView | null\n latestActionLogId?: string | null\n }\n }\n | { action: 'fallback-dialog' }\n\nexport function resolveRecordLockSaveErrorDecision(args: {\n error: unknown\n eventContextId: string | null | undefined\n formId: string\n currentState: { resourceKind?: string | null; resourceId?: string | null } | null | undefined\n}): RecordLockSaveErrorDecision {\n const { error, eventContextId, formId, currentState } = args\n const payload = extractRecordLockConflictPayload(error)\n const eventTargetsCurrentForm = !eventContextId || eventContextId === formId\n if (!eventTargetsCurrentForm) {\n if (!payload || !currentState?.resourceKind || !currentState?.resourceId) return { action: 'ignore' }\n const payloadResourceKind = payload.conflict.resourceKind?.trim() ?? ''\n const payloadResourceId = payload.conflict.resourceId?.trim() ?? ''\n if (!payloadResourceKind || !payloadResourceId) return { action: 'ignore' }\n if (payloadResourceKind !== currentState.resourceKind || payloadResourceId !== currentState.resourceId) {\n return { action: 'ignore' }\n }\n }\n if (!payload) {\n if (!currentState?.resourceKind || !currentState?.resourceId) return { action: 'ignore' }\n if (isRecordDeletedError(error)) return { action: 'record-deleted' }\n // A plain OSS `optimistic_lock_conflict` 409 (the shape `makeCrudRoute` /\n // `CrudForm` returns on a stale write) is already surfaced by the OSS conflict\n // bar via `surfaceRecordConflict`. Defer to it instead of opening the degraded\n // fallback merge dialog \u2014 otherwise both surfaces render at once (#3504) and the\n // dialog has no field diff and a no-op \"Accept incoming\" (#3505). The rich merge\n // dialog stays reserved for genuine `record_lock_conflict` payloads (handled\n // above) whose conflict carries field changes + resolution options. Delegates to\n // the shared `optimisticLockFloor` detector so this arbitration stays identical to\n // the conflict bar's ownership decision (single source of truth).\n if (isOptimisticLockFloorConflict(error)) return { action: 'ignore' }\n if (extractErrorStatus(error) === 409) return { action: 'fallback-dialog' }\n return { action: 'ignore' }\n }\n return { action: 'apply-conflict', payload }\n}\n\n/**\n * Merge the lock returned by `/api/record_locks/validate` into the widget's\n * current lock state. `/validate` intentionally strips the token from the\n * caller's OWN lock (`toLockView(..., includeToken=false)`), so a naive\n * `payloadLock ?? currentLock` overwrites a good token with `null`, flips\n * `mine` false, and flashes a spurious \"locked by another user\" banner for the\n * ~1s round-trip of every save (#5289). Preserve the current token when the\n * response describes the same *active* lock id; a genuinely different (foreign)\n * lock, or a same-id lock the server no longer reports as active, keeps its null\n * token so real contention banners and re-acquisition still work.\n */\nexport function resolveLockAfterValidate(\n currentLock: RecordLockUiView | null | undefined,\n payloadLock: RecordLockUiView | null | undefined,\n): RecordLockUiView | null {\n if (!payloadLock) return currentLock ?? null\n if (\n payloadLock.token == null\n && payloadLock.status === 'active'\n && currentLock\n && currentLock.id === payloadLock.id\n ) {\n return { ...payloadLock, token: currentLock.token }\n }\n return payloadLock\n}\n\nfunction clearIncomingChangesQueryFlag() {\n if (typeof window === 'undefined') return\n try {\n const url = new URL(window.location.href)\n if (url.searchParams.get('showIncomingChanges') !== '1') return\n url.searchParams.delete('showIncomingChanges')\n const nextUrl = `${url.pathname}${url.search}${url.hash}`\n window.history.replaceState(window.history.state, '', nextUrl)\n } catch {\n // ignore URL parse failures\n }\n}\n\nfunction clearLockContentionQueryFlag() {\n if (typeof window === 'undefined') return\n try {\n const url = new URL(window.location.href)\n if (url.searchParams.get('showLockContention') !== '1') return\n url.searchParams.delete('showLockContention')\n const nextUrl = `${url.pathname}${url.search}${url.hash}`\n window.history.replaceState(window.history.state, '', nextUrl)\n } catch {\n // ignore URL parse failures\n }\n}\n\nfunction submitCrudForm(formId: string): boolean {\n if (typeof document === 'undefined') return false\n const form = document.getElementById(formId)\n if (!(form instanceof HTMLFormElement)) return false\n form.requestSubmit()\n return true\n}\n\nfunction resolveResourceKind(context: CrudInjectionContext): string | null {\n if (context.resourceKind && context.resourceKind.trim()) return context.resourceKind\n if (context.kind === 'order') return 'sales.order'\n if (context.kind === 'quote') return 'sales.quote'\n if (context.personId) return 'customers.person'\n if (context.companyId) return 'customers.company'\n if (context.dealId) return 'customers.deal'\n const entityId = context.entityId\n if (entityId && entityId.includes(':')) {\n const [moduleId, rawEntity] = entityId.split(':')\n const entity = rawEntity ?? ''\n const normalizedModuleId = moduleId.trim()\n const normalizedEntity = entity.trim()\n if (normalizedModuleId && normalizedEntity) {\n const singularModuleId = normalizedModuleId.endsWith('s')\n ? normalizedModuleId.slice(0, -1)\n : normalizedModuleId\n\n const stripPrefixes = [\n `${normalizedModuleId}_`,\n `${singularModuleId}_`,\n ]\n\n let finalEntity = normalizedEntity\n for (const prefix of stripPrefixes) {\n if (finalEntity.startsWith(prefix)) {\n finalEntity = finalEntity.slice(prefix.length)\n break\n }\n }\n\n if (finalEntity) return `${normalizedModuleId}.${finalEntity}`\n }\n }\n\n const path = context.path ?? ''\n if (path.startsWith('/backend/customers/people/')) return 'customers.person'\n if (path.startsWith('/backend/customers/companies/')) return 'customers.company'\n if (path.startsWith('/backend/customers/deals/')) return 'customers.deal'\n if (path.startsWith('/backend/sales/orders/')) return 'sales.order'\n if (path.startsWith('/backend/sales/quotes/')) return 'sales.quote'\n if (path.startsWith('/backend/sales/documents/')) {\n const query = context.query ?? ''\n const params = new URLSearchParams(query)\n const kind = params.get('kind')\n if (kind === 'order') return 'sales.order'\n if (kind === 'quote') return 'sales.quote'\n }\n\n return null\n}\n\nfunction resolveResourceId(context: CrudInjectionContext, data: unknown): string | null {\n if (context.resourceId && context.resourceId.trim()) return context.resourceId\n if (context.recordId && context.recordId.trim()) return context.recordId\n if (context.personId && context.personId.trim()) return context.personId\n if (context.companyId && context.companyId.trim()) return context.companyId\n if (context.dealId && context.dealId.trim()) return context.dealId\n if (data && typeof data === 'object' && 'id' in data) {\n const id = (data as { id?: unknown }).id\n if (typeof id === 'string' && id.trim()) return id\n }\n if (data && typeof data === 'object') {\n const nestedPersonId = (data as { person?: { id?: unknown } }).person?.id\n if (typeof nestedPersonId === 'string' && nestedPersonId.trim()) return nestedPersonId\n const nestedCompanyId = (data as { company?: { id?: unknown } }).company?.id\n if (typeof nestedCompanyId === 'string' && nestedCompanyId.trim()) return nestedCompanyId\n const nestedDealId = (data as { deal?: { id?: unknown } }).deal?.id\n if (typeof nestedDealId === 'string' && nestedDealId.trim()) return nestedDealId\n }\n const path = context.path ?? ''\n const parts = path.split('/').filter((part) => part.length > 0)\n const candidates = [\n ['backend', 'customers', 'people'],\n ['backend', 'customers', 'companies'],\n ['backend', 'customers', 'deals'],\n ['backend', 'sales', 'orders'],\n ['backend', 'sales', 'quotes'],\n ['backend', 'sales', 'documents'],\n ] as const\n for (const prefix of candidates) {\n const matchesPrefix = prefix.every((segment, index) => parts[index] === segment)\n if (!matchesPrefix || parts.length <= prefix.length) continue\n const rawId = parts[prefix.length] ?? ''\n if (!rawId) continue\n try {\n const decoded = decodeURIComponent(rawId).trim()\n if (decoded.length > 0) return decoded\n } catch {\n const trimmed = rawId.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n}\n\nfunction resolveFormId(\n context: CrudInjectionContext,\n resourceKind: string | null,\n resourceId: string | null,\n): string {\n if (context.formId && context.formId.trim().length > 0) return context.formId\n if (resourceKind && resourceId) return `record-lock:${resourceKind}:${resourceId}`\n if (context.path && context.path.trim().length > 0) {\n const query = context.query?.trim()\n return `record-lock:${context.path}${query ? `?${query}` : ''}`\n }\n return 'record-lock:global'\n}\n\nasync function releaseLock(state: {\n resourceKind: string\n resourceId: string\n token?: string | null\n reason?: 'saved' | 'cancelled' | 'unmount'\n}) {\n await apiCall('/api/record_locks/release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: state.token ?? undefined,\n reason: state.reason ?? 'cancelled',\n }),\n })\n}\n\nfunction releaseLockWithKeepalive(state: {\n resourceKind: string\n resourceId: string\n token?: string | null\n reason?: 'saved' | 'cancelled' | 'unmount'\n}) {\n const payload = JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: state.token ?? undefined,\n reason: state.reason ?? 'unmount',\n })\n\n try {\n if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {\n const blob = new Blob([payload], { type: 'application/json' })\n const sent = navigator.sendBeacon('/api/record_locks/release', blob)\n if (sent) return\n }\n } catch {\n // ignore and fallback to fetch\n }\n\n void fetch('/api/record_locks/release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: payload,\n keepalive: true,\n credentials: 'include',\n }).catch((error) => {\n logger.warn('Failed to release lock with keepalive fallback', { err: error })\n })\n}\n\nexport default function RecordLockingWidget({\n context,\n data,\n}: InjectionWidgetComponentProps<CrudInjectionContext, Record<string, unknown>>) {\n const t = useT()\n const searchParams = useSearchParams()\n const resourceKind = React.useMemo(() => resolveResourceKind(context), [context])\n const resourceId = React.useMemo(() => resolveResourceId(context, data), [context, data])\n const formId = React.useMemo(\n () => resolveFormId(context, resourceKind, resourceId),\n [context, resourceId, resourceKind],\n )\n const [, forceRender] = React.useReducer((value) => value + 1, 0)\n const state = getRecordLockFormState(formId)\n const [mounted, setMounted] = React.useState(false)\n const [showIncomingChangesRequested, setShowIncomingChangesRequested] = React.useState(false)\n const [showLockContentionBanner, setShowLockContentionBanner] = React.useState(false)\n const [isConflictDialogOpen, setIsConflictDialogOpen] = React.useState(false)\n const instanceId = React.useMemo(\n () =>\n `record-lock-widget:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`,\n [],\n )\n const ownerPriority = context.formId ? 2 : 1\n const ownerKey = resourceKind && resourceId ? `${resourceKind}:${resourceId}` : null\n const [isPrimaryInstance, setIsPrimaryInstance] = React.useState(true)\n const releasePayloadRef = React.useRef<{\n resourceKind: string\n resourceId: string\n token: string\n } | null>(null)\n const keepMineRetryVersionRef = React.useRef(0)\n const hasAnotherOwnerForForm = React.useCallback(() => {\n if (!ownerKey) return false\n const owner = getRecordLockOwnerMap().get(ownerKey)\n return Boolean(owner && owner.instanceId !== instanceId && owner.formId === formId)\n }, [formId, instanceId, ownerKey])\n\n React.useEffect(() => {\n if (!ownerKey) {\n setIsPrimaryInstance(true)\n return\n }\n\n const owners = getRecordLockOwnerMap()\n const notifyOwnersChanged = () => {\n if (typeof window === 'undefined') return\n window.dispatchEvent(\n new CustomEvent('om:record-lock-owner-changed', {\n detail: { ownerKey },\n }),\n )\n }\n\n const claimOwnership = () => {\n const current = owners.get(ownerKey)\n if (!current) {\n owners.set(ownerKey, { instanceId, priority: ownerPriority, formId })\n setIsPrimaryInstance(true)\n notifyOwnersChanged()\n return\n }\n if (current.instanceId === instanceId) {\n owners.set(ownerKey, { instanceId, priority: ownerPriority, formId })\n setIsPrimaryInstance(true)\n return\n }\n if (ownerPriority > current.priority) {\n owners.set(ownerKey, { instanceId, priority: ownerPriority, formId })\n setIsPrimaryInstance(true)\n notifyOwnersChanged()\n return\n }\n setIsPrimaryInstance(false)\n }\n\n claimOwnership()\n const onOwnersChanged = () => claimOwnership()\n if (typeof window !== 'undefined') {\n window.addEventListener('om:record-lock-owner-changed', onOwnersChanged)\n }\n\n return () => {\n if (typeof window !== 'undefined') {\n window.removeEventListener('om:record-lock-owner-changed', onOwnersChanged)\n }\n const current = owners.get(ownerKey)\n if (current?.instanceId === instanceId) {\n owners.delete(ownerKey)\n notifyOwnersChanged()\n }\n }\n }, [formId, instanceId, ownerKey, ownerPriority])\n\n React.useEffect(() => {\n if (isPrimaryInstance) return\n if (hasAnotherOwnerForForm()) return\n const current = getRecordLockFormState(formId)\n if (!current?.lock?.token || !current.resourceKind || !current.resourceId) {\n clearRecordLockFormState(formId)\n return\n }\n void releaseLock({\n resourceKind: current.resourceKind,\n resourceId: current.resourceId,\n token: current.lock.token,\n reason: 'cancelled',\n }).catch((error) => {\n logger.warn('Failed to release lock while demoting owner', { err: error })\n })\n clearRecordLockFormState(formId)\n }, [formId, hasAnotherOwnerForForm, isPrimaryInstance])\n\n React.useEffect(() => {\n setMounted(true)\n }, [])\n\n React.useEffect(() => {\n const showIncomingChanges = searchParams?.get('showIncomingChanges') === '1'\n const showLockContention = searchParams?.get('showLockContention') === '1'\n\n if (showIncomingChanges) {\n setShowIncomingChangesRequested(true)\n clearIncomingChangesQueryFlag()\n }\n\n if (showLockContention) {\n setShowLockContentionBanner(true)\n clearLockContentionQueryFlag()\n }\n }, [searchParams])\n\n React.useEffect(() => subscribeRecordLockFormState(formId, () => forceRender()), [formId])\n\n React.useEffect(() => {\n if (!state?.conflict) {\n setIsConflictDialogOpen(false)\n return\n }\n setIsConflictDialogOpen(true)\n }, [\n state?.conflict?.id,\n state?.conflict?.incomingActionLogId,\n state?.conflict?.baseActionLogId,\n ])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!resourceKind || !resourceId) return\n setRecordLockFormState(formId, { formId, resourceKind, resourceId })\n }, [formId, isPrimaryInstance, resourceId, resourceKind])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!resourceKind || !resourceId) return\n let active = true\n const acquire = async () => {\n const requestedState = getRecordLockFormState(formId)\n const call = await apiCall<AcquireResponse>('/api/record_locks/acquire', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ resourceKind, resourceId }),\n })\n const payload = call.result ?? {}\n const currentState = getRecordLockFormState(formId)\n if (\n !active\n || !currentState\n || currentState.recordDeleted === true\n || currentState.resourceKind !== resourceKind\n || currentState.resourceId !== resourceId\n || (currentState.lock?.token ?? null) !== (requestedState?.lock?.token ?? null)\n ) return\n const latestActionLogId = currentState.latestActionLogId !== undefined\n ? currentState.latestActionLogId\n : (payload.latestActionLogId ?? null)\n if (!call.ok) {\n const defaultMessage = call.status === 403\n ? t('api.errors.forbidden', 'Forbidden')\n : t('record_locks.errors.acquire_failed', 'Failed to load record lock status.')\n const message = typeof payload.error === 'string' && payload.error.trim().length\n ? payload.error\n : defaultMessage\n flash(message, 'error')\n setRecordLockFormState(formId, {\n formId,\n resourceKind,\n resourceId,\n acquired: false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? 15,\n latestActionLogId,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n })\n return\n }\n setRecordLockFormState(formId, {\n formId,\n resourceKind,\n resourceId,\n acquired: payload.acquired ?? false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? 15,\n latestActionLogId,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n })\n }\n void acquire()\n return () => {\n active = false\n }\n }, [formId, isPrimaryInstance, resourceId, resourceKind])\n\n const mine = Boolean(state?.lock?.token)\n const participants = React.useMemo(() => {\n if (!state?.lock) return []\n const fromPayload = Array.isArray(state.lock.participants) ? state.lock.participants : []\n if (fromPayload.length) return fromPayload\n return [{\n userId: state.lock.lockedByUserId,\n lockedByName: state.lock.lockedByName,\n lockedByEmail: state.lock.lockedByEmail,\n lockedByIp: state.lock.lockedByIp,\n lockedAt: state.lock.lockedAt,\n lastHeartbeatAt: state.lock.lastHeartbeatAt,\n expiresAt: state.lock.expiresAt,\n }]\n }, [state?.lock])\n const activeParticipantCount = state?.lock?.activeParticipantCount ?? participants.length\n const otherParticipants = React.useMemo(() => {\n if (!state?.currentUserId) return participants\n return participants.filter((participant) => participant.userId !== state.currentUserId)\n }, [participants, state?.currentUserId])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!mine || !state?.lock?.id) return\n\n const onContention = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? ((event as CustomEvent<unknown>).detail as RecordLockContendedEventDetail)\n : null\n if (!detail) return\n if (detail.sourceEntityId !== state.lock?.id) return\n setShowLockContentionBanner(true)\n }\n\n window.addEventListener(RECORD_LOCKS_LOCK_CONTENDED_EVENT, onContention)\n\n return () => {\n window.removeEventListener(RECORD_LOCKS_LOCK_CONTENDED_EVENT, onContention)\n }\n }, [isPrimaryInstance, mine, state?.lock?.id])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!state?.resourceKind || !state?.resourceId) return\n if (state.recordDeleted === true) return\n const onRecordDeleted = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? ((event as CustomEvent<unknown>).detail as RecordDeletedEventDetail)\n : null\n if (!detail) return\n if (detail.resourceId !== state.resourceId) return\n const kind = readStringOrNull(detail.resourceKind)\n if (kind && kind !== state.resourceKind) return\n setIsConflictDialogOpen(true)\n setRecordLockFormState(formId, {\n recordDeleted: true,\n acquired: false,\n lock: null,\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n }\n\n window.addEventListener(RECORD_LOCKS_RECORD_DELETED_EVENT, onRecordDeleted)\n\n return () => {\n window.removeEventListener(RECORD_LOCKS_RECORD_DELETED_EVENT, onRecordDeleted)\n }\n }, [formId, isPrimaryInstance, state?.recordDeleted, state?.resourceId, state?.resourceKind])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!state?.lock?.token || !state.resourceKind || !state.resourceId) return\n const heartbeatSeconds = (\n typeof state.heartbeatSeconds === 'number'\n && Number.isFinite(state.heartbeatSeconds)\n && state.heartbeatSeconds > 0\n )\n ? state.heartbeatSeconds\n : 10\n const intervalMs = Math.max(1_000, Math.round(heartbeatSeconds * 1000))\n const interval = window.setInterval(() => {\n void apiCall('/api/record_locks/heartbeat', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: state.lock?.token,\n }),\n })\n }, intervalMs)\n return () => window.clearInterval(interval)\n }, [isPrimaryInstance, state?.heartbeatSeconds, state?.lock?.token, state?.resourceId, state?.resourceKind])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n const hasUnresolvedConflict = Boolean(state?.conflict)\n && !(\n state?.pendingResolutionArmed === true\n && typeof state?.pendingResolution === 'string'\n && state.pendingResolution !== 'normal'\n )\n if (hasUnresolvedConflict) return\n if (!state?.resourceKind || !state?.resourceId) return\n if (state.recordDeleted === true) return\n let active = true\n const refreshPresence = async () => {\n const requestedState = getRecordLockFormState(formId)\n if (\n !active\n || !requestedState\n || requestedState.recordDeleted === true\n || requestedState.resourceKind !== state.resourceKind\n || requestedState.resourceId !== state.resourceId\n ) return\n const call = await apiCall<AcquireResponse>('/api/record_locks/acquire', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n }),\n })\n const payload = call.result ?? {}\n const currentState = getRecordLockFormState(formId)\n if (\n !active\n || !currentState\n || currentState.recordDeleted === true\n || currentState.resourceKind !== requestedState.resourceKind\n || currentState.resourceId !== requestedState.resourceId\n || (currentState.lock?.token ?? null) !== (requestedState.lock?.token ?? null)\n ) return\n const nextLatestActionLogId = currentState.latestActionLogId !== undefined\n ? currentState.latestActionLogId\n : (payload.latestActionLogId ?? null)\n if (!call.ok) {\n setRecordLockFormState(formId, {\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n acquired: false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? currentState?.currentUserId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? currentState?.heartbeatSeconds ?? 15,\n latestActionLogId: nextLatestActionLogId,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n })\n return\n }\n setRecordLockFormState(formId, {\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n acquired: payload.acquired ?? false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? 15,\n latestActionLogId: nextLatestActionLogId,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n })\n }\n const onIncomingChanges = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? ((event as CustomEvent<unknown>).detail as RecordLockIncomingChangesEventDetail)\n : null\n if (!detail) return\n if (detail.resourceId !== state.resourceId) return\n const kind = readStringOrNull(detail.resourceKind)\n if (kind && kind !== state.resourceKind) return\n setShowIncomingChangesRequested(true)\n void refreshPresence()\n }\n const onForceReleased = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? ((event as CustomEvent<unknown>).detail as RecordLockForceReleasedEventDetail)\n : null\n if (!detail) return\n if (detail.resourceId !== state.resourceId) return\n const kind = readStringOrNull(detail.resourceKind)\n if (kind && kind !== state.resourceKind) return\n void refreshPresence()\n }\n const onBridgeReconnected = (event: Event) => {\n const detail = isObjectRecord((event as CustomEvent<unknown>).detail)\n ? (event as CustomEvent<Record<string, unknown>>).detail\n : null\n if (detail?.id !== 'om:bridge:reconnected') return\n void refreshPresence()\n }\n const onFocus = () => {\n void refreshPresence()\n }\n const onVisibilityChange = () => {\n if (!document.hidden) {\n void refreshPresence()\n }\n }\n\n window.addEventListener(RECORD_LOCKS_INCOMING_CHANGES_EVENT, onIncomingChanges)\n window.addEventListener(RECORD_LOCKS_FORCE_RELEASED_EVENT, onForceReleased)\n window.addEventListener('om:event', onBridgeReconnected)\n window.addEventListener('focus', onFocus)\n document.addEventListener('visibilitychange', onVisibilityChange)\n const heartbeatInterval = window.setInterval(() => {\n void refreshPresence()\n }, 30_000)\n void refreshPresence()\n\n return () => {\n active = false\n window.removeEventListener(RECORD_LOCKS_INCOMING_CHANGES_EVENT, onIncomingChanges)\n window.removeEventListener(RECORD_LOCKS_FORCE_RELEASED_EVENT, onForceReleased)\n window.removeEventListener('om:event', onBridgeReconnected)\n window.removeEventListener('focus', onFocus)\n document.removeEventListener('visibilitychange', onVisibilityChange)\n window.clearInterval(heartbeatInterval)\n }\n }, [\n formId,\n isPrimaryInstance,\n state?.conflict,\n state?.pendingResolution,\n state?.pendingResolutionArmed,\n state?.recordDeleted,\n state?.resourceId,\n state?.resourceKind,\n ])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!showIncomingChangesRequested) return\n if (!state?.resourceKind || !state?.resourceId || !state?.lock) return\n let cancelled = false\n const openIncomingChangesDialog = async () => {\n clearIncomingChangesQueryFlag()\n setShowIncomingChangesRequested(false)\n await validateBeforeSave({}, context)\n if (cancelled) return\n }\n void openIncomingChangesDialog()\n return () => {\n cancelled = true\n }\n }, [context, isPrimaryInstance, showIncomingChangesRequested, state?.lock, state?.resourceId, state?.resourceKind, t])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (\n state?.resourceKind\n && state?.resourceId\n && typeof state.lock?.token === 'string'\n && state.lock.token.trim().length > 0\n ) {\n releasePayloadRef.current = {\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: state.lock.token,\n }\n return\n }\n releasePayloadRef.current = null\n }, [isPrimaryInstance, state?.lock?.token, state?.resourceId, state?.resourceKind])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n const onPageHide = () => {\n const payload = releasePayloadRef.current\n if (!payload) return\n releaseLockWithKeepalive({\n ...payload,\n reason: 'unmount',\n })\n }\n window.addEventListener('pagehide', onPageHide)\n return () => {\n window.removeEventListener('pagehide', onPageHide)\n }\n }, [isPrimaryInstance])\n\n React.useEffect(() => {\n return () => {\n if (hasAnotherOwnerForForm()) return\n const current = getRecordLockFormState(formId)\n if (current?.lock?.token && current.resourceKind && current.resourceId) {\n void releaseLock({\n resourceKind: current.resourceKind,\n resourceId: current.resourceId,\n token: current.lock.token,\n reason: 'unmount',\n })\n }\n clearRecordLockFormState(formId)\n }\n }, [formId, hasAnotherOwnerForForm])\n\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n const onCrudSaveError = (event: Event) => {\n const applyConflictPayload = (payload: {\n conflict: RecordLockUiConflict\n lock?: RecordLockUiView | null\n latestActionLogId?: string | null\n }) => {\n setIsConflictDialogOpen(true)\n const nextPatch: Partial<RecordLockFormState> = {\n conflict: payload.conflict,\n pendingConflictId: payload.conflict.id,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n }\n if (payload.lock !== undefined) {\n nextPatch.lock = resolveLockAfterValidate(getRecordLockFormState(formId)?.lock, payload.lock)\n }\n if (payload.latestActionLogId !== undefined) {\n nextPatch.latestActionLogId = payload.latestActionLogId\n }\n setRecordLockFormState(formId, {\n ...nextPatch,\n })\n }\n\n const buildFallbackConflict = (currentState: RecordLockFormState) => {\n const preferredConflictId = isUuid(currentState.pendingConflictId)\n ? currentState.pendingConflictId\n : isUuid(currentState.conflict?.id)\n ? currentState.conflict.id\n : 'unresolved'\n return ({\n conflict: {\n id: preferredConflictId,\n resourceKind: currentState.resourceKind ?? '',\n resourceId: currentState.resourceId ?? '',\n baseActionLogId: currentState.latestActionLogId ?? null,\n incomingActionLogId: null,\n allowIncomingOverride: false,\n canOverrideIncoming: false,\n resolutionOptions: [],\n changes: [],\n } as RecordLockUiConflict,\n lock: currentState.lock ?? undefined,\n latestActionLogId: currentState.latestActionLogId ?? null,\n })\n }\n\n const detail = (event as CustomEvent<CrudSaveErrorEventDetail>).detail\n if (!detail) return\n const eventContextId = detail.contextId ?? detail.formId\n const currentState = getRecordLockFormState(formId)\n const decision = resolveRecordLockSaveErrorDecision({\n error: detail.error,\n eventContextId,\n formId,\n currentState,\n })\n if (decision.action === 'ignore') return\n if (decision.action === 'record-deleted') {\n setIsConflictDialogOpen(true)\n setRecordLockFormState(formId, {\n recordDeleted: true,\n acquired: false,\n lock: null,\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n return\n }\n if (decision.action === 'fallback-dialog') {\n if (currentState) applyConflictPayload(buildFallbackConflict(currentState))\n return\n }\n\n applyConflictPayload(decision.payload)\n }\n\n window.addEventListener(BACKEND_MUTATION_ERROR_EVENT, onCrudSaveError)\n window.addEventListener('om:crud-save-error', onCrudSaveError)\n return () => {\n window.removeEventListener(BACKEND_MUTATION_ERROR_EVENT, onCrudSaveError)\n window.removeEventListener('om:crud-save-error', onCrudSaveError)\n }\n }, [formId, isPrimaryInstance])\n\n // Single conflict surface (S3): own the surface for record_lock_conflict 409s\n // routed through `surfaceRecordConflict` (command/raw-write paths that don't\n // emit the crud-save-error event). Opening the dialog here is idempotent with\n // the listener above; returning true tells the core helper to suppress the OSS\n // bar (so we never render both). Decline (false) for a different record so the\n // bar still renders there \u2014 a conflict is never swallowed.\n React.useEffect(() => {\n if (!isPrimaryInstance) return\n if (!resourceKind || !resourceId) return\n const unregister = registerRecordLockConflictHandler((_conflict, error) => {\n const payload = extractRecordLockConflictPayload(error)\n if (!payload) return false\n const payloadKind = payload.conflict.resourceKind?.trim() ?? ''\n const payloadId = payload.conflict.resourceId?.trim() ?? ''\n if (payloadKind && payloadId && (payloadKind !== resourceKind || payloadId !== resourceId)) return false\n setIsConflictDialogOpen(true)\n setRecordLockFormState(formId, {\n conflict: payload.conflict,\n pendingConflictId: payload.conflict.id,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n return true\n })\n return unregister\n }, [formId, isPrimaryInstance, resourceKind, resourceId])\n\n const handleTakeOver = React.useCallback(async () => {\n keepMineRetryVersionRef.current += 1\n if (!state?.resourceKind || !state?.resourceId) return\n const call = await apiCall<AcquireResponse>('/api/record_locks/force-release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n }),\n })\n if (!call.ok) {\n flash(t('record_locks.errors.force_release_failed', 'Failed to take over editing.'), 'error')\n return\n }\n const acquire = await apiCall<AcquireResponse>('/api/record_locks/acquire', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ resourceKind: state.resourceKind, resourceId: state.resourceId }),\n })\n if (!acquire.ok) {\n flash(t('record_locks.errors.force_release_failed', 'Failed to take over editing.'), 'error')\n return\n }\n const payload = acquire.result ?? {}\n setRecordLockFormState(formId, {\n acquired: payload.acquired ?? false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? null,\n allowForceUnlock: payload.allowForceUnlock ?? false,\n latestActionLogId: payload.latestActionLogId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? 15,\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n }, [formId, state?.resourceId, state?.resourceKind, t])\n\n const handleAcceptIncoming = React.useCallback(async () => {\n keepMineRetryVersionRef.current += 1\n await runAcceptIncoming({\n conflict: state?.conflict,\n resourceKind: state?.resourceKind,\n resourceId: state?.resourceId,\n revalidateConflictId: async () => {\n const validation = await validateBeforeSave({}, context)\n return resolveConflictId(validation.conflict)\n },\n releaseIncoming: async (conflictId) => {\n await apiCallOrThrow('/api/record_locks/release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state?.resourceKind,\n resourceId: state?.resourceId,\n token: state?.lock?.token ?? undefined,\n reason: 'conflict_resolved',\n conflictId,\n resolution: 'accept_incoming',\n }),\n })\n },\n clearConflictState: () => {\n setRecordLockFormState(formId, {\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n },\n reload: () => {\n window.location.reload()\n },\n })\n }, [context, formId, state?.conflict, state?.lock?.token, state?.resourceId, state?.resourceKind])\n\n const handleKeepMine = React.useCallback(() => {\n if (!state?.conflict) return\n const applyAcceptMine = async () => {\n const keepMineRetryVersion = keepMineRetryVersionRef.current + 1\n keepMineRetryVersionRef.current = keepMineRetryVersion\n let conflictId: string | null = isUuid(state.conflict?.id) ? state.conflict.id : null\n if (!conflictId) {\n const validation = await validateBeforeSave({}, context)\n conflictId = isUuid(validation.conflict?.id) ? validation.conflict.id : null\n }\n if (!conflictId) {\n flash(\n t(\n 'record_locks.conflict.refresh_required',\n 'Could not confirm conflict details. Save again to refresh conflict data.',\n ),\n 'error',\n )\n return\n }\n setRecordLockFormState(formId, {\n pendingResolution: 'accept_mine',\n pendingConflictId: conflictId,\n pendingResolutionArmed: true,\n })\n window.setTimeout(async () => {\n if (keepMineRetryVersionRef.current !== keepMineRetryVersion) return\n const currentState = getRecordLockFormState(formId)\n if (currentState?.pendingResolution !== 'accept_mine') return\n const submitted = submitCrudForm(formId)\n if (submitted) return\n const retried = await Promise.resolve(context.retryLastMutation?.()).catch(() => false)\n if (!retried) {\n flash(\n t(\n 'record_locks.conflict.retry_save_after_accept_mine',\n 'Click save again to apply your changes.',\n ),\n 'info',\n )\n }\n }, 0)\n }\n void applyAcceptMine()\n }, [context, context.retryLastMutation, formId, state?.conflict, t])\n\n const handleKeepEditing = React.useCallback(() => {\n keepMineRetryVersionRef.current += 1\n setIsConflictDialogOpen(false)\n }, [])\n\n const noneLabel = t('audit_logs.common.none')\n const conflictChangeRows = React.useMemo<ChangeRow[]>(\n () => (state?.conflict?.changes ?? []).map((change) => ({\n field: change.field,\n from: change.incomingValue,\n to: change.mineValue,\n })),\n [state?.conflict?.changes],\n )\n const canKeepMyChanges = Boolean(\n state?.conflict?.allowIncomingOverride\n && state?.conflict?.canOverrideIncoming === true\n && state?.conflict?.resolutionOptions?.includes('accept_mine'),\n )\n const isRecordDeleted = state?.recordDeleted === true\n const showOverrideBlockedNotice = Boolean(\n state?.conflict?.allowIncomingOverride\n && !state?.conflict?.canOverrideIncoming,\n )\n const conflictDialog = (\n <Dialog open={Boolean(state?.conflict || isRecordDeleted) && isConflictDialogOpen} onOpenChange={(open) => {\n if (open) {\n setIsConflictDialogOpen(true)\n return\n }\n if (isRecordDeleted) {\n setIsConflictDialogOpen(true)\n return\n }\n setIsConflictDialogOpen(false)\n }}>\n <DialogContent data-testid=\"record-lock-conflict-dialog\">\n <DialogHeader>\n <DialogTitle>\n {isRecordDeleted\n ? t('record_locks.conflict.record_deleted_title', 'Record was deleted')\n : t('record_locks.conflict.title', 'Conflict detected')}\n </DialogTitle>\n </DialogHeader>\n <div className=\"space-y-3 text-sm\">\n <p className=\"text-muted-foreground\">\n {isRecordDeleted\n ? t(\n 'record_locks.conflict.record_deleted_description',\n 'This record was deleted by another user while you were editing. Saving is blocked to avoid unexpected results.',\n )\n : t('record_locks.conflict.description', 'The record was changed by another user after you started editing.')}\n </p>\n {!isRecordDeleted ? (\n <ChangedFieldsTable\n changeRows={conflictChangeRows}\n noneLabel={noneLabel}\n t={t}\n beforeLabel={t('record_locks.conflict.incoming_label', 'Incoming')}\n afterLabel={t('record_locks.conflict.current_label', 'Current')}\n />\n ) : null}\n {(state?.conflict?.changes?.length ?? 0) === 0 ? (\n !isRecordDeleted ? (\n <Alert status=\"information\">\n <AlertDescription>\n {t(\n 'record_locks.conflict.no_field_details',\n 'Field-level conflict details are unavailable for this record. Choose a resolution to continue.'\n )}\n </AlertDescription>\n </Alert>\n ) : null\n ) : null}\n {showOverrideBlockedNotice ? (\n <Alert status=\"warning\">\n <AlertDescription>\n {t(\n 'record_locks.conflict.override_blocked_notice',\n 'You cannot keep your version because you do not have permission to override incoming changes.',\n )}\n </AlertDescription>\n </Alert>\n ) : null}\n <div className=\"-mx-6 -mb-6 mt-4 border-t bg-background/95 px-6 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80\">\n <div className=\"flex flex-wrap justify-end gap-2\">\n {isRecordDeleted ? null : (\n <>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n void handleAcceptIncoming()\n }}\n >\n {t('record_locks.conflict.accept_incoming', 'Accept incoming')}\n </Button>\n {canKeepMyChanges ? (\n <Button\n type=\"button\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n void handleKeepMine()\n }}\n >\n {t('record_locks.conflict.accept_mine', 'Keep my changes')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n handleKeepEditing()\n }}\n >\n {t('record_locks.conflict.keep_editing', 'Keep editing')}\n </Button>\n </>\n )}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n\n const participantEmails = React.useMemo(() => {\n return otherParticipants\n .map((participant) => participant.lockedByEmail?.trim() ?? '')\n .filter((email, index, all) => email.length > 0 && all.indexOf(email) === index)\n .slice(0, 4)\n }, [otherParticipants])\n\n React.useEffect(() => {\n if (!showLockContentionBanner) return\n if (!mine) return\n if (activeParticipantCount > 1) return\n setShowLockContentionBanner(false)\n }, [activeParticipantCount, mine, showLockContentionBanner])\n\n const topBannerTarget = mounted ? document.getElementById('om-top-banners') : null\n\n if (!isPrimaryInstance) return null\n if (!state?.lock) return conflictDialog\n\n const defaultPresenceMessage = activeParticipantCount > 1\n ? `${activeParticipantCount} users are currently on this record.`\n : 'This record is currently locked by another user.'\n const bannerMessageRaw = !mine\n ? t('record_locks.banner.locked_by_other', 'This record is currently locked by another user.')\n : showLockContentionBanner\n ? t('record_locks.banner.contention_notice', 'Another user opened this record while you are editing it. Conflicts may occur on save.')\n : t('record_locks.banner.participants_notice', 'Multiple users are currently on this record.')\n const bannerMessage = typeof bannerMessageRaw === 'string' && bannerMessageRaw.trim().length > 0\n ? bannerMessageRaw\n : defaultPresenceMessage\n const shouldShowPresenceBanner = Boolean(\n showLockContentionBanner\n || activeParticipantCount > 1\n || !mine,\n )\n\n const lockBanner = shouldShowPresenceBanner ? (\n <div className=\"rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-900 shadow-sm\">\n <div className=\"font-medium\">\n {bannerMessage}\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-amber-900/90\">\n <span>{`${t('record_locks.banner.active_users_label', 'Active users')}: ${activeParticipantCount}`}</span>\n {participantEmails.map((email) => (\n <span\n key={email}\n className=\"inline-flex items-center gap-1 rounded-full border border-amber-300 bg-amber-100 px-2 py-0.5 text-overline font-medium text-amber-900\"\n >\n <Mail className=\"h-3 w-3\" />\n <span>{email}</span>\n </span>\n ))}\n </div>\n <div className=\"mt-2 flex gap-2\">\n {state.allowForceUnlock && !mine ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={handleTakeOver}\n className=\"border-amber-300 bg-amber-50 text-amber-900 hover:bg-amber-100 hover:text-amber-900\"\n >\n {t('record_locks.banner.take_over', 'Take over editing')}\n </Button>\n ) : null}\n {showLockContentionBanner ? (\n <div className=\"mt-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={() => setShowLockContentionBanner(false)}\n className=\"border-amber-300 bg-amber-50 text-amber-900 hover:bg-amber-100 hover:text-amber-900\"\n >\n {t('common.close', 'Close')}\n </Button>\n </div>\n ) : null}\n </div>\n </div>\n ) : null\n\n return (\n <>\n {lockBanner ? (topBannerTarget ? createPortal(lockBanner, topBannerTarget) : lockBanner) : null}\n {conflictDialog}\n </>\n )\n}\n\nexport async function validateBeforeSave(\n data: Record<string, unknown>,\n context: CrudInjectionContext,\n): Promise<ValidateResponse> {\n const contextResourceKind = resolveResourceKind(context)\n const contextResourceId = resolveResourceId(context, data)\n const formId = resolveFormId(context, contextResourceKind, contextResourceId)\n const state = getRecordLockFormState(formId)\n const resourceKind = state?.resourceKind || contextResourceKind\n const resourceId = state?.resourceId || contextResourceId\n if (!resourceKind || !resourceId) {\n return { ok: true }\n }\n const hasSelectedConflictResolution = Boolean(\n state?.pendingResolutionArmed === true\n && typeof state?.pendingResolution === 'string'\n && state.pendingResolution !== 'normal',\n )\n if (state?.conflict && !hasSelectedConflictResolution) {\n return {\n ok: false,\n status: 409,\n code: 'record_lock_conflict',\n lock: state.lock ?? null,\n conflict: state.conflict,\n latestActionLogId: state.latestActionLogId ?? null,\n }\n }\n const hasResolvableConflict = Boolean(state?.conflict?.id && isUuid(state.conflict.id))\n const requestedResolution = state?.pendingResolution ?? 'normal'\n const resolution = requestedResolution !== 'normal' && !hasResolvableConflict\n ? 'normal'\n : requestedResolution\n const rawConflictId = resolution === 'normal' || !hasResolvableConflict\n ? undefined\n : (state?.pendingConflictId ?? state?.conflict?.id ?? undefined)\n const conflictId = isUuid(rawConflictId) ? rawConflictId : undefined\n const call = await apiCall<ValidateResponse>('/api/record_locks/validate', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind,\n resourceId,\n method: 'PUT',\n token: state?.lock?.token ?? undefined,\n baseLogId: state?.latestActionLogId ?? state?.lock?.baseActionLogId ?? undefined,\n conflictId,\n resolution,\n mutationPayload: data,\n }),\n })\n const payload = call.result ?? { ok: false }\n if (payload.ok) {\n const nextResolution = resolution === 'normal' ? 'normal' : resolution\n const preserveConflictUntilSuccessfulSave = nextResolution !== 'normal'\n setRecordLockFormState(formId, {\n resourceKind,\n resourceId,\n latestActionLogId: payload.latestActionLogId ?? state?.latestActionLogId ?? null,\n lock: resolveLockAfterValidate(state?.lock, payload.lock),\n conflict: preserveConflictUntilSuccessfulSave ? (state?.conflict ?? null) : null,\n pendingConflictId: nextResolution === 'normal' ? null : (conflictId ?? state?.pendingConflictId ?? null),\n pendingResolution: nextResolution,\n pendingResolutionArmed: nextResolution === 'normal' ? false : Boolean(state?.pendingResolutionArmed),\n })\n return payload\n }\n setRecordLockFormState(formId, {\n resourceKind,\n resourceId,\n lock: resolveLockAfterValidate(state?.lock, payload.lock),\n conflict: payload.conflict ?? state?.conflict ?? null,\n pendingConflictId: payload.conflict?.id ?? conflictId ?? state?.pendingConflictId ?? null,\n pendingResolution: resolution === 'normal' ? 'normal' : resolution,\n pendingResolutionArmed: resolution === 'normal' ? false : Boolean(state?.pendingResolutionArmed),\n })\n return payload\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["currentState"]
7
7
  }
@@ -1,11 +1,27 @@
1
- import RecordLockingWidget, { validateBeforeSave } from "./widget.client.js";
1
+ import RecordLockingWidget, { resolveLockAfterValidate, validateBeforeSave } from "./widget.client.js";
2
2
  import { getRecordLockFormState, setRecordLockFormState } from "@open-mercato/enterprise/modules/record_locks/lib/clientLockStore";
3
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
3
4
  function isUuid(value) {
4
5
  if (typeof value !== "string") return false;
5
6
  const trimmed = value.trim();
6
7
  if (!trimmed) return false;
7
8
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed);
8
9
  }
10
+ function isSameLockSession(leftLock, rightLock) {
11
+ return leftLock?.id === rightLock?.id && leftLock?.status === rightLock?.status && (leftLock?.token ?? null) === (rightLock?.token ?? null);
12
+ }
13
+ function clearSavedRecordLockState(formId) {
14
+ setRecordLockFormState(formId, {
15
+ recordDeleted: false,
16
+ acquired: false,
17
+ lock: null,
18
+ latestActionLogId: null,
19
+ conflict: null,
20
+ pendingConflictId: null,
21
+ pendingResolution: "normal",
22
+ pendingResolutionArmed: false
23
+ });
24
+ }
9
25
  const widget = {
10
26
  metadata: {
11
27
  id: "record_locks.injection.crud-form-locking",
@@ -81,7 +97,7 @@ const widget = {
81
97
  pendingConflictId: validation.conflict?.id ?? state2?.pendingConflictId ?? null,
82
98
  pendingResolution: "normal",
83
99
  pendingResolutionArmed: false,
84
- lock: validation.lock ?? state2?.lock ?? null,
100
+ lock: resolveLockAfterValidate(state2?.lock, validation.lock),
85
101
  latestActionLogId: validation.latestActionLogId ?? state2?.latestActionLogId ?? null
86
102
  });
87
103
  return {
@@ -122,15 +138,71 @@ const widget = {
122
138
  },
123
139
  async onAfterSave(_data, context) {
124
140
  if (!context.formId) return;
141
+ const state = getRecordLockFormState(context.formId);
142
+ if (!state?.resourceKind || !state.resourceId) {
143
+ clearSavedRecordLockState(context.formId);
144
+ return;
145
+ }
146
+ if (context.operation === "delete") {
147
+ if (state.lock?.token) {
148
+ const release = await apiCall("/api/record_locks/release", {
149
+ method: "POST",
150
+ headers: { "content-type": "application/json" },
151
+ body: JSON.stringify({
152
+ resourceKind: state.resourceKind,
153
+ resourceId: state.resourceId,
154
+ token: state.lock.token,
155
+ reason: "saved"
156
+ })
157
+ });
158
+ if (!release.ok) throw new Error("[internal] Failed to release the saved record lock");
159
+ }
160
+ const current2 = getRecordLockFormState(context.formId);
161
+ if (current2?.resourceKind === state.resourceKind && current2.resourceId === state.resourceId) {
162
+ clearSavedRecordLockState(context.formId);
163
+ }
164
+ return;
165
+ }
166
+ const acquire = await apiCall("/api/record_locks/acquire", {
167
+ method: "POST",
168
+ headers: { "content-type": "application/json" },
169
+ body: JSON.stringify({ resourceKind: state.resourceKind, resourceId: state.resourceId })
170
+ });
171
+ if (!acquire.ok || !acquire.result) throw new Error("[internal] Failed to refresh the saved record lock");
172
+ const payload = acquire.result;
173
+ const current = getRecordLockFormState(context.formId);
174
+ if (!current || current.recordDeleted === true || current.resourceKind !== state.resourceKind || current.resourceId !== state.resourceId) {
175
+ if (payload.lock?.token) {
176
+ const cleanup = await apiCall("/api/record_locks/release", {
177
+ method: "POST",
178
+ headers: { "content-type": "application/json" },
179
+ body: JSON.stringify({
180
+ resourceKind: state.resourceKind,
181
+ resourceId: state.resourceId,
182
+ token: payload.lock.token,
183
+ reason: "unmount"
184
+ })
185
+ });
186
+ if (!cleanup.ok) throw new Error("[internal] Failed to release a record lock after navigation");
187
+ }
188
+ return;
189
+ }
190
+ if (!isSameLockSession(current.lock, state.lock) && !isSameLockSession(current.lock, payload.lock)) return;
191
+ const hasIncomingConflict = Boolean(current.conflict && current.conflict !== state.conflict);
125
192
  setRecordLockFormState(context.formId, {
126
- recordDeleted: false,
127
- acquired: false,
128
- lock: null,
129
- latestActionLogId: null,
130
- conflict: null,
131
- pendingConflictId: null,
132
- pendingResolution: "normal",
133
- pendingResolutionArmed: false
193
+ acquired: payload.acquired ?? false,
194
+ lock: payload.lock ?? null,
195
+ currentUserId: payload.currentUserId ?? current.currentUserId ?? null,
196
+ heartbeatSeconds: payload.heartbeatSeconds ?? current.heartbeatSeconds ?? 15,
197
+ allowForceUnlock: payload.allowForceUnlock ?? current.allowForceUnlock ?? false,
198
+ ...hasIncomingConflict ? {} : {
199
+ recordDeleted: false,
200
+ latestActionLogId: payload.latestActionLogId ?? payload.lock?.baseActionLogId ?? null,
201
+ conflict: null,
202
+ pendingConflictId: null,
203
+ pendingResolution: "normal",
204
+ pendingResolutionArmed: false
205
+ }
134
206
  });
135
207
  },
136
208
  async onBeforeDelete(data, context) {
@@ -139,8 +211,8 @@ const widget = {
139
211
  async onDelete(_data, _context) {
140
212
  return;
141
213
  },
142
- async onAfterDelete(data, context) {
143
- await widget.eventHandlers?.onAfterSave?.(data, context);
214
+ async onAfterDelete(_data, context) {
215
+ if (context.formId) clearSavedRecordLockState(context.formId);
144
216
  },
145
217
  async onDeleteError(_data, _context, _error) {
146
218
  return;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/record_locks/widgets/injection/record-locking/widget.ts"],
4
- "sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport RecordLockingWidget, { validateBeforeSave } from './widget.client'\nimport { getRecordLockFormState, setRecordLockFormState } from '@open-mercato/enterprise/modules/record_locks/lib/clientLockStore'\n\nfunction isUuid(value: string | null | undefined): value is string {\n if (typeof value !== 'string') return false\n const trimmed = value.trim()\n if (!trimmed) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)\n}\n\ntype CrudInjectionContext = {\n formId?: string\n entityId?: string\n resourceKind?: string\n resourceId?: string\n recordId?: string\n path?: string\n query?: string\n kind?: string\n personId?: string\n companyId?: string\n dealId?: string\n retryLastMutation?: () => Promise<boolean | void> | boolean | void\n}\n\nconst widget: InjectionWidgetModule<CrudInjectionContext, Record<string, unknown>> = {\n metadata: {\n id: 'record_locks.injection.crud-form-locking',\n title: 'Record locking',\n description: 'Enterprise record lock status and conflict handling for CRUD forms.',\n features: ['record_locks.view'],\n priority: 400,\n enabled: true,\n },\n Widget: RecordLockingWidget,\n eventHandlers: {\n async onBeforeSave(data, context) {\n if (context.formId) {\n const currentState = getRecordLockFormState(context.formId)\n if (currentState?.recordDeleted) {\n return {\n ok: false,\n message: 'Record was deleted by another user',\n details: {\n code: 'record_deleted',\n resourceKind: currentState.resourceKind ?? null,\n resourceId: currentState.resourceId ?? null,\n },\n }\n }\n const hasSelectedConflictResolution = Boolean(\n currentState?.pendingResolutionArmed === true\n && currentState.pendingResolution\n && currentState.pendingResolution !== 'normal',\n )\n if (currentState?.conflict && !hasSelectedConflictResolution) {\n return {\n ok: false,\n message: 'Record conflict detected',\n details: {\n code: 'record_lock_conflict',\n lock: currentState.lock ?? null,\n conflict: currentState.conflict,\n latestActionLogId: currentState.latestActionLogId ?? null,\n },\n }\n }\n }\n const validation = await validateBeforeSave(data ?? {}, context)\n if (!context.formId) {\n if (!validation.ok) {\n return {\n ok: false,\n message: 'Record conflict detected',\n details: {\n code: 'record_lock_conflict',\n lock: validation.lock ?? null,\n conflict: validation.conflict ?? null,\n latestActionLogId: validation.latestActionLogId ?? null,\n },\n }\n }\n if (validation.lock?.resourceKind && validation.lock?.resourceId) {\n return {\n ok: true,\n requestHeaders: {\n 'x-om-record-lock-kind': validation.lock.resourceKind,\n 'x-om-record-lock-resource-id': validation.lock.resourceId,\n ...(validation.lock?.token ? { 'x-om-record-lock-token': validation.lock.token } : {}),\n ...(validation.latestActionLogId ? { 'x-om-record-lock-base-log-id': validation.latestActionLogId } : {}),\n },\n }\n }\n return { ok: true }\n }\n if (!validation.ok) {\n const state = getRecordLockFormState(context.formId)\n setRecordLockFormState(context.formId, {\n conflict: validation.conflict ?? state?.conflict ?? null,\n pendingConflictId: validation.conflict?.id ?? state?.pendingConflictId ?? null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n lock: validation.lock ?? state?.lock ?? null,\n latestActionLogId: validation.latestActionLogId ?? state?.latestActionLogId ?? null,\n })\n return {\n ok: false,\n message: 'Record conflict detected',\n details: {\n code: 'record_lock_conflict',\n lock: validation.lock ?? null,\n conflict: validation.conflict ?? null,\n latestActionLogId: validation.latestActionLogId ?? null,\n },\n }\n }\n const state = getRecordLockFormState(context.formId)\n if (!state?.resourceKind || !state?.resourceId) return { ok: true }\n const hasResolvableConflict = Boolean(state.conflict?.id && isUuid(state.conflict.id))\n const shouldSendResolution = hasResolvableConflict\n && state.pendingResolutionArmed === true\n && Boolean(state.pendingResolution && state.pendingResolution !== 'normal')\n const shouldSendConflictId = hasResolvableConflict\n const rawConflictId = shouldSendConflictId\n ? (state.pendingConflictId ?? state.conflict?.id ?? undefined)\n : undefined\n const conflictIdHeader = isUuid(rawConflictId) ? rawConflictId : undefined\n if (shouldSendResolution) {\n setRecordLockFormState(context.formId, {\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n }\n return {\n ok: true,\n requestHeaders: {\n 'x-om-record-lock-kind': state.resourceKind,\n 'x-om-record-lock-resource-id': state.resourceId,\n ...(state.lock?.token ? { 'x-om-record-lock-token': state.lock.token } : {}),\n ...(state.latestActionLogId ? { 'x-om-record-lock-base-log-id': state.latestActionLogId } : {}),\n ...(shouldSendResolution\n ? { 'x-om-record-lock-resolution': state.pendingResolution }\n : {}),\n ...(conflictIdHeader ? { 'x-om-record-lock-conflict-id': conflictIdHeader } : {}),\n },\n }\n },\n async onAfterSave(_data, context) {\n if (!context.formId) return\n setRecordLockFormState(context.formId, {\n recordDeleted: false,\n acquired: false,\n lock: null,\n latestActionLogId: null,\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n },\n async onBeforeDelete(data, context) {\n return (await widget.eventHandlers?.onBeforeSave?.(data, context)) ?? { ok: true }\n },\n async onDelete(_data, _context) {\n return\n },\n async onAfterDelete(data, context) {\n await widget.eventHandlers?.onAfterSave?.(data, context)\n },\n async onDeleteError(_data, _context, _error) {\n return\n },\n },\n}\n\nexport default widget\n"],
5
- "mappings": "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",
6
- "names": ["state"]
4
+ "sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport RecordLockingWidget, { resolveLockAfterValidate, validateBeforeSave } from './widget.client'\nimport { getRecordLockFormState, setRecordLockFormState, type RecordLockFormState } from '@open-mercato/enterprise/modules/record_locks/lib/clientLockStore'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\n\nfunction isUuid(value: string | null | undefined): value is string {\n if (typeof value !== 'string') return false\n const trimmed = value.trim()\n if (!trimmed) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)\n}\n\ntype CrudInjectionContext = {\n formId?: string\n entityId?: string\n resourceKind?: string\n resourceId?: string\n recordId?: string\n path?: string\n query?: string\n kind?: string\n operation?: 'create' | 'update' | 'delete'\n personId?: string\n companyId?: string\n dealId?: string\n retryLastMutation?: () => Promise<boolean | void> | boolean | void\n}\n\ntype RecordLockAcquireResponse = Pick<\n RecordLockFormState,\n 'acquired' | 'allowForceUnlock' | 'currentUserId' | 'heartbeatSeconds' | 'latestActionLogId' | 'lock'\n>\n\nfunction isSameLockSession(leftLock: RecordLockFormState['lock'], rightLock: RecordLockFormState['lock']): boolean {\n return leftLock?.id === rightLock?.id\n && leftLock?.status === rightLock?.status\n && (leftLock?.token ?? null) === (rightLock?.token ?? null)\n}\n\nfunction clearSavedRecordLockState(formId: string) {\n setRecordLockFormState(formId, {\n recordDeleted: false,\n acquired: false,\n lock: null,\n latestActionLogId: null,\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n}\n\nconst widget: InjectionWidgetModule<CrudInjectionContext, Record<string, unknown>> = {\n metadata: {\n id: 'record_locks.injection.crud-form-locking',\n title: 'Record locking',\n description: 'Enterprise record lock status and conflict handling for CRUD forms.',\n features: ['record_locks.view'],\n priority: 400,\n enabled: true,\n },\n Widget: RecordLockingWidget,\n eventHandlers: {\n async onBeforeSave(data, context) {\n if (context.formId) {\n const currentState = getRecordLockFormState(context.formId)\n if (currentState?.recordDeleted) {\n return {\n ok: false,\n message: 'Record was deleted by another user',\n details: {\n code: 'record_deleted',\n resourceKind: currentState.resourceKind ?? null,\n resourceId: currentState.resourceId ?? null,\n },\n }\n }\n const hasSelectedConflictResolution = Boolean(\n currentState?.pendingResolutionArmed === true\n && currentState.pendingResolution\n && currentState.pendingResolution !== 'normal',\n )\n if (currentState?.conflict && !hasSelectedConflictResolution) {\n return {\n ok: false,\n message: 'Record conflict detected',\n details: {\n code: 'record_lock_conflict',\n lock: currentState.lock ?? null,\n conflict: currentState.conflict,\n latestActionLogId: currentState.latestActionLogId ?? null,\n },\n }\n }\n }\n const validation = await validateBeforeSave(data ?? {}, context)\n if (!context.formId) {\n if (!validation.ok) {\n return {\n ok: false,\n message: 'Record conflict detected',\n details: {\n code: 'record_lock_conflict',\n lock: validation.lock ?? null,\n conflict: validation.conflict ?? null,\n latestActionLogId: validation.latestActionLogId ?? null,\n },\n }\n }\n if (validation.lock?.resourceKind && validation.lock?.resourceId) {\n return {\n ok: true,\n requestHeaders: {\n 'x-om-record-lock-kind': validation.lock.resourceKind,\n 'x-om-record-lock-resource-id': validation.lock.resourceId,\n ...(validation.lock?.token ? { 'x-om-record-lock-token': validation.lock.token } : {}),\n ...(validation.latestActionLogId ? { 'x-om-record-lock-base-log-id': validation.latestActionLogId } : {}),\n },\n }\n }\n return { ok: true }\n }\n if (!validation.ok) {\n const state = getRecordLockFormState(context.formId)\n setRecordLockFormState(context.formId, {\n conflict: validation.conflict ?? state?.conflict ?? null,\n pendingConflictId: validation.conflict?.id ?? state?.pendingConflictId ?? null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n lock: resolveLockAfterValidate(state?.lock, validation.lock),\n latestActionLogId: validation.latestActionLogId ?? state?.latestActionLogId ?? null,\n })\n return {\n ok: false,\n message: 'Record conflict detected',\n details: {\n code: 'record_lock_conflict',\n lock: validation.lock ?? null,\n conflict: validation.conflict ?? null,\n latestActionLogId: validation.latestActionLogId ?? null,\n },\n }\n }\n const state = getRecordLockFormState(context.formId)\n if (!state?.resourceKind || !state?.resourceId) return { ok: true }\n const hasResolvableConflict = Boolean(state.conflict?.id && isUuid(state.conflict.id))\n const shouldSendResolution = hasResolvableConflict\n && state.pendingResolutionArmed === true\n && Boolean(state.pendingResolution && state.pendingResolution !== 'normal')\n const shouldSendConflictId = hasResolvableConflict\n const rawConflictId = shouldSendConflictId\n ? (state.pendingConflictId ?? state.conflict?.id ?? undefined)\n : undefined\n const conflictIdHeader = isUuid(rawConflictId) ? rawConflictId : undefined\n if (shouldSendResolution) {\n setRecordLockFormState(context.formId, {\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n }\n return {\n ok: true,\n requestHeaders: {\n 'x-om-record-lock-kind': state.resourceKind,\n 'x-om-record-lock-resource-id': state.resourceId,\n ...(state.lock?.token ? { 'x-om-record-lock-token': state.lock.token } : {}),\n ...(state.latestActionLogId ? { 'x-om-record-lock-base-log-id': state.latestActionLogId } : {}),\n ...(shouldSendResolution\n ? { 'x-om-record-lock-resolution': state.pendingResolution }\n : {}),\n ...(conflictIdHeader ? { 'x-om-record-lock-conflict-id': conflictIdHeader } : {}),\n },\n }\n },\n async onAfterSave(_data, context) {\n if (!context.formId) return\n const state = getRecordLockFormState(context.formId)\n if (!state?.resourceKind || !state.resourceId) {\n clearSavedRecordLockState(context.formId)\n return\n }\n if (context.operation === 'delete') {\n if (state.lock?.token) {\n const release = await apiCall('/api/record_locks/release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: state.lock.token,\n reason: 'saved',\n }),\n })\n if (!release.ok) throw new Error('[internal] Failed to release the saved record lock')\n }\n const current = getRecordLockFormState(context.formId)\n if (current?.resourceKind === state.resourceKind && current.resourceId === state.resourceId) {\n clearSavedRecordLockState(context.formId)\n }\n return\n }\n const acquire = await apiCall<RecordLockAcquireResponse>('/api/record_locks/acquire', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ resourceKind: state.resourceKind, resourceId: state.resourceId }),\n })\n if (!acquire.ok || !acquire.result) throw new Error('[internal] Failed to refresh the saved record lock')\n const payload = acquire.result\n const current = getRecordLockFormState(context.formId)\n if (\n !current\n || current.recordDeleted === true\n || current.resourceKind !== state.resourceKind\n || current.resourceId !== state.resourceId\n ) {\n if (payload.lock?.token) {\n const cleanup = await apiCall('/api/record_locks/release', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n resourceKind: state.resourceKind,\n resourceId: state.resourceId,\n token: payload.lock.token,\n reason: 'unmount',\n }),\n })\n if (!cleanup.ok) throw new Error('[internal] Failed to release a record lock after navigation')\n }\n return\n }\n if (!isSameLockSession(current.lock, state.lock) && !isSameLockSession(current.lock, payload.lock)) return\n const hasIncomingConflict = Boolean(current.conflict && current.conflict !== state.conflict)\n setRecordLockFormState(context.formId, {\n acquired: payload.acquired ?? false,\n lock: payload.lock ?? null,\n currentUserId: payload.currentUserId ?? current.currentUserId ?? null,\n heartbeatSeconds: payload.heartbeatSeconds ?? current.heartbeatSeconds ?? 15,\n allowForceUnlock: payload.allowForceUnlock ?? current.allowForceUnlock ?? false,\n ...(hasIncomingConflict ? {} : {\n recordDeleted: false,\n latestActionLogId: payload.latestActionLogId ?? payload.lock?.baseActionLogId ?? null,\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n }),\n })\n },\n async onBeforeDelete(data, context) {\n return (await widget.eventHandlers?.onBeforeSave?.(data, context)) ?? { ok: true }\n },\n async onDelete(_data, _context) {\n return\n },\n async onAfterDelete(_data, context) {\n if (context.formId) clearSavedRecordLockState(context.formId)\n },\n async onDeleteError(_data, _context, _error) {\n return\n },\n },\n}\n\nexport default widget\n"],
5
+ "mappings": "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",
6
+ "names": ["state", "current"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/enterprise",
3
- "version": "0.10.1-develop.7628.1.db711688d7",
3
+ "version": "0.10.1-develop.7635.1.03de276351",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -70,12 +70,12 @@
70
70
  "@dnd-kit/core": "^6.3.1",
71
71
  "@dnd-kit/sortable": "^10.0.0",
72
72
  "@dnd-kit/utilities": "^3.2.2",
73
- "@open-mercato/ai-assistant": "0.10.1-develop.7628.1.db711688d7",
74
- "@open-mercato/core": "0.10.1-develop.7628.1.db711688d7",
75
- "@open-mercato/queue": "0.10.1-develop.7628.1.db711688d7",
76
- "@open-mercato/scheduler": "0.10.1-develop.7628.1.db711688d7",
77
- "@open-mercato/ui": "0.10.1-develop.7628.1.db711688d7",
78
- "@open-mercato/web-research": "0.10.1-develop.7628.1.db711688d7",
73
+ "@open-mercato/ai-assistant": "0.10.1-develop.7635.1.03de276351",
74
+ "@open-mercato/core": "0.10.1-develop.7635.1.03de276351",
75
+ "@open-mercato/queue": "0.10.1-develop.7635.1.03de276351",
76
+ "@open-mercato/scheduler": "0.10.1-develop.7635.1.03de276351",
77
+ "@open-mercato/ui": "0.10.1-develop.7635.1.03de276351",
78
+ "@open-mercato/web-research": "0.10.1-develop.7635.1.03de276351",
79
79
  "@simplewebauthn/browser": "^13.3.0",
80
80
  "@simplewebauthn/server": "^13.3.2",
81
81
  "@simplewebauthn/types": "^12.0.0",
@@ -87,13 +87,13 @@
87
87
  },
88
88
  "peerDependencies": {
89
89
  "@mikro-orm/core": "^7.0.14",
90
- "@open-mercato/shared": "0.10.1-develop.7628.1.db711688d7",
90
+ "@open-mercato/shared": "0.10.1-develop.7635.1.03de276351",
91
91
  "react": "^19.0.0",
92
92
  "react-dom": "^19.0.0",
93
93
  "zod": ">=3.23.0"
94
94
  },
95
95
  "devDependencies": {
96
- "@open-mercato/shared": "0.10.1-develop.7628.1.db711688d7",
96
+ "@open-mercato/shared": "0.10.1-develop.7635.1.03de276351",
97
97
  "@types/jest": "^30.0.0",
98
98
  "@types/react": "^19.2.18",
99
99
  "@types/react-dom": "^19.2.5",
@@ -56,6 +56,7 @@ type CrudInjectionContext = {
56
56
  type RecordLockWidgetOwner = {
57
57
  instanceId: string
58
58
  priority: number
59
+ formId: string
59
60
  }
60
61
 
61
62
  const GLOBAL_RECORD_LOCK_OWNERS_KEY = '__openMercatoRecordLockWidgetOwners__'
@@ -622,6 +623,11 @@ export default function RecordLockingWidget({
622
623
  token: string
623
624
  } | null>(null)
624
625
  const keepMineRetryVersionRef = React.useRef(0)
626
+ const hasAnotherOwnerForForm = React.useCallback(() => {
627
+ if (!ownerKey) return false
628
+ const owner = getRecordLockOwnerMap().get(ownerKey)
629
+ return Boolean(owner && owner.instanceId !== instanceId && owner.formId === formId)
630
+ }, [formId, instanceId, ownerKey])
625
631
 
626
632
  React.useEffect(() => {
627
633
  if (!ownerKey) {
@@ -642,17 +648,18 @@ export default function RecordLockingWidget({
642
648
  const claimOwnership = () => {
643
649
  const current = owners.get(ownerKey)
644
650
  if (!current) {
645
- owners.set(ownerKey, { instanceId, priority: ownerPriority })
651
+ owners.set(ownerKey, { instanceId, priority: ownerPriority, formId })
646
652
  setIsPrimaryInstance(true)
647
653
  notifyOwnersChanged()
648
654
  return
649
655
  }
650
656
  if (current.instanceId === instanceId) {
657
+ owners.set(ownerKey, { instanceId, priority: ownerPriority, formId })
651
658
  setIsPrimaryInstance(true)
652
659
  return
653
660
  }
654
661
  if (ownerPriority > current.priority) {
655
- owners.set(ownerKey, { instanceId, priority: ownerPriority })
662
+ owners.set(ownerKey, { instanceId, priority: ownerPriority, formId })
656
663
  setIsPrimaryInstance(true)
657
664
  notifyOwnersChanged()
658
665
  return
@@ -676,10 +683,11 @@ export default function RecordLockingWidget({
676
683
  notifyOwnersChanged()
677
684
  }
678
685
  }
679
- }, [instanceId, ownerKey, ownerPriority])
686
+ }, [formId, instanceId, ownerKey, ownerPriority])
680
687
 
681
688
  React.useEffect(() => {
682
689
  if (isPrimaryInstance) return
690
+ if (hasAnotherOwnerForForm()) return
683
691
  const current = getRecordLockFormState(formId)
684
692
  if (!current?.lock?.token || !current.resourceKind || !current.resourceId) {
685
693
  clearRecordLockFormState(formId)
@@ -694,7 +702,7 @@ export default function RecordLockingWidget({
694
702
  logger.warn('Failed to release lock while demoting owner', { err: error })
695
703
  })
696
704
  clearRecordLockFormState(formId)
697
- }, [formId, isPrimaryInstance])
705
+ }, [formId, hasAnotherOwnerForForm, isPrimaryInstance])
698
706
 
699
707
  React.useEffect(() => {
700
708
  setMounted(true)
@@ -740,13 +748,25 @@ export default function RecordLockingWidget({
740
748
  if (!resourceKind || !resourceId) return
741
749
  let active = true
742
750
  const acquire = async () => {
751
+ const requestedState = getRecordLockFormState(formId)
743
752
  const call = await apiCall<AcquireResponse>('/api/record_locks/acquire', {
744
753
  method: 'POST',
745
754
  headers: { 'content-type': 'application/json' },
746
755
  body: JSON.stringify({ resourceKind, resourceId }),
747
756
  })
748
757
  const payload = call.result ?? {}
749
- if (!active) return
758
+ const currentState = getRecordLockFormState(formId)
759
+ if (
760
+ !active
761
+ || !currentState
762
+ || currentState.recordDeleted === true
763
+ || currentState.resourceKind !== resourceKind
764
+ || currentState.resourceId !== resourceId
765
+ || (currentState.lock?.token ?? null) !== (requestedState?.lock?.token ?? null)
766
+ ) return
767
+ const latestActionLogId = currentState.latestActionLogId !== undefined
768
+ ? currentState.latestActionLogId
769
+ : (payload.latestActionLogId ?? null)
750
770
  if (!call.ok) {
751
771
  const defaultMessage = call.status === 403
752
772
  ? t('api.errors.forbidden', 'Forbidden')
@@ -763,7 +783,7 @@ export default function RecordLockingWidget({
763
783
  lock: payload.lock ?? null,
764
784
  currentUserId: payload.currentUserId ?? null,
765
785
  heartbeatSeconds: payload.heartbeatSeconds ?? 15,
766
- latestActionLogId: payload.latestActionLogId ?? null,
786
+ latestActionLogId,
767
787
  allowForceUnlock: payload.allowForceUnlock ?? false,
768
788
  })
769
789
  return
@@ -776,7 +796,7 @@ export default function RecordLockingWidget({
776
796
  lock: payload.lock ?? null,
777
797
  currentUserId: payload.currentUserId ?? null,
778
798
  heartbeatSeconds: payload.heartbeatSeconds ?? 15,
779
- latestActionLogId: payload.latestActionLogId ?? null,
799
+ latestActionLogId,
780
800
  allowForceUnlock: payload.allowForceUnlock ?? false,
781
801
  })
782
802
  }
@@ -893,7 +913,17 @@ export default function RecordLockingWidget({
893
913
  )
894
914
  if (hasUnresolvedConflict) return
895
915
  if (!state?.resourceKind || !state?.resourceId) return
916
+ if (state.recordDeleted === true) return
917
+ let active = true
896
918
  const refreshPresence = async () => {
919
+ const requestedState = getRecordLockFormState(formId)
920
+ if (
921
+ !active
922
+ || !requestedState
923
+ || requestedState.recordDeleted === true
924
+ || requestedState.resourceKind !== state.resourceKind
925
+ || requestedState.resourceId !== state.resourceId
926
+ ) return
897
927
  const call = await apiCall<AcquireResponse>('/api/record_locks/acquire', {
898
928
  method: 'POST',
899
929
  headers: { 'content-type': 'application/json' },
@@ -903,8 +933,19 @@ export default function RecordLockingWidget({
903
933
  }),
904
934
  })
905
935
  const payload = call.result ?? {}
936
+ const currentState = getRecordLockFormState(formId)
937
+ if (
938
+ !active
939
+ || !currentState
940
+ || currentState.recordDeleted === true
941
+ || currentState.resourceKind !== requestedState.resourceKind
942
+ || currentState.resourceId !== requestedState.resourceId
943
+ || (currentState.lock?.token ?? null) !== (requestedState.lock?.token ?? null)
944
+ ) return
945
+ const nextLatestActionLogId = currentState.latestActionLogId !== undefined
946
+ ? currentState.latestActionLogId
947
+ : (payload.latestActionLogId ?? null)
906
948
  if (!call.ok) {
907
- const currentState = getRecordLockFormState(formId)
908
949
  setRecordLockFormState(formId, {
909
950
  resourceKind: state.resourceKind,
910
951
  resourceId: state.resourceId,
@@ -912,19 +953,11 @@ export default function RecordLockingWidget({
912
953
  lock: payload.lock ?? null,
913
954
  currentUserId: payload.currentUserId ?? currentState?.currentUserId ?? null,
914
955
  heartbeatSeconds: payload.heartbeatSeconds ?? currentState?.heartbeatSeconds ?? 15,
915
- latestActionLogId: payload.latestActionLogId ?? currentState?.latestActionLogId ?? null,
956
+ latestActionLogId: nextLatestActionLogId,
916
957
  allowForceUnlock: payload.allowForceUnlock ?? false,
917
958
  })
918
959
  return
919
960
  }
920
- const currentState = getRecordLockFormState(formId)
921
- const previousToken = currentState?.lock?.token ?? null
922
- const nextToken = payload.lock?.token ?? null
923
- const isSameSession = Boolean(previousToken && nextToken && previousToken === nextToken)
924
- const nextLatestActionLogId = isSameSession
925
- ? (currentState?.latestActionLogId ?? null)
926
- : (payload.latestActionLogId ?? currentState?.latestActionLogId ?? null)
927
-
928
961
  setRecordLockFormState(formId, {
929
962
  resourceKind: state.resourceKind,
930
963
  resourceId: state.resourceId,
@@ -984,6 +1017,7 @@ export default function RecordLockingWidget({
984
1017
  void refreshPresence()
985
1018
 
986
1019
  return () => {
1020
+ active = false
987
1021
  window.removeEventListener(RECORD_LOCKS_INCOMING_CHANGES_EVENT, onIncomingChanges)
988
1022
  window.removeEventListener(RECORD_LOCKS_FORCE_RELEASED_EVENT, onForceReleased)
989
1023
  window.removeEventListener('om:event', onBridgeReconnected)
@@ -997,6 +1031,7 @@ export default function RecordLockingWidget({
997
1031
  state?.conflict,
998
1032
  state?.pendingResolution,
999
1033
  state?.pendingResolutionArmed,
1034
+ state?.recordDeleted,
1000
1035
  state?.resourceId,
1001
1036
  state?.resourceKind,
1002
1037
  ])
@@ -1053,18 +1088,20 @@ export default function RecordLockingWidget({
1053
1088
  }, [isPrimaryInstance])
1054
1089
 
1055
1090
  React.useEffect(() => {
1056
- if (!isPrimaryInstance) return
1057
1091
  return () => {
1058
- const payload = releasePayloadRef.current
1059
- if (payload) {
1092
+ if (hasAnotherOwnerForForm()) return
1093
+ const current = getRecordLockFormState(formId)
1094
+ if (current?.lock?.token && current.resourceKind && current.resourceId) {
1060
1095
  void releaseLock({
1061
- ...payload,
1096
+ resourceKind: current.resourceKind,
1097
+ resourceId: current.resourceId,
1098
+ token: current.lock.token,
1062
1099
  reason: 'unmount',
1063
1100
  })
1064
1101
  }
1065
1102
  clearRecordLockFormState(formId)
1066
1103
  }
1067
- }, [formId, isPrimaryInstance])
1104
+ }, [formId, hasAnotherOwnerForForm])
1068
1105
 
1069
1106
  React.useEffect(() => {
1070
1107
  if (!isPrimaryInstance) return
@@ -1082,7 +1119,7 @@ export default function RecordLockingWidget({
1082
1119
  pendingResolutionArmed: false,
1083
1120
  }
1084
1121
  if (payload.lock !== undefined) {
1085
- nextPatch.lock = payload.lock
1122
+ nextPatch.lock = resolveLockAfterValidate(getRecordLockFormState(formId)?.lock, payload.lock)
1086
1123
  }
1087
1124
  if (payload.latestActionLogId !== undefined) {
1088
1125
  nextPatch.latestActionLogId = payload.latestActionLogId
@@ -1,6 +1,7 @@
1
1
  import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
2
- import RecordLockingWidget, { validateBeforeSave } from './widget.client'
3
- import { getRecordLockFormState, setRecordLockFormState } from '@open-mercato/enterprise/modules/record_locks/lib/clientLockStore'
2
+ import RecordLockingWidget, { resolveLockAfterValidate, validateBeforeSave } from './widget.client'
3
+ import { getRecordLockFormState, setRecordLockFormState, type RecordLockFormState } from '@open-mercato/enterprise/modules/record_locks/lib/clientLockStore'
4
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
4
5
 
5
6
  function isUuid(value: string | null | undefined): value is string {
6
7
  if (typeof value !== 'string') return false
@@ -18,12 +19,37 @@ type CrudInjectionContext = {
18
19
  path?: string
19
20
  query?: string
20
21
  kind?: string
22
+ operation?: 'create' | 'update' | 'delete'
21
23
  personId?: string
22
24
  companyId?: string
23
25
  dealId?: string
24
26
  retryLastMutation?: () => Promise<boolean | void> | boolean | void
25
27
  }
26
28
 
29
+ type RecordLockAcquireResponse = Pick<
30
+ RecordLockFormState,
31
+ 'acquired' | 'allowForceUnlock' | 'currentUserId' | 'heartbeatSeconds' | 'latestActionLogId' | 'lock'
32
+ >
33
+
34
+ function isSameLockSession(leftLock: RecordLockFormState['lock'], rightLock: RecordLockFormState['lock']): boolean {
35
+ return leftLock?.id === rightLock?.id
36
+ && leftLock?.status === rightLock?.status
37
+ && (leftLock?.token ?? null) === (rightLock?.token ?? null)
38
+ }
39
+
40
+ function clearSavedRecordLockState(formId: string) {
41
+ setRecordLockFormState(formId, {
42
+ recordDeleted: false,
43
+ acquired: false,
44
+ lock: null,
45
+ latestActionLogId: null,
46
+ conflict: null,
47
+ pendingConflictId: null,
48
+ pendingResolution: 'normal',
49
+ pendingResolutionArmed: false,
50
+ })
51
+ }
52
+
27
53
  const widget: InjectionWidgetModule<CrudInjectionContext, Record<string, unknown>> = {
28
54
  metadata: {
29
55
  id: 'record_locks.injection.crud-form-locking',
@@ -101,7 +127,7 @@ const widget: InjectionWidgetModule<CrudInjectionContext, Record<string, unknown
101
127
  pendingConflictId: validation.conflict?.id ?? state?.pendingConflictId ?? null,
102
128
  pendingResolution: 'normal',
103
129
  pendingResolutionArmed: false,
104
- lock: validation.lock ?? state?.lock ?? null,
130
+ lock: resolveLockAfterValidate(state?.lock, validation.lock),
105
131
  latestActionLogId: validation.latestActionLogId ?? state?.latestActionLogId ?? null,
106
132
  })
107
133
  return {
@@ -148,15 +174,76 @@ const widget: InjectionWidgetModule<CrudInjectionContext, Record<string, unknown
148
174
  },
149
175
  async onAfterSave(_data, context) {
150
176
  if (!context.formId) return
177
+ const state = getRecordLockFormState(context.formId)
178
+ if (!state?.resourceKind || !state.resourceId) {
179
+ clearSavedRecordLockState(context.formId)
180
+ return
181
+ }
182
+ if (context.operation === 'delete') {
183
+ if (state.lock?.token) {
184
+ const release = await apiCall('/api/record_locks/release', {
185
+ method: 'POST',
186
+ headers: { 'content-type': 'application/json' },
187
+ body: JSON.stringify({
188
+ resourceKind: state.resourceKind,
189
+ resourceId: state.resourceId,
190
+ token: state.lock.token,
191
+ reason: 'saved',
192
+ }),
193
+ })
194
+ if (!release.ok) throw new Error('[internal] Failed to release the saved record lock')
195
+ }
196
+ const current = getRecordLockFormState(context.formId)
197
+ if (current?.resourceKind === state.resourceKind && current.resourceId === state.resourceId) {
198
+ clearSavedRecordLockState(context.formId)
199
+ }
200
+ return
201
+ }
202
+ const acquire = await apiCall<RecordLockAcquireResponse>('/api/record_locks/acquire', {
203
+ method: 'POST',
204
+ headers: { 'content-type': 'application/json' },
205
+ body: JSON.stringify({ resourceKind: state.resourceKind, resourceId: state.resourceId }),
206
+ })
207
+ if (!acquire.ok || !acquire.result) throw new Error('[internal] Failed to refresh the saved record lock')
208
+ const payload = acquire.result
209
+ const current = getRecordLockFormState(context.formId)
210
+ if (
211
+ !current
212
+ || current.recordDeleted === true
213
+ || current.resourceKind !== state.resourceKind
214
+ || current.resourceId !== state.resourceId
215
+ ) {
216
+ if (payload.lock?.token) {
217
+ const cleanup = await apiCall('/api/record_locks/release', {
218
+ method: 'POST',
219
+ headers: { 'content-type': 'application/json' },
220
+ body: JSON.stringify({
221
+ resourceKind: state.resourceKind,
222
+ resourceId: state.resourceId,
223
+ token: payload.lock.token,
224
+ reason: 'unmount',
225
+ }),
226
+ })
227
+ if (!cleanup.ok) throw new Error('[internal] Failed to release a record lock after navigation')
228
+ }
229
+ return
230
+ }
231
+ if (!isSameLockSession(current.lock, state.lock) && !isSameLockSession(current.lock, payload.lock)) return
232
+ const hasIncomingConflict = Boolean(current.conflict && current.conflict !== state.conflict)
151
233
  setRecordLockFormState(context.formId, {
152
- recordDeleted: false,
153
- acquired: false,
154
- lock: null,
155
- latestActionLogId: null,
156
- conflict: null,
157
- pendingConflictId: null,
158
- pendingResolution: 'normal',
159
- pendingResolutionArmed: false,
234
+ acquired: payload.acquired ?? false,
235
+ lock: payload.lock ?? null,
236
+ currentUserId: payload.currentUserId ?? current.currentUserId ?? null,
237
+ heartbeatSeconds: payload.heartbeatSeconds ?? current.heartbeatSeconds ?? 15,
238
+ allowForceUnlock: payload.allowForceUnlock ?? current.allowForceUnlock ?? false,
239
+ ...(hasIncomingConflict ? {} : {
240
+ recordDeleted: false,
241
+ latestActionLogId: payload.latestActionLogId ?? payload.lock?.baseActionLogId ?? null,
242
+ conflict: null,
243
+ pendingConflictId: null,
244
+ pendingResolution: 'normal',
245
+ pendingResolutionArmed: false,
246
+ }),
160
247
  })
161
248
  },
162
249
  async onBeforeDelete(data, context) {
@@ -165,8 +252,8 @@ const widget: InjectionWidgetModule<CrudInjectionContext, Record<string, unknown
165
252
  async onDelete(_data, _context) {
166
253
  return
167
254
  },
168
- async onAfterDelete(data, context) {
169
- await widget.eventHandlers?.onAfterSave?.(data, context)
255
+ async onAfterDelete(_data, context) {
256
+ if (context.formId) clearSavedRecordLockState(context.formId)
170
257
  },
171
258
  async onDeleteError(_data, _context, _error) {
172
259
  return