@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js +37 -22
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js.map +2 -2
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.js +84 -12
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.js.map +3 -3
- package/package.json +9 -9
- package/src/modules/record_locks/widgets/injection/record-locking/widget.client.tsx +60 -23
- package/src/modules/record_locks/widgets/injection/record-locking/widget.ts +100 -13
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:enterprise] found
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ??
|
|
615
|
-
heartbeatSeconds: payload.heartbeatSeconds ??
|
|
616
|
-
latestActionLogId:
|
|
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
|
-
|
|
740
|
-
|
|
751
|
+
if (hasAnotherOwnerForForm()) return;
|
|
752
|
+
const current = getRecordLockFormState(formId);
|
|
753
|
+
if (current?.lock?.token && current.resourceKind && current.resourceId) {
|
|
741
754
|
void releaseLock({
|
|
742
|
-
|
|
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,
|
|
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": ";AAs2CU,SAiDI,UAjDJ,KAiDI,YAjDJ;AAp2CV,YAAY,WAAW;AACvB,SAAS,oBAAoB;AAC7B,SAAS,SAAS,sBAAsB;AACxC,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,cAAc,mBAAmB;AACjE,SAAS,OAAO,wBAAwB;AACxC,SAAS,YAAY;AAErB,SAAS,oCAAoC;AAC7C,SAAS,yCAAyC;AAClD,SAAS,uBAAuB;AAChC,SAAS,YAAY;AACrB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,QAAQ,mBAAmB,yBAAyB;AAC7D,SAAS,qCAAqC;AAC9C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,cAAc,EAAE,MAAM,EAAE,WAAW,wBAAwB,CAAC;AAuBxF,MAAM,gCAAgC;AAEtC,SAAS,wBAA4D;AACnE,QAAM,QAAQ;AACd,QAAM,WAAW,MAAM,6BAA6B;AACpD,MAAI,oBAAoB,IAAK,QAAO;AACpC,QAAM,OAAO,oBAAI,IAAmC;AACpD,QAAM,6BAA6B,IAAI;AACvC,SAAO;AACT;AAgDA,SAAS,eAAe,OAAkD;AACxE,SAAO,QAAQ,KAAK,KAAK,OAAO,UAAU;AAC5C;AAEA,SAAS,iBAAiB,OAA+B;AACvD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAEA,SAAS,mBAAmB,OAA+B;AACzD,QAAM,QAAmB,CAAC,KAAK;AAC/B,QAAM,UAAU,oBAAI,IAAa;AAEjC,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,UAAU,MAAM,MAAM;AAC5B,QAAI,CAAC,WAAW,QAAQ,IAAI,OAAO,EAAG;AACtC,YAAQ,IAAI,OAAO;AACnB,QAAI,CAAC,eAAe,OAAO,EAAG;AAE9B,UAAM,SAAS,QAAQ;AACvB,QAAI,OAAO,WAAW,YAAY,OAAO,SAAS,MAAM,EAAG,QAAO;AAClE,QAAI,OAAO,WAAW,UAAU;AAC9B,YAAM,SAAS,OAAO,MAAM;AAC5B,UAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AAAA,IACtC;AAEA,UAAM,aAAa,QAAQ;AAC3B,QAAI,OAAO,eAAe,YAAY,OAAO,SAAS,UAAU,EAAG,QAAO;AAC1E,QAAI,OAAO,eAAe,UAAU;AAClC,YAAM,SAAS,OAAO,UAAU;AAChC,UAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AAAA,IACtC;AAEA,UAAM,SAAS,CAAC,QAAQ,YAAY,QAAQ,WAAW,SAAS,OAAO;AACvE,eAAW,OAAO,QAAQ;AACxB,YAAM,OAAO,QAAQ,GAAG;AACxB,UAAI,QAAQ,CAAC,QAAQ,IAAI,IAAI,EAAG,OAAM,KAAK,IAAI;AAAA,IACjD;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,iCAAiC,OAIjC;AACP,QAAM,QAAmB,CAAC,KAAK;AAC/B,QAAM,UAAU,oBAAI,IAAa;AAEjC,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,UAAU,MAAM,MAAM;AAC5B,QAAI,CAAC,WAAW,QAAQ,IAAI,OAAO,EAAG;AACtC,YAAQ,IAAI,OAAO;AACnB,QAAI,CAAC,eAAe,OAAO,EAAG;AAE9B,UAAM,SAAS,CAAC,QAAQ,YAAY,QAAQ,WAAW,OAAO;AAC9D,eAAW,OAAO,QAAQ;AACxB,YAAM,OAAO,QAAQ,GAAG;AACxB,UAAI,QAAQ,CAAC,QAAQ,IAAI,IAAI,EAAG,OAAM,KAAK,IAAI;AAAA,IACjD;AAEA,UAAM,OAAO,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AAC/D,UAAM,SAAS,mBAAmB,OAAO;AACzC,UAAM,iBACJ,eAAe,QAAQ,IAAI,KACxB,eAAe,QAAQ,QAAQ,KAC/B,OAAO,QAAQ,eAAe,YAC9B,OAAO,QAAQ,iBAAiB,YAChC,OAAO,QAAQ,eAAe,YAC9B,MAAM,QAAQ,QAAQ,iBAAiB,KACvC,OAAO,QAAQ,0BAA0B,aACzC,OAAO,QAAQ,wBAAwB;AAE5C,UAAM,UAAU,OAAO,QAAQ,YAAY,WACvC,QAAQ,QAAQ,YAAY,IAC5B,OAAO,QAAQ,UAAU,WACvB,QAAQ,MAAM,YAAY,IAC1B;AACN,UAAM,+BACJ,QAAQ,SAAS,iBAAiB,KAC/B,QAAQ,SAAS,sBAAsB,KACvC,QAAQ,SAAS,mBAAmB;AAEzC,UAAM,uBACJ,SAAS,0BACL,WAAW,QAAQ,kBAAkB;AAE3C,QAAI,CAAC,qBAAsB;AAC3B,QAAI,CAAC,eAAe,QAAQ,QAAQ,GAAG;AACrC,YAAM,OAAO,eAAe,QAAQ,IAAI,IAAK,QAAQ,OAA4B;AACjF,YAAM,qBAAqB,OAAO,QAAQ,eAAe,YAAY,OAAO,QAAQ,UAAU,IAC1F,QAAQ,aACR;AACJ,YAAM,mBAAyC;AAAA,QAC7C,IAAI;AAAA,QACJ,eACG,OAAO,QAAQ,iBAAiB,YAAY,QAAQ,aAAa,KAAK,EAAE,SAAS,IAC9E,QAAQ,eACR,MAAM,iBAAiB;AAAA,QAC7B,aACG,OAAO,QAAQ,eAAe,YAAY,QAAQ,WAAW,KAAK,EAAE,SAAS,IAC1E,QAAQ,aACR,MAAM,eAAe;AAAA,QAC3B,iBACE,OAAO,QAAQ,oBAAoB,YAAY,QAAQ,oBAAoB,OACvE,QAAQ,kBACR,MAAM,mBAAmB;AAAA,QAC/B,qBACE,OAAO,QAAQ,wBAAwB,YAAY,QAAQ,wBAAwB,OAC/E,QAAQ,sBACR;AAAA,QACN,uBAAuB,QAAQ,QAAQ,qBAAqB;AAAA,QAC5D,qBAAqB,QAAQ,QAAQ,mBAAmB;AAAA,QACxD,mBAAmB,MAAM,QAAQ,QAAQ,iBAAiB,KAAK,QAAQ,kBAAkB,SAAS,aAAa,IAC3G,CAAC,aAAa,IACd,CAAC;AAAA,QACL,SAAS,CAAC;AAAA,MACZ;AACA,aAAO;AAAA,QACL,UAAU;AAAA,QACV;AAAA,QACA,mBAAmB,OAAO,QAAQ,sBAAsB,YAAY,QAAQ,sBAAsB,OAC9F,QAAQ,oBACR;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,MACL,UAAU,QAAQ;AAAA,MAClB,MAAM,eAAe,QAAQ,IAAI,IAAK,QAAQ,OAA4B;AAAA,MAC1E,mBAAmB,OAAO,QAAQ,sBAAsB,YAAY,QAAQ,sBAAsB,OAC9F,QAAQ,oBACR;AAAA,IACN;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,qBAAqB,OAAyB;AACrD,QAAM,QAAmB,CAAC,KAAK;AAC/B,QAAM,UAAU,oBAAI,IAAa;AAEjC,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,UAAU,MAAM,MAAM;AAC5B,QAAI,CAAC,WAAW,QAAQ,IAAI,OAAO,EAAG;AACtC,YAAQ,IAAI,OAAO;AACnB,QAAI,CAAC,eAAe,OAAO,EAAG;AAE9B,UAAM,SAAS,mBAAmB,OAAO;AACzC,UAAM,OAAO,OAAO,QAAQ,SAAS,WAAW,QAAQ,KAAK,YAAY,IAAI;AAC7E,UAAM,UAAU,OAAO,QAAQ,YAAY,WACvC,QAAQ,QAAQ,YAAY,IAC5B,OAAO,QAAQ,UAAU,WACvB,QAAQ,MAAM,YAAY,IAC1B;AAEN,UAAM,cACJ,SAAS,sBACN,SAAS,eACT,SAAS;AAEd,UAAM,iBACJ,QAAQ,SAAS,WAAW,KACzB,QAAQ,SAAS,aAAa,KAC9B,QAAQ,SAAS,gBAAgB;AAGtC,QAAI,WAAW,OAAO,eAAe,gBAAgB;AACnD,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,CAAC,QAAQ,YAAY,QAAQ,WAAW,SAAS,OAAO;AACvE,eAAW,OAAO,QAAQ;AACxB,YAAM,OAAO,QAAQ,GAAG;AACxB,UAAI,QAAQ,CAAC,QAAQ,IAAI,IAAI,EAAG,OAAM,KAAK,IAAI;AAAA,IACjD;AAAA,EACF;AAEA,SAAO;AACT;AAiCO,SAAS,mCAAmC,MAKnB;AAC9B,QAAM,EAAE,OAAO,gBAAgB,QAAQ,aAAa,IAAI;AACxD,QAAM,UAAU,iCAAiC,KAAK;AACtD,QAAM,0BAA0B,CAAC,kBAAkB,mBAAmB;AACtE,MAAI,CAAC,yBAAyB;AAC5B,QAAI,CAAC,WAAW,CAAC,cAAc,gBAAgB,CAAC,cAAc,WAAY,QAAO,EAAE,QAAQ,SAAS;AACpG,UAAM,sBAAsB,QAAQ,SAAS,cAAc,KAAK,KAAK;AACrE,UAAM,oBAAoB,QAAQ,SAAS,YAAY,KAAK,KAAK;AACjE,QAAI,CAAC,uBAAuB,CAAC,kBAAmB,QAAO,EAAE,QAAQ,SAAS;AAC1E,QAAI,wBAAwB,aAAa,gBAAgB,sBAAsB,aAAa,YAAY;AACtG,aAAO,EAAE,QAAQ,SAAS;AAAA,IAC5B;AAAA,EACF;AACA,MAAI,CAAC,SAAS;AACZ,QAAI,CAAC,cAAc,gBAAgB,CAAC,cAAc,WAAY,QAAO,EAAE,QAAQ,SAAS;AACxF,QAAI,qBAAqB,KAAK,EAAG,QAAO,EAAE,QAAQ,iBAAiB;AAUnE,QAAI,8BAA8B,KAAK,EAAG,QAAO,EAAE,QAAQ,SAAS;AACpE,QAAI,mBAAmB,KAAK,MAAM,IAAK,QAAO,EAAE,QAAQ,kBAAkB;AAC1E,WAAO,EAAE,QAAQ,SAAS;AAAA,EAC5B;AACA,SAAO,EAAE,QAAQ,kBAAkB,QAAQ;AAC7C;AAaO,SAAS,yBACd,aACA,aACyB;AACzB,MAAI,CAAC,YAAa,QAAO,eAAe;AACxC,MACE,YAAY,SAAS,QAClB,YAAY,WAAW,YACvB,eACA,YAAY,OAAO,YAAY,IAClC;AACA,WAAO,EAAE,GAAG,aAAa,OAAO,YAAY,MAAM;AAAA,EACpD;AACA,SAAO;AACT;AAEA,SAAS,gCAAgC;AACvC,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,QAAI,IAAI,aAAa,IAAI,qBAAqB,MAAM,IAAK;AACzD,QAAI,aAAa,OAAO,qBAAqB;AAC7C,UAAM,UAAU,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM,GAAG,IAAI,IAAI;AACvD,WAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,OAAO;AAAA,EAC/D,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,+BAA+B;AACtC,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,QAAI,IAAI,aAAa,IAAI,oBAAoB,MAAM,IAAK;AACxD,QAAI,aAAa,OAAO,oBAAoB;AAC5C,UAAM,UAAU,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM,GAAG,IAAI,IAAI;AACvD,WAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,OAAO;AAAA,EAC/D,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,eAAe,QAAyB;AAC/C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,OAAO,SAAS,eAAe,MAAM;AAC3C,MAAI,EAAE,gBAAgB,iBAAkB,QAAO;AAC/C,OAAK,cAAc;AACnB,SAAO;AACT;AAEA,SAAS,oBAAoB,SAA8C;AACzE,MAAI,QAAQ,gBAAgB,QAAQ,aAAa,KAAK,EAAG,QAAO,QAAQ;AACxE,MAAI,QAAQ,SAAS,QAAS,QAAO;AACrC,MAAI,QAAQ,SAAS,QAAS,QAAO;AACrC,MAAI,QAAQ,SAAU,QAAO;AAC7B,MAAI,QAAQ,UAAW,QAAO;AAC9B,MAAI,QAAQ,OAAQ,QAAO;AAC3B,QAAM,WAAW,QAAQ;AACzB,MAAI,YAAY,SAAS,SAAS,GAAG,GAAG;AACtC,UAAM,CAAC,UAAU,SAAS,IAAI,SAAS,MAAM,GAAG;AAChD,UAAM,SAAS,aAAa;AAC5B,UAAM,qBAAqB,SAAS,KAAK;AACzC,UAAM,mBAAmB,OAAO,KAAK;AACrC,QAAI,sBAAsB,kBAAkB;AAC1C,YAAM,mBAAmB,mBAAmB,SAAS,GAAG,IACpD,mBAAmB,MAAM,GAAG,EAAE,IAC9B;AAEJ,YAAM,gBAAgB;AAAA,QACpB,GAAG,kBAAkB;AAAA,QACrB,GAAG,gBAAgB;AAAA,MACrB;AAEA,UAAI,cAAc;AAClB,iBAAW,UAAU,eAAe;AAClC,YAAI,YAAY,WAAW,MAAM,GAAG;AAClC,wBAAc,YAAY,MAAM,OAAO,MAAM;AAC7C;AAAA,QACF;AAAA,MACF;AAEA,UAAI,YAAa,QAAO,GAAG,kBAAkB,IAAI,WAAW;AAAA,IAC9D;AAAA,EACF;AAEA,QAAM,OAAO,QAAQ,QAAQ;AAC7B,MAAI,KAAK,WAAW,4BAA4B,EAAG,QAAO;AAC1D,MAAI,KAAK,WAAW,+BAA+B,EAAG,QAAO;AAC7D,MAAI,KAAK,WAAW,2BAA2B,EAAG,QAAO;AACzD,MAAI,KAAK,WAAW,wBAAwB,EAAG,QAAO;AACtD,MAAI,KAAK,WAAW,wBAAwB,EAAG,QAAO;AACtD,MAAI,KAAK,WAAW,2BAA2B,GAAG;AAChD,UAAM,QAAQ,QAAQ,SAAS;AAC/B,UAAM,SAAS,IAAI,gBAAgB,KAAK;AACxC,UAAM,OAAO,OAAO,IAAI,MAAM;AAC9B,QAAI,SAAS,QAAS,QAAO;AAC7B,QAAI,SAAS,QAAS,QAAO;AAAA,EAC/B;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,SAA+B,MAA8B;AACtF,MAAI,QAAQ,cAAc,QAAQ,WAAW,KAAK,EAAG,QAAO,QAAQ;AACpE,MAAI,QAAQ,YAAY,QAAQ,SAAS,KAAK,EAAG,QAAO,QAAQ;AAChE,MAAI,QAAQ,YAAY,QAAQ,SAAS,KAAK,EAAG,QAAO,QAAQ;AAChE,MAAI,QAAQ,aAAa,QAAQ,UAAU,KAAK,EAAG,QAAO,QAAQ;AAClE,MAAI,QAAQ,UAAU,QAAQ,OAAO,KAAK,EAAG,QAAO,QAAQ;AAC5D,MAAI,QAAQ,OAAO,SAAS,YAAY,QAAQ,MAAM;AACpD,UAAM,KAAM,KAA0B;AACtC,QAAI,OAAO,OAAO,YAAY,GAAG,KAAK,EAAG,QAAO;AAAA,EAClD;AACA,MAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,UAAM,iBAAkB,KAAuC,QAAQ;AACvE,QAAI,OAAO,mBAAmB,YAAY,eAAe,KAAK,EAAG,QAAO;AACxE,UAAM,kBAAmB,KAAwC,SAAS;AAC1E,QAAI,OAAO,oBAAoB,YAAY,gBAAgB,KAAK,EAAG,QAAO;AAC1E,UAAM,eAAgB,KAAqC,MAAM;AACjE,QAAI,OAAO,iBAAiB,YAAY,aAAa,KAAK,EAAG,QAAO;AAAA,EACtE;AACA,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,QAAQ,KAAK,MAAM,GAAG,EAAE,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;AAC9D,QAAM,aAAa;AAAA,IACjB,CAAC,WAAW,aAAa,QAAQ;AAAA,IACjC,CAAC,WAAW,aAAa,WAAW;AAAA,IACpC,CAAC,WAAW,aAAa,OAAO;AAAA,IAChC,CAAC,WAAW,SAAS,QAAQ;AAAA,IAC7B,CAAC,WAAW,SAAS,QAAQ;AAAA,IAC7B,CAAC,WAAW,SAAS,WAAW;AAAA,EAClC;AACA,aAAW,UAAU,YAAY;AAC/B,UAAM,gBAAgB,OAAO,MAAM,CAAC,SAAS,UAAU,MAAM,KAAK,MAAM,OAAO;AAC/E,QAAI,CAAC,iBAAiB,MAAM,UAAU,OAAO,OAAQ;AACrD,UAAM,QAAQ,MAAM,OAAO,MAAM,KAAK;AACtC,QAAI,CAAC,MAAO;AACZ,QAAI;AACF,YAAM,UAAU,mBAAmB,KAAK,EAAE,KAAK;AAC/C,UAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,IACjC,QAAQ;AACN,YAAM,UAAU,MAAM,KAAK;AAC3B,UAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,cACP,SACA,cACA,YACQ;AACR,MAAI,QAAQ,UAAU,QAAQ,OAAO,KAAK,EAAE,SAAS,EAAG,QAAO,QAAQ;AACvE,MAAI,gBAAgB,WAAY,QAAO,eAAe,YAAY,IAAI,UAAU;AAChF,MAAI,QAAQ,QAAQ,QAAQ,KAAK,KAAK,EAAE,SAAS,GAAG;AAClD,UAAM,QAAQ,QAAQ,OAAO,KAAK;AAClC,WAAO,eAAe,QAAQ,IAAI,GAAG,QAAQ,IAAI,KAAK,KAAK,EAAE;AAAA,EAC/D;AACA,SAAO;AACT;AAEA,eAAe,YAAY,OAKxB;AACD,QAAM,QAAQ,6BAA6B;AAAA,IACzC,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU;AAAA,MACnB,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM;AAAA,MAClB,OAAO,MAAM,SAAS;AAAA,MACtB,QAAQ,MAAM,UAAU;AAAA,IAC1B,CAAC;AAAA,EACH,CAAC;AACH;AAEA,SAAS,yBAAyB,OAK/B;AACD,QAAM,UAAU,KAAK,UAAU;AAAA,IAC7B,cAAc,MAAM;AAAA,IACpB,YAAY,MAAM;AAAA,IAClB,OAAO,MAAM,SAAS;AAAA,IACtB,QAAQ,MAAM,UAAU;AAAA,EAC1B,CAAC;AAED,MAAI;AACF,QAAI,OAAO,cAAc,eAAe,OAAO,UAAU,eAAe,YAAY;AAClF,YAAM,OAAO,IAAI,KAAK,CAAC,OAAO,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAC7D,YAAM,OAAO,UAAU,WAAW,6BAA6B,IAAI;AACnE,UAAI,KAAM;AAAA,IACZ;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,OAAK,MAAM,6BAA6B;AAAA,IACtC,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM;AAAA,IACN,WAAW;AAAA,IACX,aAAa;AAAA,EACf,CAAC,EAAE,MAAM,CAAC,UAAU;AAClB,WAAO,KAAK,kDAAkD,EAAE,KAAK,MAAM,CAAC;AAAA,EAC9E,CAAC;AACH;AAEe,SAAR,oBAAqC;AAAA,EAC1C;AAAA,EACA;AACF,GAAiF;AAC/E,QAAM,IAAI,KAAK;AACf,QAAM,eAAe,gBAAgB;AACrC,QAAM,eAAe,MAAM,QAAQ,MAAM,oBAAoB,OAAO,GAAG,CAAC,OAAO,CAAC;AAChF,QAAM,aAAa,MAAM,QAAQ,MAAM,kBAAkB,SAAS,IAAI,GAAG,CAAC,SAAS,IAAI,CAAC;AACxF,QAAM,SAAS,MAAM;AAAA,IACnB,MAAM,cAAc,SAAS,cAAc,UAAU;AAAA,IACrD,CAAC,SAAS,YAAY,YAAY;AAAA,EACpC;AACA,QAAM,CAAC,EAAE,WAAW,IAAI,MAAM,WAAW,CAAC,UAAU,QAAQ,GAAG,CAAC;AAChE,QAAM,QAAQ,uBAAuB,MAAM;AAC3C,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,8BAA8B,+BAA+B,IAAI,MAAM,SAAS,KAAK;AAC5F,QAAM,CAAC,0BAA0B,2BAA2B,IAAI,MAAM,SAAS,KAAK;AACpF,QAAM,CAAC,sBAAsB,uBAAuB,IAAI,MAAM,SAAS,KAAK;AAC5E,QAAM,aAAa,MAAM;AAAA,IACvB,MACE,sBAAsB,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,IAC1F,CAAC;AAAA,EACH;AACA,QAAM,gBAAgB,QAAQ,SAAS,IAAI;AAC3C,QAAM,WAAW,gBAAgB,aAAa,GAAG,YAAY,IAAI,UAAU,KAAK;AAChF,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAS,IAAI;AACrE,QAAM,oBAAoB,MAAM,OAItB,IAAI;AACd,QAAM,0BAA0B,MAAM,OAAO,CAAC;AAC9C,QAAM,yBAAyB,MAAM,YAAY,MAAM;AACrD,QAAI,CAAC,SAAU,QAAO;AACtB,UAAM,QAAQ,sBAAsB,EAAE,IAAI,QAAQ;AAClD,WAAO,QAAQ,SAAS,MAAM,eAAe,cAAc,MAAM,WAAW,MAAM;AAAA,EACpF,GAAG,CAAC,QAAQ,YAAY,QAAQ,CAAC;AAEjC,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,UAAU;AACb,2BAAqB,IAAI;AACzB;AAAA,IACF;AAEA,UAAM,SAAS,sBAAsB;AACrC,UAAM,sBAAsB,MAAM;AAChC,UAAI,OAAO,WAAW,YAAa;AACnC,aAAO;AAAA,QACL,IAAI,YAAY,gCAAgC;AAAA,UAC9C,QAAQ,EAAE,SAAS;AAAA,QACrB,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,iBAAiB,MAAM;AAC3B,YAAM,UAAU,OAAO,IAAI,QAAQ;AACnC,UAAI,CAAC,SAAS;AACZ,eAAO,IAAI,UAAU,EAAE,YAAY,UAAU,eAAe,OAAO,CAAC;AACpE,6BAAqB,IAAI;AACzB,4BAAoB;AACpB;AAAA,MACF;AACA,UAAI,QAAQ,eAAe,YAAY;AACrC,eAAO,IAAI,UAAU,EAAE,YAAY,UAAU,eAAe,OAAO,CAAC;AACpE,6BAAqB,IAAI;AACzB;AAAA,MACF;AACA,UAAI,gBAAgB,QAAQ,UAAU;AACpC,eAAO,IAAI,UAAU,EAAE,YAAY,UAAU,eAAe,OAAO,CAAC;AACpE,6BAAqB,IAAI;AACzB,4BAAoB;AACpB;AAAA,MACF;AACA,2BAAqB,KAAK;AAAA,IAC5B;AAEA,mBAAe;AACf,UAAM,kBAAkB,MAAM,eAAe;AAC7C,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,iBAAiB,gCAAgC,eAAe;AAAA,IACzE;AAEA,WAAO,MAAM;AACX,UAAI,OAAO,WAAW,aAAa;AACjC,eAAO,oBAAoB,gCAAgC,eAAe;AAAA,MAC5E;AACA,YAAM,UAAU,OAAO,IAAI,QAAQ;AACnC,UAAI,SAAS,eAAe,YAAY;AACtC,eAAO,OAAO,QAAQ;AACtB,4BAAoB;AAAA,MACtB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,YAAY,UAAU,aAAa,CAAC;AAEhD,QAAM,UAAU,MAAM;AACpB,QAAI,kBAAmB;AACvB,QAAI,uBAAuB,EAAG;AAC9B,UAAM,UAAU,uBAAuB,MAAM;AAC7C,QAAI,CAAC,SAAS,MAAM,SAAS,CAAC,QAAQ,gBAAgB,CAAC,QAAQ,YAAY;AACzE,+BAAyB,MAAM;AAC/B;AAAA,IACF;AACA,SAAK,YAAY;AAAA,MACf,cAAc,QAAQ;AAAA,MACtB,YAAY,QAAQ;AAAA,MACpB,OAAO,QAAQ,KAAK;AAAA,MACpB,QAAQ;AAAA,IACV,CAAC,EAAE,MAAM,CAAC,UAAU;AAClB,aAAO,KAAK,+CAA+C,EAAE,KAAK,MAAM,CAAC;AAAA,IAC3E,CAAC;AACD,6BAAyB,MAAM;AAAA,EACjC,GAAG,CAAC,QAAQ,wBAAwB,iBAAiB,CAAC;AAEtD,QAAM,UAAU,MAAM;AACpB,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,UAAM,sBAAsB,cAAc,IAAI,qBAAqB,MAAM;AACzE,UAAM,qBAAqB,cAAc,IAAI,oBAAoB,MAAM;AAEvE,QAAI,qBAAqB;AACvB,sCAAgC,IAAI;AACpC,oCAA8B;AAAA,IAChC;AAEA,QAAI,oBAAoB;AACtB,kCAA4B,IAAI;AAChC,mCAA6B;AAAA,IAC/B;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,UAAU,MAAM,6BAA6B,QAAQ,MAAM,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC;AAEzF,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,OAAO,UAAU;AACpB,8BAAwB,KAAK;AAC7B;AAAA,IACF;AACA,4BAAwB,IAAI;AAAA,EAC9B,GAAG;AAAA,IACD,OAAO,UAAU;AAAA,IACjB,OAAO,UAAU;AAAA,IACjB,OAAO,UAAU;AAAA,EACnB,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,QAAI,CAAC,gBAAgB,CAAC,WAAY;AAClC,2BAAuB,QAAQ,EAAE,QAAQ,cAAc,WAAW,CAAC;AAAA,EACrE,GAAG,CAAC,QAAQ,mBAAmB,YAAY,YAAY,CAAC;AAExD,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,QAAI,CAAC,gBAAgB,CAAC,WAAY;AAClC,QAAI,SAAS;AACb,UAAM,UAAU,YAAY;AAC1B,YAAM,iBAAiB,uBAAuB,MAAM;AACpD,YAAM,OAAO,MAAM,QAAyB,6BAA6B;AAAA,QACvE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,cAAc,WAAW,CAAC;AAAA,MACnD,CAAC;AACD,YAAM,UAAU,KAAK,UAAU,CAAC;AAChC,YAAM,eAAe,uBAAuB,MAAM;AAClD,UACE,CAAC,UACE,CAAC,gBACD,aAAa,kBAAkB,QAC/B,aAAa,iBAAiB,gBAC9B,aAAa,eAAe,eAC3B,aAAa,MAAM,SAAS,WAAW,gBAAgB,MAAM,SAAS,MAC1E;AACF,YAAM,oBAAoB,aAAa,sBAAsB,SACzD,aAAa,oBACZ,QAAQ,qBAAqB;AAClC,UAAI,CAAC,KAAK,IAAI;AACZ,cAAM,iBAAiB,KAAK,WAAW,MACnC,EAAE,wBAAwB,WAAW,IACrC,EAAE,sCAAsC,oCAAoC;AAChF,cAAM,UAAU,OAAO,QAAQ,UAAU,YAAY,QAAQ,MAAM,KAAK,EAAE,SACtE,QAAQ,QACR;AACJ,cAAM,SAAS,OAAO;AACtB,+BAAuB,QAAQ;AAAA,UAC7B;AAAA,UACA;AAAA,UACA;AAAA,UACA,UAAU;AAAA,UACV,MAAM,QAAQ,QAAQ;AAAA,UACtB,eAAe,QAAQ,iBAAiB;AAAA,UACxC,kBAAkB,QAAQ,oBAAoB;AAAA,UAC9C;AAAA,UACA,kBAAkB,QAAQ,oBAAoB;AAAA,QAChD,CAAC;AACD;AAAA,MACF;AACA,6BAAuB,QAAQ;AAAA,QAC7B;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,QAAQ,YAAY;AAAA,QAC9B,MAAM,QAAQ,QAAQ;AAAA,QACtB,eAAe,QAAQ,iBAAiB;AAAA,QACxC,kBAAkB,QAAQ,oBAAoB;AAAA,QAC9C;AAAA,QACA,kBAAkB,QAAQ,oBAAoB;AAAA,MAChD,CAAC;AAAA,IACH;AACA,SAAK,QAAQ;AACb,WAAO,MAAM;AACX,eAAS;AAAA,IACX;AAAA,EACF,GAAG,CAAC,QAAQ,mBAAmB,YAAY,YAAY,CAAC;AAExD,QAAM,OAAO,QAAQ,OAAO,MAAM,KAAK;AACvC,QAAM,eAAe,MAAM,QAAQ,MAAM;AACvC,QAAI,CAAC,OAAO,KAAM,QAAO,CAAC;AAC1B,UAAM,cAAc,MAAM,QAAQ,MAAM,KAAK,YAAY,IAAI,MAAM,KAAK,eAAe,CAAC;AACxF,QAAI,YAAY,OAAQ,QAAO;AAC/B,WAAO,CAAC;AAAA,MACN,QAAQ,MAAM,KAAK;AAAA,MACnB,cAAc,MAAM,KAAK;AAAA,MACzB,eAAe,MAAM,KAAK;AAAA,MAC1B,YAAY,MAAM,KAAK;AAAA,MACvB,UAAU,MAAM,KAAK;AAAA,MACrB,iBAAiB,MAAM,KAAK;AAAA,MAC5B,WAAW,MAAM,KAAK;AAAA,IACxB,CAAC;AAAA,EACH,GAAG,CAAC,OAAO,IAAI,CAAC;AAChB,QAAM,yBAAyB,OAAO,MAAM,0BAA0B,aAAa;AACnF,QAAM,oBAAoB,MAAM,QAAQ,MAAM;AAC5C,QAAI,CAAC,OAAO,cAAe,QAAO;AAClC,WAAO,aAAa,OAAO,CAAC,gBAAgB,YAAY,WAAW,MAAM,aAAa;AAAA,EACxF,GAAG,CAAC,cAAc,OAAO,aAAa,CAAC;AAEvC,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,QAAI,CAAC,QAAQ,CAAC,OAAO,MAAM,GAAI;AAE/B,UAAM,eAAe,CAAC,UAAiB;AACrC,YAAM,SAAS,eAAgB,MAA+B,MAAM,IAC9D,MAA+B,SACjC;AACJ,UAAI,CAAC,OAAQ;AACb,UAAI,OAAO,mBAAmB,MAAM,MAAM,GAAI;AAC9C,kCAA4B,IAAI;AAAA,IAClC;AAEA,WAAO,iBAAiB,mCAAmC,YAAY;AAEvE,WAAO,MAAM;AACX,aAAO,oBAAoB,mCAAmC,YAAY;AAAA,IAC5E;AAAA,EACF,GAAG,CAAC,mBAAmB,MAAM,OAAO,MAAM,EAAE,CAAC;AAE7C,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,QAAI,CAAC,OAAO,gBAAgB,CAAC,OAAO,WAAY;AAChD,QAAI,MAAM,kBAAkB,KAAM;AAClC,UAAM,kBAAkB,CAAC,UAAiB;AACxC,YAAM,SAAS,eAAgB,MAA+B,MAAM,IAC9D,MAA+B,SACjC;AACJ,UAAI,CAAC,OAAQ;AACb,UAAI,OAAO,eAAe,MAAM,WAAY;AAC5C,YAAM,OAAO,iBAAiB,OAAO,YAAY;AACjD,UAAI,QAAQ,SAAS,MAAM,aAAc;AACzC,8BAAwB,IAAI;AAC5B,6BAAuB,QAAQ;AAAA,QAC7B,eAAe;AAAA,QACf,UAAU;AAAA,QACV,MAAM;AAAA,QACN,UAAU;AAAA,QACV,mBAAmB;AAAA,QACnB,mBAAmB;AAAA,QACnB,wBAAwB;AAAA,MAC1B,CAAC;AAAA,IACH;AAEA,WAAO,iBAAiB,mCAAmC,eAAe;AAE1E,WAAO,MAAM;AACX,aAAO,oBAAoB,mCAAmC,eAAe;AAAA,IAC/E;AAAA,EACF,GAAG,CAAC,QAAQ,mBAAmB,OAAO,eAAe,OAAO,YAAY,OAAO,YAAY,CAAC;AAE5F,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,QAAI,CAAC,OAAO,MAAM,SAAS,CAAC,MAAM,gBAAgB,CAAC,MAAM,WAAY;AACrE,UAAM,mBACJ,OAAO,MAAM,qBAAqB,YAC/B,OAAO,SAAS,MAAM,gBAAgB,KACtC,MAAM,mBAAmB,IAE1B,MAAM,mBACN;AACJ,UAAM,aAAa,KAAK,IAAI,KAAO,KAAK,MAAM,mBAAmB,GAAI,CAAC;AACtE,UAAM,WAAW,OAAO,YAAY,MAAM;AACxC,WAAK,QAAQ,+BAA+B;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU;AAAA,UACnB,cAAc,MAAM;AAAA,UACpB,YAAY,MAAM;AAAA,UAClB,OAAO,MAAM,MAAM;AAAA,QACrB,CAAC;AAAA,MACH,CAAC;AAAA,IACH,GAAG,UAAU;AACb,WAAO,MAAM,OAAO,cAAc,QAAQ;AAAA,EAC5C,GAAG,CAAC,mBAAmB,OAAO,kBAAkB,OAAO,MAAM,OAAO,OAAO,YAAY,OAAO,YAAY,CAAC;AAE3G,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,UAAM,wBAAwB,QAAQ,OAAO,QAAQ,KAChD,EACD,OAAO,2BAA2B,QAC/B,OAAO,OAAO,sBAAsB,YACpC,MAAM,sBAAsB;AAEnC,QAAI,sBAAuB;AAC3B,QAAI,CAAC,OAAO,gBAAgB,CAAC,OAAO,WAAY;AAChD,QAAI,MAAM,kBAAkB,KAAM;AAClC,QAAI,SAAS;AACb,UAAM,kBAAkB,YAAY;AAClC,YAAM,iBAAiB,uBAAuB,MAAM;AACpD,UACE,CAAC,UACE,CAAC,kBACD,eAAe,kBAAkB,QACjC,eAAe,iBAAiB,MAAM,gBACtC,eAAe,eAAe,MAAM,WACvC;AACF,YAAM,OAAO,MAAM,QAAyB,6BAA6B;AAAA,QACvE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU;AAAA,UACnB,cAAc,MAAM;AAAA,UACpB,YAAY,MAAM;AAAA,QACpB,CAAC;AAAA,MACH,CAAC;AACD,YAAM,UAAU,KAAK,UAAU,CAAC;AAChC,YAAM,eAAe,uBAAuB,MAAM;AAClD,UACE,CAAC,UACE,CAAC,gBACD,aAAa,kBAAkB,QAC/B,aAAa,iBAAiB,eAAe,gBAC7C,aAAa,eAAe,eAAe,eAC1C,aAAa,MAAM,SAAS,WAAW,eAAe,MAAM,SAAS,MACzE;AACF,YAAM,wBAAwB,aAAa,sBAAsB,SAC7D,aAAa,oBACZ,QAAQ,qBAAqB;AAClC,UAAI,CAAC,KAAK,IAAI;AACZ,+BAAuB,QAAQ;AAAA,UAC7B,cAAc,MAAM;AAAA,UACpB,YAAY,MAAM;AAAA,UAClB,UAAU;AAAA,UACV,MAAM,QAAQ,QAAQ;AAAA,UACtB,eAAe,QAAQ,iBAAiB,cAAc,iBAAiB;AAAA,UACvE,kBAAkB,QAAQ,oBAAoB,cAAc,oBAAoB;AAAA,UAChF,mBAAmB;AAAA,UACnB,kBAAkB,QAAQ,oBAAoB;AAAA,QAChD,CAAC;AACD;AAAA,MACF;AACA,6BAAuB,QAAQ;AAAA,QAC7B,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,UAAU,QAAQ,YAAY;AAAA,QAC9B,MAAM,QAAQ,QAAQ;AAAA,QACtB,eAAe,QAAQ,iBAAiB;AAAA,QACxC,kBAAkB,QAAQ,oBAAoB;AAAA,QAC9C,mBAAmB;AAAA,QACnB,kBAAkB,QAAQ,oBAAoB;AAAA,MAChD,CAAC;AAAA,IACH;AACA,UAAM,oBAAoB,CAAC,UAAiB;AAC1C,YAAM,SAAS,eAAgB,MAA+B,MAAM,IAC9D,MAA+B,SACjC;AACJ,UAAI,CAAC,OAAQ;AACb,UAAI,OAAO,eAAe,MAAM,WAAY;AAC5C,YAAM,OAAO,iBAAiB,OAAO,YAAY;AACjD,UAAI,QAAQ,SAAS,MAAM,aAAc;AACzC,sCAAgC,IAAI;AACpC,WAAK,gBAAgB;AAAA,IACvB;AACA,UAAM,kBAAkB,CAAC,UAAiB;AACxC,YAAM,SAAS,eAAgB,MAA+B,MAAM,IAC9D,MAA+B,SACjC;AACJ,UAAI,CAAC,OAAQ;AACb,UAAI,OAAO,eAAe,MAAM,WAAY;AAC5C,YAAM,OAAO,iBAAiB,OAAO,YAAY;AACjD,UAAI,QAAQ,SAAS,MAAM,aAAc;AACzC,WAAK,gBAAgB;AAAA,IACvB;AACA,UAAM,sBAAsB,CAAC,UAAiB;AAC5C,YAAM,SAAS,eAAgB,MAA+B,MAAM,IAC/D,MAA+C,SAChD;AACJ,UAAI,QAAQ,OAAO,wBAAyB;AAC5C,WAAK,gBAAgB;AAAA,IACvB;AACA,UAAM,UAAU,MAAM;AACpB,WAAK,gBAAgB;AAAA,IACvB;AACA,UAAM,qBAAqB,MAAM;AAC/B,UAAI,CAAC,SAAS,QAAQ;AACpB,aAAK,gBAAgB;AAAA,MACvB;AAAA,IACF;AAEA,WAAO,iBAAiB,qCAAqC,iBAAiB;AAC9E,WAAO,iBAAiB,mCAAmC,eAAe;AAC1E,WAAO,iBAAiB,YAAY,mBAAmB;AACvD,WAAO,iBAAiB,SAAS,OAAO;AACxC,aAAS,iBAAiB,oBAAoB,kBAAkB;AAChE,UAAM,oBAAoB,OAAO,YAAY,MAAM;AACjD,WAAK,gBAAgB;AAAA,IACvB,GAAG,GAAM;AACT,SAAK,gBAAgB;AAErB,WAAO,MAAM;AACX,eAAS;AACT,aAAO,oBAAoB,qCAAqC,iBAAiB;AACjF,aAAO,oBAAoB,mCAAmC,eAAe;AAC7E,aAAO,oBAAoB,YAAY,mBAAmB;AAC1D,aAAO,oBAAoB,SAAS,OAAO;AAC3C,eAAS,oBAAoB,oBAAoB,kBAAkB;AACnE,aAAO,cAAc,iBAAiB;AAAA,IACxC;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,EACT,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,QAAI,CAAC,6BAA8B;AACnC,QAAI,CAAC,OAAO,gBAAgB,CAAC,OAAO,cAAc,CAAC,OAAO,KAAM;AAChE,QAAI,YAAY;AAChB,UAAM,4BAA4B,YAAY;AAC5C,oCAA8B;AAC9B,sCAAgC,KAAK;AACrC,YAAM,mBAAmB,CAAC,GAAG,OAAO;AACpC,UAAI,UAAW;AAAA,IACjB;AACA,SAAK,0BAA0B;AAC/B,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,SAAS,mBAAmB,8BAA8B,OAAO,MAAM,OAAO,YAAY,OAAO,cAAc,CAAC,CAAC;AAErH,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,QACE,OAAO,gBACJ,OAAO,cACP,OAAO,MAAM,MAAM,UAAU,YAC7B,MAAM,KAAK,MAAM,KAAK,EAAE,SAAS,GACpC;AACA,wBAAkB,UAAU;AAAA,QAC1B,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,OAAO,MAAM,KAAK;AAAA,MACpB;AACA;AAAA,IACF;AACA,sBAAkB,UAAU;AAAA,EAC9B,GAAG,CAAC,mBAAmB,OAAO,MAAM,OAAO,OAAO,YAAY,OAAO,YAAY,CAAC;AAElF,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,UAAM,aAAa,MAAM;AACvB,YAAM,UAAU,kBAAkB;AAClC,UAAI,CAAC,QAAS;AACd,+BAAyB;AAAA,QACvB,GAAG;AAAA,QACH,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AACA,WAAO,iBAAiB,YAAY,UAAU;AAC9C,WAAO,MAAM;AACX,aAAO,oBAAoB,YAAY,UAAU;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,iBAAiB,CAAC;AAEtB,QAAM,UAAU,MAAM;AACpB,WAAO,MAAM;AACX,UAAI,uBAAuB,EAAG;AAC9B,YAAM,UAAU,uBAAuB,MAAM;AAC7C,UAAI,SAAS,MAAM,SAAS,QAAQ,gBAAgB,QAAQ,YAAY;AACtE,aAAK,YAAY;AAAA,UACf,cAAc,QAAQ;AAAA,UACtB,YAAY,QAAQ;AAAA,UACpB,OAAO,QAAQ,KAAK;AAAA,UACpB,QAAQ;AAAA,QACV,CAAC;AAAA,MACH;AACA,+BAAyB,MAAM;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,QAAQ,sBAAsB,CAAC;AAEnC,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,UAAM,kBAAkB,CAAC,UAAiB;AACxC,YAAM,uBAAuB,CAAC,YAIxB;AACJ,gCAAwB,IAAI;AAC5B,cAAM,YAA0C;AAAA,UAC9C,UAAU,QAAQ;AAAA,UAClB,mBAAmB,QAAQ,SAAS;AAAA,UACpC,mBAAmB;AAAA,UACnB,wBAAwB;AAAA,QAC1B;AACA,YAAI,QAAQ,SAAS,QAAW;AAC9B,oBAAU,OAAO,yBAAyB,uBAAuB,MAAM,GAAG,MAAM,QAAQ,IAAI;AAAA,QAC9F;AACA,YAAI,QAAQ,sBAAsB,QAAW;AAC3C,oBAAU,oBAAoB,QAAQ;AAAA,QACxC;AACA,+BAAuB,QAAQ;AAAA,UAC7B,GAAG;AAAA,QACL,CAAC;AAAA,MACH;AAEA,YAAM,wBAAwB,CAACA,kBAAsC;AACnE,cAAM,sBAAsB,OAAOA,cAAa,iBAAiB,IAC7DA,cAAa,oBACb,OAAOA,cAAa,UAAU,EAAE,IAC9BA,cAAa,SAAS,KACtB;AACN,eAAQ;AAAA,UACN,UAAU;AAAA,YACR,IAAI;AAAA,YACJ,cAAcA,cAAa,gBAAgB;AAAA,YAC3C,YAAYA,cAAa,cAAc;AAAA,YACzC,iBAAiBA,cAAa,qBAAqB;AAAA,YACnD,qBAAqB;AAAA,YACrB,uBAAuB;AAAA,YACvB,qBAAqB;AAAA,YACrB,mBAAmB,CAAC;AAAA,YACpB,SAAS,CAAC;AAAA,UACZ;AAAA,UACA,MAAMA,cAAa,QAAQ;AAAA,UAC3B,mBAAmBA,cAAa,qBAAqB;AAAA,QACvD;AAAA,MACA;AAEA,YAAM,SAAU,MAAgD;AAChE,UAAI,CAAC,OAAQ;AACb,YAAM,iBAAiB,OAAO,aAAa,OAAO;AAClD,YAAM,eAAe,uBAAuB,MAAM;AAClD,YAAM,WAAW,mCAAmC;AAAA,QAClD,OAAO,OAAO;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,UAAI,SAAS,WAAW,SAAU;AAClC,UAAI,SAAS,WAAW,kBAAkB;AACxC,gCAAwB,IAAI;AAC5B,+BAAuB,QAAQ;AAAA,UAC7B,eAAe;AAAA,UACf,UAAU;AAAA,UACV,MAAM;AAAA,UACN,UAAU;AAAA,UACV,mBAAmB;AAAA,UACnB,mBAAmB;AAAA,UACnB,wBAAwB;AAAA,QAC1B,CAAC;AACD;AAAA,MACF;AACA,UAAI,SAAS,WAAW,mBAAmB;AACzC,YAAI,aAAc,sBAAqB,sBAAsB,YAAY,CAAC;AAC1E;AAAA,MACF;AAEA,2BAAqB,SAAS,OAAO;AAAA,IACvC;AAEA,WAAO,iBAAiB,8BAA8B,eAAe;AACrE,WAAO,iBAAiB,sBAAsB,eAAe;AAC7D,WAAO,MAAM;AACX,aAAO,oBAAoB,8BAA8B,eAAe;AACxE,aAAO,oBAAoB,sBAAsB,eAAe;AAAA,IAClE;AAAA,EACF,GAAG,CAAC,QAAQ,iBAAiB,CAAC;AAQ9B,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,kBAAmB;AACxB,QAAI,CAAC,gBAAgB,CAAC,WAAY;AAClC,UAAM,aAAa,kCAAkC,CAAC,WAAW,UAAU;AACzE,YAAM,UAAU,iCAAiC,KAAK;AACtD,UAAI,CAAC,QAAS,QAAO;AACrB,YAAM,cAAc,QAAQ,SAAS,cAAc,KAAK,KAAK;AAC7D,YAAM,YAAY,QAAQ,SAAS,YAAY,KAAK,KAAK;AACzD,UAAI,eAAe,cAAc,gBAAgB,gBAAgB,cAAc,YAAa,QAAO;AACnG,8BAAwB,IAAI;AAC5B,6BAAuB,QAAQ;AAAA,QAC7B,UAAU,QAAQ;AAAA,QAClB,mBAAmB,QAAQ,SAAS;AAAA,QACpC,mBAAmB;AAAA,QACnB,wBAAwB;AAAA,MAC1B,CAAC;AACD,aAAO;AAAA,IACT,CAAC;AACD,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,mBAAmB,cAAc,UAAU,CAAC;AAExD,QAAM,iBAAiB,MAAM,YAAY,YAAY;AACnD,4BAAwB,WAAW;AACnC,QAAI,CAAC,OAAO,gBAAgB,CAAC,OAAO,WAAY;AAChD,UAAM,OAAO,MAAM,QAAyB,mCAAmC;AAAA,MAC7E,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU;AAAA,QACnB,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,MACpB,CAAC;AAAA,IACH,CAAC;AACD,QAAI,CAAC,KAAK,IAAI;AACZ,YAAM,EAAE,4CAA4C,8BAA8B,GAAG,OAAO;AAC5F;AAAA,IACF;AACA,UAAM,UAAU,MAAM,QAAyB,6BAA6B;AAAA,MAC1E,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,cAAc,MAAM,cAAc,YAAY,MAAM,WAAW,CAAC;AAAA,IACzF,CAAC;AACD,QAAI,CAAC,QAAQ,IAAI;AACf,YAAM,EAAE,4CAA4C,8BAA8B,GAAG,OAAO;AAC5F;AAAA,IACF;AACA,UAAM,UAAU,QAAQ,UAAU,CAAC;AACnC,2BAAuB,QAAQ;AAAA,MAC7B,UAAU,QAAQ,YAAY;AAAA,MAC9B,MAAM,QAAQ,QAAQ;AAAA,MACtB,eAAe,QAAQ,iBAAiB;AAAA,MACxC,kBAAkB,QAAQ,oBAAoB;AAAA,MAC9C,mBAAmB,QAAQ,qBAAqB;AAAA,MAChD,kBAAkB,QAAQ,oBAAoB;AAAA,MAC9C,UAAU;AAAA,MACV,mBAAmB;AAAA,MACnB,mBAAmB;AAAA,MACnB,wBAAwB;AAAA,IAC1B,CAAC;AAAA,EACH,GAAG,CAAC,QAAQ,OAAO,YAAY,OAAO,cAAc,CAAC,CAAC;AAEtD,QAAM,uBAAuB,MAAM,YAAY,YAAY;AACzD,4BAAwB,WAAW;AACnC,UAAM,kBAAkB;AAAA,MACtB,UAAU,OAAO;AAAA,MACjB,cAAc,OAAO;AAAA,MACrB,YAAY,OAAO;AAAA,MACnB,sBAAsB,YAAY;AAChC,cAAM,aAAa,MAAM,mBAAmB,CAAC,GAAG,OAAO;AACvD,eAAO,kBAAkB,WAAW,QAAQ;AAAA,MAC9C;AAAA,MACA,iBAAiB,OAAO,eAAe;AACrC,cAAM,eAAe,6BAA6B;AAAA,UAChD,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU;AAAA,YACnB,cAAc,OAAO;AAAA,YACrB,YAAY,OAAO;AAAA,YACnB,OAAO,OAAO,MAAM,SAAS;AAAA,YAC7B,QAAQ;AAAA,YACR;AAAA,YACA,YAAY;AAAA,UACd,CAAC;AAAA,QACH,CAAC;AAAA,MACH;AAAA,MACA,oBAAoB,MAAM;AACxB,+BAAuB,QAAQ;AAAA,UAC7B,UAAU;AAAA,UACV,mBAAmB;AAAA,UACnB,mBAAmB;AAAA,UACnB,wBAAwB;AAAA,QAC1B,CAAC;AAAA,MACH;AAAA,MACA,QAAQ,MAAM;AACZ,eAAO,SAAS,OAAO;AAAA,MACzB;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,SAAS,QAAQ,OAAO,UAAU,OAAO,MAAM,OAAO,OAAO,YAAY,OAAO,YAAY,CAAC;AAEjG,QAAM,iBAAiB,MAAM,YAAY,MAAM;AAC7C,QAAI,CAAC,OAAO,SAAU;AACtB,UAAM,kBAAkB,YAAY;AAClC,YAAM,uBAAuB,wBAAwB,UAAU;AAC/D,8BAAwB,UAAU;AAClC,UAAI,aAA4B,OAAO,MAAM,UAAU,EAAE,IAAI,MAAM,SAAS,KAAK;AACjF,UAAI,CAAC,YAAY;AACf,cAAM,aAAa,MAAM,mBAAmB,CAAC,GAAG,OAAO;AACvD,qBAAa,OAAO,WAAW,UAAU,EAAE,IAAI,WAAW,SAAS,KAAK;AAAA,MAC1E;AACA,UAAI,CAAC,YAAY;AACf;AAAA,UACE;AAAA,YACE;AAAA,YACA;AAAA,UACF;AAAA,UACA;AAAA,QACF;AACA;AAAA,MACF;AACA,6BAAuB,QAAQ;AAAA,QAC7B,mBAAmB;AAAA,QACnB,mBAAmB;AAAA,QACnB,wBAAwB;AAAA,MAC1B,CAAC;AACD,aAAO,WAAW,YAAY;AAC5B,YAAI,wBAAwB,YAAY,qBAAsB;AAC9D,cAAM,eAAe,uBAAuB,MAAM;AAClD,YAAI,cAAc,sBAAsB,cAAe;AACvD,cAAM,YAAY,eAAe,MAAM;AACvC,YAAI,UAAW;AACf,cAAM,UAAU,MAAM,QAAQ,QAAQ,QAAQ,oBAAoB,CAAC,EAAE,MAAM,MAAM,KAAK;AACtF,YAAI,CAAC,SAAS;AACZ;AAAA,YACE;AAAA,cACE;AAAA,cACA;AAAA,YACF;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF,GAAG,CAAC;AAAA,IACN;AACA,SAAK,gBAAgB;AAAA,EACvB,GAAG,CAAC,SAAS,QAAQ,mBAAmB,QAAQ,OAAO,UAAU,CAAC,CAAC;AAEnE,QAAM,oBAAoB,MAAM,YAAY,MAAM;AAChD,4BAAwB,WAAW;AACnC,4BAAwB,KAAK;AAAA,EAC/B,GAAG,CAAC,CAAC;AAEL,QAAM,YAAY,EAAE,wBAAwB;AAC5C,QAAM,qBAAqB,MAAM;AAAA,IAC/B,OAAO,OAAO,UAAU,WAAW,CAAC,GAAG,IAAI,CAAC,YAAY;AAAA,MACtD,OAAO,OAAO;AAAA,MACd,MAAM,OAAO;AAAA,MACb,IAAI,OAAO;AAAA,IACb,EAAE;AAAA,IACF,CAAC,OAAO,UAAU,OAAO;AAAA,EAC3B;AACA,QAAM,mBAAmB;AAAA,IACvB,OAAO,UAAU,yBACd,OAAO,UAAU,wBAAwB,QACzC,OAAO,UAAU,mBAAmB,SAAS,aAAa;AAAA,EAC/D;AACA,QAAM,kBAAkB,OAAO,kBAAkB;AACjD,QAAM,4BAA4B;AAAA,IAChC,OAAO,UAAU,yBACd,CAAC,OAAO,UAAU;AAAA,EACvB;AACA,QAAM,iBACJ,oBAAC,UAAO,MAAM,QAAQ,OAAO,YAAY,eAAe,KAAK,sBAAsB,cAAc,CAAC,SAAS;AACzG,QAAI,MAAM;AACR,8BAAwB,IAAI;AAC5B;AAAA,IACF;AACA,QAAI,iBAAiB;AACnB,8BAAwB,IAAI;AAC5B;AAAA,IACF;AACA,4BAAwB,KAAK;AAAA,EAC/B,GACE,+BAAC,iBAAc,eAAY,+BACzB;AAAA,wBAAC,gBACC,8BAAC,eACE,4BACG,EAAE,8CAA8C,oBAAoB,IACpE,EAAE,+BAA+B,mBAAmB,GAC1D,GACF;AAAA,IACA,qBAAC,SAAI,WAAU,qBACb;AAAA,0BAAC,OAAE,WAAU,yBACV,4BACG;AAAA,QACA;AAAA,QACA;AAAA,MACF,IACE,EAAE,qCAAqC,mEAAmE,GAChH;AAAA,MACC,CAAC,kBACF;AAAA,QAAC;AAAA;AAAA,UACC,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,UACA,aAAa,EAAE,wCAAwC,UAAU;AAAA,UACjE,YAAY,EAAE,uCAAuC,SAAS;AAAA;AAAA,MAChE,IACI;AAAA,OACF,OAAO,UAAU,SAAS,UAAU,OAAO,IAC3C,CAAC,kBACD,oBAAC,SAAM,QAAO,eACZ,8BAAC,oBACE;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF,GACF,IACI,OACF;AAAA,MACH,4BACC,oBAAC,SAAM,QAAO,WACZ,8BAAC,oBACE;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF,GACF,IACE;AAAA,MACJ,oBAAC,SAAI,WAAU,kHACb,8BAAC,SAAI,WAAU,oCACd,4BAAkB,OACjB,iCACF;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS,CAAC,UAAU;AAClB,oBAAM,eAAe;AACrB,oBAAM,gBAAgB;AACtB,mBAAK,qBAAqB;AAAA,YAC5B;AAAA,YAEC,YAAE,yCAAyC,iBAAiB;AAAA;AAAA,QAC/D;AAAA,QACC,mBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,CAAC,UAAU;AAClB,oBAAM,eAAe;AACrB,oBAAM,gBAAgB;AACtB,mBAAK,eAAe;AAAA,YACtB;AAAA,YAEC,YAAE,qCAAqC,iBAAiB;AAAA;AAAA,QAC3D,IACE;AAAA,QACJ;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS,CAAC,UAAU;AAClB,oBAAM,eAAe;AACrB,oBAAM,gBAAgB;AACtB,gCAAkB;AAAA,YACpB;AAAA,YAEC,YAAE,sCAAsC,cAAc;AAAA;AAAA,QACzD;AAAA,SACE,GAEF,GACF;AAAA,OACF;AAAA,KACF,GACF;AAGF,QAAM,oBAAoB,MAAM,QAAQ,MAAM;AAC5C,WAAO,kBACJ,IAAI,CAAC,gBAAgB,YAAY,eAAe,KAAK,KAAK,EAAE,EAC5D,OAAO,CAAC,OAAO,OAAO,QAAQ,MAAM,SAAS,KAAK,IAAI,QAAQ,KAAK,MAAM,KAAK,EAC9E,MAAM,GAAG,CAAC;AAAA,EACf,GAAG,CAAC,iBAAiB,CAAC;AAEtB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,yBAA0B;AAC/B,QAAI,CAAC,KAAM;AACX,QAAI,yBAAyB,EAAG;AAChC,gCAA4B,KAAK;AAAA,EACnC,GAAG,CAAC,wBAAwB,MAAM,wBAAwB,CAAC;AAE3D,QAAM,kBAAkB,UAAU,SAAS,eAAe,gBAAgB,IAAI;AAE9E,MAAI,CAAC,kBAAmB,QAAO;AAC/B,MAAI,CAAC,OAAO,KAAM,QAAO;AAEzB,QAAM,yBAAyB,yBAAyB,IACpD,GAAG,sBAAsB,yCACzB;AACJ,QAAM,mBAAmB,CAAC,OACtB,EAAE,uCAAuC,kDAAkD,IAC3F,2BACE,EAAE,yCAAyC,wFAAwF,IACnI,EAAE,2CAA2C,8CAA8C;AACjG,QAAM,gBAAgB,OAAO,qBAAqB,YAAY,iBAAiB,KAAK,EAAE,SAAS,IAC3F,mBACA;AACJ,QAAM,2BAA2B;AAAA,IAC/B,4BACG,yBAAyB,KACzB,CAAC;AAAA,EACN;AAEA,QAAM,aAAa,2BACjB,qBAAC,SAAI,WAAU,6FACb;AAAA,wBAAC,SAAI,WAAU,eACZ,yBACH;AAAA,IACA,qBAAC,SAAI,WAAU,oEACb;AAAA,0BAAC,UAAM,aAAG,EAAE,0CAA0C,cAAc,CAAC,KAAK,sBAAsB,IAAG;AAAA,MAClG,kBAAkB,IAAI,CAAC,UACtB;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UAEV;AAAA,gCAAC,QAAK,WAAU,WAAU;AAAA,YAC1B,oBAAC,UAAM,iBAAM;AAAA;AAAA;AAAA,QAJR;AAAA,MAKP,CACD;AAAA,OACH;AAAA,IACA,qBAAC,SAAI,WAAU,mBACd;AAAA,YAAM,oBAAoB,CAAC,OAC1B;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,SAAS;AAAA,UACT,WAAU;AAAA,UAET,YAAE,iCAAiC,mBAAmB;AAAA;AAAA,MACzD,IACE;AAAA,MACH,2BACC,oBAAC,SAAI,WAAU,QACb;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,SAAS,MAAM,4BAA4B,KAAK;AAAA,UAChD,WAAU;AAAA,UAET,YAAE,gBAAgB,OAAO;AAAA;AAAA,MAC5B,GACF,IACE;AAAA,OACJ;AAAA,KACF,IACE;AAEJ,SACE,iCACG;AAAA,iBAAc,kBAAkB,aAAa,YAAY,eAAe,IAAI,aAAc;AAAA,IAC1F;AAAA,KACH;AAEJ;AAEA,eAAsB,mBACpB,MACA,SAC2B;AAC3B,QAAM,sBAAsB,oBAAoB,OAAO;AACvD,QAAM,oBAAoB,kBAAkB,SAAS,IAAI;AACzD,QAAM,SAAS,cAAc,SAAS,qBAAqB,iBAAiB;AAC5E,QAAM,QAAQ,uBAAuB,MAAM;AAC3C,QAAM,eAAe,OAAO,gBAAgB;AAC5C,QAAM,aAAa,OAAO,cAAc;AACxC,MAAI,CAAC,gBAAgB,CAAC,YAAY;AAChC,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AACA,QAAM,gCAAgC;AAAA,IACpC,OAAO,2BAA2B,QAC/B,OAAO,OAAO,sBAAsB,YACpC,MAAM,sBAAsB;AAAA,EACjC;AACA,MAAI,OAAO,YAAY,CAAC,+BAA+B;AACrD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM,MAAM,QAAQ;AAAA,MACpB,UAAU,MAAM;AAAA,MAChB,mBAAmB,MAAM,qBAAqB;AAAA,IAChD;AAAA,EACF;AACA,QAAM,wBAAwB,QAAQ,OAAO,UAAU,MAAM,OAAO,MAAM,SAAS,EAAE,CAAC;AACtF,QAAM,sBAAsB,OAAO,qBAAqB;AACxD,QAAM,aAAa,wBAAwB,YAAY,CAAC,wBACpD,WACA;AACJ,QAAM,gBAAgB,eAAe,YAAY,CAAC,wBAC9C,SACC,OAAO,qBAAqB,OAAO,UAAU,MAAM;AACxD,QAAM,aAAa,OAAO,aAAa,IAAI,gBAAgB;AAC3D,QAAM,OAAO,MAAM,QAA0B,8BAA8B;AAAA,IACzE,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU;AAAA,MACnB;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,OAAO,OAAO,MAAM,SAAS;AAAA,MAC7B,WAAW,OAAO,qBAAqB,OAAO,MAAM,mBAAmB;AAAA,MACvE;AAAA,MACA;AAAA,MACA,iBAAiB;AAAA,IACnB,CAAC;AAAA,EACH,CAAC;AACD,QAAM,UAAU,KAAK,UAAU,EAAE,IAAI,MAAM;AAC3C,MAAI,QAAQ,IAAI;AACd,UAAM,iBAAiB,eAAe,WAAW,WAAW;AAC5D,UAAM,sCAAsC,mBAAmB;AAC/D,2BAAuB,QAAQ;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,mBAAmB,QAAQ,qBAAqB,OAAO,qBAAqB;AAAA,MAC5E,MAAM,yBAAyB,OAAO,MAAM,QAAQ,IAAI;AAAA,MACxD,UAAU,sCAAuC,OAAO,YAAY,OAAQ;AAAA,MAC5E,mBAAmB,mBAAmB,WAAW,OAAQ,cAAc,OAAO,qBAAqB;AAAA,MACnG,mBAAmB;AAAA,MACnB,wBAAwB,mBAAmB,WAAW,QAAQ,QAAQ,OAAO,sBAAsB;AAAA,IACrG,CAAC;AACD,WAAO;AAAA,EACT;AACA,yBAAuB,QAAQ;AAAA,IAC7B;AAAA,IACA;AAAA,IACA,MAAM,yBAAyB,OAAO,MAAM,QAAQ,IAAI;AAAA,IACxD,UAAU,QAAQ,YAAY,OAAO,YAAY;AAAA,IACjD,mBAAmB,QAAQ,UAAU,MAAM,cAAc,OAAO,qBAAqB;AAAA,IACrF,mBAAmB,eAAe,WAAW,WAAW;AAAA,IACxD,wBAAwB,eAAe,WAAW,QAAQ,QAAQ,OAAO,sBAAsB;AAAA,EACjG,CAAC;AACD,SAAO;AACT;",
|
|
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:
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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(
|
|
143
|
-
|
|
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": "AACA,OAAO,uBAAuB,0BAA0B;
|
|
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.
|
|
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.
|
|
74
|
-
"@open-mercato/core": "0.10.1-develop.
|
|
75
|
-
"@open-mercato/queue": "0.10.1-develop.
|
|
76
|
-
"@open-mercato/scheduler": "0.10.1-develop.
|
|
77
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
78
|
-
"@open-mercato/web-research": "0.10.1-develop.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
1059
|
-
|
|
1092
|
+
if (hasAnotherOwnerForForm()) return
|
|
1093
|
+
const current = getRecordLockFormState(formId)
|
|
1094
|
+
if (current?.lock?.token && current.resourceKind && current.resourceId) {
|
|
1060
1095
|
void releaseLock({
|
|
1061
|
-
|
|
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,
|
|
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:
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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(
|
|
169
|
-
|
|
255
|
+
async onAfterDelete(_data, context) {
|
|
256
|
+
if (context.formId) clearSavedRecordLockState(context.formId)
|
|
170
257
|
},
|
|
171
258
|
async onDeleteError(_data, _context, _error) {
|
|
172
259
|
return
|