@open-mercato/enterprise 0.5.1-develop.2860.07af3a6a9d → 0.5.1-develop.2874.77704bccbd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js +1 -1
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js.map +1 -1
- package/dist/modules/record_locks/widgets/notifications/IncomingChangesRenderer.js +5 -5
- package/dist/modules/record_locks/widgets/notifications/IncomingChangesRenderer.js.map +2 -2
- package/dist/modules/security/backend/security/sudo/page.js +1 -1
- package/dist/modules/security/backend/security/sudo/page.js.map +1 -1
- package/dist/modules/security/components/EnforcementPolicyForm.js +1 -1
- package/dist/modules/security/components/EnforcementPolicyForm.js.map +1 -1
- package/dist/modules/security/components/SudoChallengeModal.js +1 -1
- package/dist/modules/security/components/SudoChallengeModal.js.map +1 -1
- package/package.json +5 -5
- package/src/modules/record_locks/widgets/injection/record-locking/widget.client.tsx +1 -1
- package/src/modules/record_locks/widgets/notifications/IncomingChangesRenderer.tsx +6 -6
- package/src/modules/security/backend/security/sudo/page.tsx +1 -1
- package/src/modules/security/components/EnforcementPolicyForm.tsx +1 -1
- package/src/modules/security/components/SudoChallengeModal.tsx +1 -1
|
@@ -1042,7 +1042,7 @@ function RecordLockingWidget({
|
|
|
1042
1042
|
participantEmails.map((email) => /* @__PURE__ */ jsxs(
|
|
1043
1043
|
"span",
|
|
1044
1044
|
{
|
|
1045
|
-
className: "inline-flex items-center gap-1 rounded-full border border-amber-300 bg-amber-100 px-2 py-0.5 text-
|
|
1045
|
+
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",
|
|
1046
1046
|
children: [
|
|
1047
1047
|
/* @__PURE__ */ jsx(Mail, { className: "h-3 w-3" }),
|
|
1048
1048
|
/* @__PURE__ */ jsx("span", { children: email })
|
|
@@ -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 { 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'\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 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\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\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 console.warn('[RecordLockingWidget] Failed to release lock with keepalive fallback', 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 console.warn('[RecordLockingWidget] Failed to release lock while demoting owner', 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 let payload = extractRecordLockConflictPayload(detail.error)\n const currentState = getRecordLockFormState(formId)\n const eventTargetsCurrentForm = !eventContextId || eventContextId === formId\n if (!eventTargetsCurrentForm) {\n if (!payload || !currentState?.resourceKind || !currentState?.resourceId) return\n const payloadResourceKind = payload.conflict.resourceKind?.trim() ?? ''\n const payloadResourceId = payload.conflict.resourceId?.trim() ?? ''\n if (!payloadResourceKind || !payloadResourceId) return\n if (payloadResourceKind !== currentState.resourceKind || payloadResourceId !== currentState.resourceId) return\n }\n if (!payload) {\n if (!currentState?.resourceKind || !currentState?.resourceId) return\n if (isRecordDeletedError(detail.error)) {\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 (extractErrorStatus(detail.error) === 409) {\n applyConflictPayload(buildFallbackConflict(currentState))\n }\n return\n }\n\n applyConflictPayload(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 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 if (!state?.conflict || !state?.resourceKind || !state?.resourceId) return\n let conflictId: string | undefined = isUuid(state.conflict.id) ? state.conflict.id : undefined\n if (!conflictId) {\n const validation = await validateBeforeSave({}, context)\n conflictId = isUuid(validation.conflict?.id) ? validation.conflict.id : undefined\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 }\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 setRecordLockFormState(formId, {\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n window.location.reload()\n }, [context, formId, state?.conflict, state?.lock?.token, state?.resourceId, state?.resourceKind, t])\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>\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 variant=\"info\">\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 variant=\"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-[11px] 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: payload.lock ?? state?.lock ?? null,\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: payload.lock ?? state?.lock ?? null,\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"],
|
|
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 { 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'\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 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\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\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 console.warn('[RecordLockingWidget] Failed to release lock with keepalive fallback', 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 console.warn('[RecordLockingWidget] Failed to release lock while demoting owner', 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 let payload = extractRecordLockConflictPayload(detail.error)\n const currentState = getRecordLockFormState(formId)\n const eventTargetsCurrentForm = !eventContextId || eventContextId === formId\n if (!eventTargetsCurrentForm) {\n if (!payload || !currentState?.resourceKind || !currentState?.resourceId) return\n const payloadResourceKind = payload.conflict.resourceKind?.trim() ?? ''\n const payloadResourceId = payload.conflict.resourceId?.trim() ?? ''\n if (!payloadResourceKind || !payloadResourceId) return\n if (payloadResourceKind !== currentState.resourceKind || payloadResourceId !== currentState.resourceId) return\n }\n if (!payload) {\n if (!currentState?.resourceKind || !currentState?.resourceId) return\n if (isRecordDeletedError(detail.error)) {\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 (extractErrorStatus(detail.error) === 409) {\n applyConflictPayload(buildFallbackConflict(currentState))\n }\n return\n }\n\n applyConflictPayload(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 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 if (!state?.conflict || !state?.resourceKind || !state?.resourceId) return\n let conflictId: string | undefined = isUuid(state.conflict.id) ? state.conflict.id : undefined\n if (!conflictId) {\n const validation = await validateBeforeSave({}, context)\n conflictId = isUuid(validation.conflict?.id) ? validation.conflict.id : undefined\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 }\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 setRecordLockFormState(formId, {\n conflict: null,\n pendingConflictId: null,\n pendingResolution: 'normal',\n pendingResolutionArmed: false,\n })\n window.location.reload()\n }, [context, formId, state?.conflict, state?.lock?.token, state?.resourceId, state?.resourceKind, t])\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>\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 variant=\"info\">\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 variant=\"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: payload.lock ?? state?.lock ?? null,\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: payload.lock ?? state?.lock ?? null,\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
5
|
"mappings": "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",
|
|
6
6
|
"names": ["currentState", "payload"]
|
|
7
7
|
}
|
|
@@ -24,13 +24,13 @@ function IncomingChangesRenderer({ notification }) {
|
|
|
24
24
|
"div",
|
|
25
25
|
{
|
|
26
26
|
className: cn(
|
|
27
|
-
"group relative px-4 py-3 hover:bg-muted/50 transition-colors border-l-4 border-l-
|
|
28
|
-
isUnread && "bg-
|
|
27
|
+
"group relative px-4 py-3 hover:bg-muted/50 transition-colors border-l-4 border-l-status-info-border",
|
|
28
|
+
isUnread && "bg-status-info-bg"
|
|
29
29
|
),
|
|
30
30
|
children: [
|
|
31
31
|
isUnread ? /* @__PURE__ */ jsx("div", { className: "absolute left-1.5 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full bg-primary" }) : null,
|
|
32
32
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
|
|
33
|
-
/* @__PURE__ */ jsx("div", { className: "mt-0.5 flex-shrink-0", children: /* @__PURE__ */ jsx("div", { className: "h-10 w-10 rounded-lg bg-
|
|
33
|
+
/* @__PURE__ */ jsx("div", { className: "mt-0.5 flex-shrink-0", children: /* @__PURE__ */ jsx("div", { className: "h-10 w-10 rounded-lg bg-status-info-bg flex items-center justify-center", children: /* @__PURE__ */ jsx(GitPullRequestArrow, { className: "h-5 w-5 text-status-info-icon" }) }) }),
|
|
34
34
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
35
35
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
36
36
|
/* @__PURE__ */ jsx("h4", { className: cn("text-sm font-medium", isUnread && "font-semibold"), children: notification.title }),
|
|
@@ -40,12 +40,12 @@ function IncomingChangesRenderer({ notification }) {
|
|
|
40
40
|
] })
|
|
41
41
|
] }),
|
|
42
42
|
/* @__PURE__ */ jsx("div", { className: "mt-2 overflow-x-auto rounded border border-border/70", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
|
|
43
|
-
/* @__PURE__ */ jsx("thead", { className: "bg-muted/
|
|
43
|
+
/* @__PURE__ */ jsx("thead", { className: "bg-muted/50", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
44
44
|
/* @__PURE__ */ jsx("th", { className: "px-2 py-1.5 text-left font-medium", children: t("record_locks.conflict.field", "Field") }),
|
|
45
45
|
/* @__PURE__ */ jsx("th", { className: "px-2 py-1.5 text-left font-medium", children: t("record_locks.conflict.incoming_label", "Incoming") }),
|
|
46
46
|
/* @__PURE__ */ jsx("th", { className: "px-2 py-1.5 text-left font-medium", children: t("record_locks.conflict.current_label", "Current") })
|
|
47
47
|
] }) }),
|
|
48
|
-
/* @__PURE__ */ jsx("tbody", { children: rows.length ? rows.map((row, index) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/
|
|
48
|
+
/* @__PURE__ */ jsx("tbody", { children: rows.length ? rows.map((row, index) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/70", children: [
|
|
49
49
|
/* @__PURE__ */ jsx("td", { className: "px-2 py-1.5 align-top text-foreground", children: row.field }),
|
|
50
50
|
/* @__PURE__ */ jsx("td", { className: "px-2 py-1.5 align-top text-muted-foreground", children: row.incoming }),
|
|
51
51
|
/* @__PURE__ */ jsx("td", { className: "px-2 py-1.5 align-top text-muted-foreground", children: row.current })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/record_locks/widgets/notifications/IncomingChangesRenderer.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { GitPullRequestArrow, Calendar } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { formatRelativeTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\n\ntype ChangeRow = {\n field: string\n incoming: string\n current: string\n}\n\nfunction parseRows(notification: NotificationRendererProps['notification']): ChangeRow[] {\n const raw = notification.bodyVariables?.changedRowsJson\n if (!raw || typeof raw !== 'string') return []\n try {\n const parsed = JSON.parse(raw)\n if (!Array.isArray(parsed)) return []\n return parsed\n .filter((row): row is ChangeRow => (\n row\n && typeof row === 'object'\n && typeof (row as ChangeRow).field === 'string'\n && typeof (row as ChangeRow).incoming === 'string'\n && typeof (row as ChangeRow).current === 'string'\n ))\n .slice(0, 12)\n } catch {\n return []\n }\n}\n\nexport function IncomingChangesRenderer({ notification }: NotificationRendererProps) {\n const t = useT()\n const rows = React.useMemo(() => parseRows(notification), [notification])\n const isUnread = notification.status === 'unread'\n\n return (\n <div\n className={cn(\n 'group relative px-4 py-3 hover:bg-muted/50 transition-colors border-l-4 border-l-
|
|
5
|
-
"mappings": ";AAgDQ,cAeI,YAfJ;AA9CR,YAAY,WAAW;AACvB,SAAS,qBAAqB,gBAAgB;AAC9C,SAAS,UAAU;AACnB,SAAS,0BAA0B;AACnC,SAAS,YAAY;AASrB,SAAS,UAAU,cAAsE;AACvF,QAAM,MAAM,aAAa,eAAe;AACxC,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAC7C,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AACpC,WAAO,OACJ,OAAO,CAAC,QACP,OACG,OAAO,QAAQ,YACf,OAAQ,IAAkB,UAAU,YACpC,OAAQ,IAAkB,aAAa,YACvC,OAAQ,IAAkB,YAAY,QAC1C,EACA,MAAM,GAAG,EAAE;AAAA,EAChB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,wBAAwB,EAAE,aAAa,GAA8B;AACnF,QAAM,IAAI,KAAK;AACf,QAAM,OAAO,MAAM,QAAQ,MAAM,UAAU,YAAY,GAAG,CAAC,YAAY,CAAC;AACxE,QAAM,WAAW,aAAa,WAAW;AAEzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,YAAY;AAAA,MACd;AAAA,MAEC;AAAA,mBACC,oBAAC,SAAI,WAAU,8EAA6E,IAC1F;AAAA,QAEJ,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,SAAI,WAAU,wBACb,8BAAC,SAAI,WAAU,
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { GitPullRequestArrow, Calendar } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { formatRelativeTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\n\ntype ChangeRow = {\n field: string\n incoming: string\n current: string\n}\n\nfunction parseRows(notification: NotificationRendererProps['notification']): ChangeRow[] {\n const raw = notification.bodyVariables?.changedRowsJson\n if (!raw || typeof raw !== 'string') return []\n try {\n const parsed = JSON.parse(raw)\n if (!Array.isArray(parsed)) return []\n return parsed\n .filter((row): row is ChangeRow => (\n row\n && typeof row === 'object'\n && typeof (row as ChangeRow).field === 'string'\n && typeof (row as ChangeRow).incoming === 'string'\n && typeof (row as ChangeRow).current === 'string'\n ))\n .slice(0, 12)\n } catch {\n return []\n }\n}\n\nexport function IncomingChangesRenderer({ notification }: NotificationRendererProps) {\n const t = useT()\n const rows = React.useMemo(() => parseRows(notification), [notification])\n const isUnread = notification.status === 'unread'\n\n return (\n <div\n className={cn(\n 'group relative px-4 py-3 hover:bg-muted/50 transition-colors border-l-4 border-l-status-info-border',\n isUnread && 'bg-status-info-bg',\n )}\n >\n {isUnread ? (\n <div className=\"absolute left-1.5 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full bg-primary\" />\n ) : null}\n\n <div className=\"flex gap-3\">\n <div className=\"mt-0.5 flex-shrink-0\">\n <div className=\"h-10 w-10 rounded-lg bg-status-info-bg flex items-center justify-center\">\n <GitPullRequestArrow className=\"h-5 w-5 text-status-info-icon\" />\n </div>\n </div>\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <h4 className={cn('text-sm font-medium', isUnread && 'font-semibold')}>\n {notification.title}\n </h4>\n <span className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n <Calendar className=\"h-3 w-3\" />\n {formatRelativeTime(notification.createdAt, { translate: t }) ?? ''}\n </span>\n </div>\n\n <div className=\"mt-2 overflow-x-auto rounded border border-border/70\">\n <table className=\"w-full text-xs\">\n <thead className=\"bg-muted/50\">\n <tr>\n <th className=\"px-2 py-1.5 text-left font-medium\">{t('record_locks.conflict.field', 'Field')}</th>\n <th className=\"px-2 py-1.5 text-left font-medium\">{t('record_locks.conflict.incoming_label', 'Incoming')}</th>\n <th className=\"px-2 py-1.5 text-left font-medium\">{t('record_locks.conflict.current_label', 'Current')}</th>\n </tr>\n </thead>\n <tbody>\n {rows.length ? rows.map((row, index) => (\n <tr key={`${row.field}-${index}`} className=\"border-t border-border/70\">\n <td className=\"px-2 py-1.5 align-top text-foreground\">{row.field}</td>\n <td className=\"px-2 py-1.5 align-top text-muted-foreground\">{row.incoming}</td>\n <td className=\"px-2 py-1.5 align-top text-muted-foreground\">{row.current}</td>\n </tr>\n )) : (\n <tr>\n <td className=\"px-2 py-2 text-muted-foreground\" colSpan={3}>\n {t('record_locks.conflict.no_field_details', 'Field-level conflict details are unavailable for this record. Choose a resolution to continue.')}\n </td>\n </tr>\n )}\n </tbody>\n </table>\n </div>\n </div>\n </div>\n </div>\n )\n}\n\nexport default IncomingChangesRenderer\n"],
|
|
5
|
+
"mappings": ";AAgDQ,cAeI,YAfJ;AA9CR,YAAY,WAAW;AACvB,SAAS,qBAAqB,gBAAgB;AAC9C,SAAS,UAAU;AACnB,SAAS,0BAA0B;AACnC,SAAS,YAAY;AASrB,SAAS,UAAU,cAAsE;AACvF,QAAM,MAAM,aAAa,eAAe;AACxC,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAC7C,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AACpC,WAAO,OACJ,OAAO,CAAC,QACP,OACG,OAAO,QAAQ,YACf,OAAQ,IAAkB,UAAU,YACpC,OAAQ,IAAkB,aAAa,YACvC,OAAQ,IAAkB,YAAY,QAC1C,EACA,MAAM,GAAG,EAAE;AAAA,EAChB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,wBAAwB,EAAE,aAAa,GAA8B;AACnF,QAAM,IAAI,KAAK;AACf,QAAM,OAAO,MAAM,QAAQ,MAAM,UAAU,YAAY,GAAG,CAAC,YAAY,CAAC;AACxE,QAAM,WAAW,aAAa,WAAW;AAEzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,YAAY;AAAA,MACd;AAAA,MAEC;AAAA,mBACC,oBAAC,SAAI,WAAU,8EAA6E,IAC1F;AAAA,QAEJ,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,SAAI,WAAU,wBACb,8BAAC,SAAI,WAAU,2EACb,8BAAC,uBAAoB,WAAU,iCAAgC,GACjE,GACF;AAAA,UAEA,qBAAC,SAAI,WAAU,kBACb;AAAA,iCAAC,SAAI,WAAU,0CACb;AAAA,kCAAC,QAAG,WAAW,GAAG,uBAAuB,YAAY,eAAe,GACjE,uBAAa,OAChB;AAAA,cACA,qBAAC,UAAK,WAAU,yDACd;AAAA,oCAAC,YAAS,WAAU,WAAU;AAAA,gBAC7B,mBAAmB,aAAa,WAAW,EAAE,WAAW,EAAE,CAAC,KAAK;AAAA,iBACnE;AAAA,eACF;AAAA,YAEA,oBAAC,SAAI,WAAU,wDACb,+BAAC,WAAM,WAAU,kBACf;AAAA,kCAAC,WAAM,WAAU,eACf,+BAAC,QACC;AAAA,oCAAC,QAAG,WAAU,qCAAqC,YAAE,+BAA+B,OAAO,GAAE;AAAA,gBAC7F,oBAAC,QAAG,WAAU,qCAAqC,YAAE,wCAAwC,UAAU,GAAE;AAAA,gBACzG,oBAAC,QAAG,WAAU,qCAAqC,YAAE,uCAAuC,SAAS,GAAE;AAAA,iBACzG,GACF;AAAA,cACA,oBAAC,WACE,eAAK,SAAS,KAAK,IAAI,CAAC,KAAK,UAC5B,qBAAC,QAAiC,WAAU,6BAC1C;AAAA,oCAAC,QAAG,WAAU,yCAAyC,cAAI,OAAM;AAAA,gBACjE,oBAAC,QAAG,WAAU,+CAA+C,cAAI,UAAS;AAAA,gBAC1E,oBAAC,QAAG,WAAU,+CAA+C,cAAI,SAAQ;AAAA,mBAHlE,GAAG,IAAI,KAAK,IAAI,KAAK,EAI9B,CACD,IACC,oBAAC,QACC,8BAAC,QAAG,WAAU,mCAAkC,SAAS,GACtD,YAAE,0CAA0C,gGAAgG,GAC/I,GACF,GAEJ;AAAA,eACF,GACF;AAAA,aACF;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,kCAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -178,7 +178,7 @@ function SecuritySudoPageInner() {
|
|
|
178
178
|
}
|
|
179
179
|
], [handleDelete, t]);
|
|
180
180
|
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsxs(PageBody, { className: "space-y-6", children: [
|
|
181
|
-
/* @__PURE__ */ jsx("div", { className: "rounded-xl border bg-muted/
|
|
181
|
+
/* @__PURE__ */ jsx("div", { className: "rounded-xl border bg-muted/30 p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
182
182
|
/* @__PURE__ */ jsx(ShieldAlert, { className: "mt-0.5 size-5 text-amber-600" }),
|
|
183
183
|
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
184
184
|
/* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: t("security.admin.sudo.notice.title", "Sensitive operations require re-authentication") }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/security/backend/security/sudo/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Pencil, Plus, ShieldAlert, Trash2 } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ChallengeMethod } from '../../../data/constants'\nimport { SudoProvider } from '../../../components/SudoProvider'\nimport { useSudoChallenge } from '../../../components/hooks/useSudoChallenge'\n\ntype SudoConfigRow = {\n id: string\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n label: string | null\n targetIdentifier: string\n isEnabled: boolean\n isDeveloperDefault: boolean\n ttlSeconds: number\n challengeMethod: ChallengeMethod\n configuredBy: string | null\n createdAt: string\n updatedAt: string\n}\n\ntype SudoConfigListResponse = {\n items: SudoConfigRow[]\n}\n\nfunction renderScopeLabel(row: SudoConfigRow, platformLabel: string) {\n if (!row.tenantId && !row.organizationId) {\n return <span>{platformLabel}</span>\n }\n const tenantLabel = row.tenantName ?? row.tenantId ?? '-'\n if (!row.organizationId) {\n return <span className=\"whitespace-normal break-words\">{tenantLabel}</span>\n }\n const orgLabel = row.organizationName ?? row.organizationId ?? '-'\n return (\n <div className=\"space-y-1 whitespace-normal break-words\">\n <div>{tenantLabel}</div>\n <div className=\"text-muted-foreground\">{orgLabel}</div>\n </div>\n )\n}\n\nfunction SecuritySudoPageInner() {\n const t = useT()\n const { requireSudo } = useSudoChallenge()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: 'security-sudo-management',\n })\n\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [rows, setRows] = React.useState<SudoConfigRow[]>([])\n const [saving, setSaving] = React.useState(false)\n\n const runMutationWithContext = React.useCallback(\n async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {\n return runMutation({\n operation,\n mutationPayload,\n context: { retryLastMutation },\n })\n },\n [retryLastMutation, runMutation],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n const response = await apiCall<SudoConfigListResponse>('/api/security/sudo/configs')\n if (!response.ok || !response.result) {\n setRows([])\n setError(t('security.admin.sudo.errors.load', 'Failed to load sudo configuration.'))\n setLoading(false)\n return\n }\n setRows(Array.isArray(response.result.items) ? response.result.items : [])\n setLoading(false)\n }, [t])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const requestToken = React.useCallback(async () => {\n const sudoToken = await requireSudo('security.sudo.manage')\n if (!sudoToken) {\n flash(t('security.admin.sudo.flash.cancelled', 'Sudo challenge cancelled.'), 'error')\n return null\n }\n return sudoToken\n }, [requireSudo, t])\n\n const handleDelete = React.useCallback(async (row: SudoConfigRow) => {\n const accepted = await confirm({\n title: t('security.admin.sudo.delete.title', 'Delete sudo rule?'),\n text: t('security.admin.sudo.delete.text', 'This removes the selected sudo protection rule.'),\n variant: 'destructive',\n confirmText: t('ui.actions.delete', 'Delete'),\n cancelText: t('ui.actions.cancel', 'Cancel'),\n })\n if (!accepted) return\n\n const sudoToken = await requestToken()\n if (!sudoToken) return\n\n setSaving(true)\n try {\n await runMutationWithContext(\n () =>\n apiCallOrThrow(`/api/security/sudo/configs/${encodeURIComponent(row.id)}`, {\n method: 'DELETE',\n headers: { 'x-sudo-token': sudoToken },\n }),\n { id: row.id },\n )\n flash(t('security.admin.sudo.flash.deleted', 'Sudo configuration deleted.'), 'success')\n await loadRows()\n } catch {\n flash(t('security.admin.sudo.flash.deleteError', 'Failed to delete sudo configuration.'), 'error')\n } finally {\n setSaving(false)\n }\n }, [confirm, loadRows, requestToken, runMutationWithContext, t])\n\n const columns = React.useMemo<ColumnDef<SudoConfigRow>[]>(() => [\n {\n accessorKey: 'label',\n header: t('security.admin.sudo.table.label', 'Label'),\n cell: ({ row }) => row.original.label ?? '\u2014',\n },\n {\n accessorKey: 'targetIdentifier',\n header: t('security.admin.sudo.table.targetIdentifier', 'Target'),\n },\n {\n id: 'scope',\n header: t('security.admin.sudo.table.scope', 'Scope'),\n cell: ({ row }) => renderScopeLabel(row.original, t('security.admin.sudo.scope.platform', 'Platform')),\n },\n {\n accessorKey: 'challengeMethod',\n header: t('security.admin.sudo.table.challengeMethod', 'Method'),\n cell: ({ row }) => t(`security.admin.sudo.challengeMethod.${row.original.challengeMethod}`, row.original.challengeMethod),\n },\n {\n accessorKey: 'ttlSeconds',\n header: t('security.admin.sudo.table.ttl', 'TTL'),\n cell: ({ row }) => t('security.admin.sudo.table.ttlValue', '{value}s', { value: String(row.original.ttlSeconds) }),\n },\n {\n id: 'source',\n header: t('security.admin.sudo.table.source', 'Source'),\n cell: ({ row }) => row.original.isDeveloperDefault\n ? t('security.admin.sudo.source.developer', 'Developer default')\n : t('security.admin.sudo.source.admin', 'Admin override'),\n },\n {\n accessorKey: 'isEnabled',\n header: t('security.admin.sudo.table.status', 'Status'),\n cell: ({ row }) => row.original.isEnabled\n ? t('security.admin.sudo.status.enabled', 'Enabled')\n : t('security.admin.sudo.status.disabled', 'Disabled'),\n },\n {\n id: 'actions',\n header: t('security.admin.sudo.table.actions', 'Actions'),\n cell: ({ row }) => {\n const isDeveloperDefault = row.original.isDeveloperDefault\n return (\n <div className=\"flex items-center gap-2\">\n {isDeveloperDefault ? (\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ui.actions.edit', 'Edit')}\n disabled\n >\n <Pencil className=\"size-4\" />\n </IconButton>\n ) : (\n <IconButton\n asChild\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ui.actions.edit', 'Edit')}\n >\n <Link href={`/backend/security/sudo/${row.original.id}/edit`}>\n <Pencil className=\"size-4\" />\n </Link>\n </IconButton>\n )}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ui.actions.delete', 'Delete')}\n disabled={isDeveloperDefault}\n onClick={() => void handleDelete(row.original)}\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n </div>\n )\n },\n },\n ], [handleDelete, t])\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div className=\"rounded-xl border bg-muted/
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Pencil, Plus, ShieldAlert, Trash2 } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ChallengeMethod } from '../../../data/constants'\nimport { SudoProvider } from '../../../components/SudoProvider'\nimport { useSudoChallenge } from '../../../components/hooks/useSudoChallenge'\n\ntype SudoConfigRow = {\n id: string\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n label: string | null\n targetIdentifier: string\n isEnabled: boolean\n isDeveloperDefault: boolean\n ttlSeconds: number\n challengeMethod: ChallengeMethod\n configuredBy: string | null\n createdAt: string\n updatedAt: string\n}\n\ntype SudoConfigListResponse = {\n items: SudoConfigRow[]\n}\n\nfunction renderScopeLabel(row: SudoConfigRow, platformLabel: string) {\n if (!row.tenantId && !row.organizationId) {\n return <span>{platformLabel}</span>\n }\n const tenantLabel = row.tenantName ?? row.tenantId ?? '-'\n if (!row.organizationId) {\n return <span className=\"whitespace-normal break-words\">{tenantLabel}</span>\n }\n const orgLabel = row.organizationName ?? row.organizationId ?? '-'\n return (\n <div className=\"space-y-1 whitespace-normal break-words\">\n <div>{tenantLabel}</div>\n <div className=\"text-muted-foreground\">{orgLabel}</div>\n </div>\n )\n}\n\nfunction SecuritySudoPageInner() {\n const t = useT()\n const { requireSudo } = useSudoChallenge()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: 'security-sudo-management',\n })\n\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [rows, setRows] = React.useState<SudoConfigRow[]>([])\n const [saving, setSaving] = React.useState(false)\n\n const runMutationWithContext = React.useCallback(\n async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {\n return runMutation({\n operation,\n mutationPayload,\n context: { retryLastMutation },\n })\n },\n [retryLastMutation, runMutation],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n const response = await apiCall<SudoConfigListResponse>('/api/security/sudo/configs')\n if (!response.ok || !response.result) {\n setRows([])\n setError(t('security.admin.sudo.errors.load', 'Failed to load sudo configuration.'))\n setLoading(false)\n return\n }\n setRows(Array.isArray(response.result.items) ? response.result.items : [])\n setLoading(false)\n }, [t])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const requestToken = React.useCallback(async () => {\n const sudoToken = await requireSudo('security.sudo.manage')\n if (!sudoToken) {\n flash(t('security.admin.sudo.flash.cancelled', 'Sudo challenge cancelled.'), 'error')\n return null\n }\n return sudoToken\n }, [requireSudo, t])\n\n const handleDelete = React.useCallback(async (row: SudoConfigRow) => {\n const accepted = await confirm({\n title: t('security.admin.sudo.delete.title', 'Delete sudo rule?'),\n text: t('security.admin.sudo.delete.text', 'This removes the selected sudo protection rule.'),\n variant: 'destructive',\n confirmText: t('ui.actions.delete', 'Delete'),\n cancelText: t('ui.actions.cancel', 'Cancel'),\n })\n if (!accepted) return\n\n const sudoToken = await requestToken()\n if (!sudoToken) return\n\n setSaving(true)\n try {\n await runMutationWithContext(\n () =>\n apiCallOrThrow(`/api/security/sudo/configs/${encodeURIComponent(row.id)}`, {\n method: 'DELETE',\n headers: { 'x-sudo-token': sudoToken },\n }),\n { id: row.id },\n )\n flash(t('security.admin.sudo.flash.deleted', 'Sudo configuration deleted.'), 'success')\n await loadRows()\n } catch {\n flash(t('security.admin.sudo.flash.deleteError', 'Failed to delete sudo configuration.'), 'error')\n } finally {\n setSaving(false)\n }\n }, [confirm, loadRows, requestToken, runMutationWithContext, t])\n\n const columns = React.useMemo<ColumnDef<SudoConfigRow>[]>(() => [\n {\n accessorKey: 'label',\n header: t('security.admin.sudo.table.label', 'Label'),\n cell: ({ row }) => row.original.label ?? '\u2014',\n },\n {\n accessorKey: 'targetIdentifier',\n header: t('security.admin.sudo.table.targetIdentifier', 'Target'),\n },\n {\n id: 'scope',\n header: t('security.admin.sudo.table.scope', 'Scope'),\n cell: ({ row }) => renderScopeLabel(row.original, t('security.admin.sudo.scope.platform', 'Platform')),\n },\n {\n accessorKey: 'challengeMethod',\n header: t('security.admin.sudo.table.challengeMethod', 'Method'),\n cell: ({ row }) => t(`security.admin.sudo.challengeMethod.${row.original.challengeMethod}`, row.original.challengeMethod),\n },\n {\n accessorKey: 'ttlSeconds',\n header: t('security.admin.sudo.table.ttl', 'TTL'),\n cell: ({ row }) => t('security.admin.sudo.table.ttlValue', '{value}s', { value: String(row.original.ttlSeconds) }),\n },\n {\n id: 'source',\n header: t('security.admin.sudo.table.source', 'Source'),\n cell: ({ row }) => row.original.isDeveloperDefault\n ? t('security.admin.sudo.source.developer', 'Developer default')\n : t('security.admin.sudo.source.admin', 'Admin override'),\n },\n {\n accessorKey: 'isEnabled',\n header: t('security.admin.sudo.table.status', 'Status'),\n cell: ({ row }) => row.original.isEnabled\n ? t('security.admin.sudo.status.enabled', 'Enabled')\n : t('security.admin.sudo.status.disabled', 'Disabled'),\n },\n {\n id: 'actions',\n header: t('security.admin.sudo.table.actions', 'Actions'),\n cell: ({ row }) => {\n const isDeveloperDefault = row.original.isDeveloperDefault\n return (\n <div className=\"flex items-center gap-2\">\n {isDeveloperDefault ? (\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ui.actions.edit', 'Edit')}\n disabled\n >\n <Pencil className=\"size-4\" />\n </IconButton>\n ) : (\n <IconButton\n asChild\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ui.actions.edit', 'Edit')}\n >\n <Link href={`/backend/security/sudo/${row.original.id}/edit`}>\n <Pencil className=\"size-4\" />\n </Link>\n </IconButton>\n )}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ui.actions.delete', 'Delete')}\n disabled={isDeveloperDefault}\n onClick={() => void handleDelete(row.original)}\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n </div>\n )\n },\n },\n ], [handleDelete, t])\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div className=\"rounded-xl border bg-muted/30 p-4\">\n <div className=\"flex items-start gap-3\">\n <ShieldAlert className=\"mt-0.5 size-5 text-amber-600\" />\n <div className=\"space-y-1\">\n <h2 className=\"text-sm font-semibold\">\n {t('security.admin.sudo.notice.title', 'Sensitive operations require re-authentication')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'security.admin.sudo.notice.description',\n 'Use sudo rules to require a fresh password or MFA challenge for selected features, routes, modules, or packages.',\n )}\n </p>\n </div>\n </div>\n </div>\n\n <DataTable<SudoConfigRow>\n title={t('security.admin.sudo.title', 'Sudo protection')}\n columns={columns}\n data={rows}\n actions={(\n <Button\n asChild\n variant=\"outline\"\n size=\"sm\"\n >\n <Link href=\"/backend/security/sudo/create\">\n <Plus className=\"mr-2 size-4\" />\n {t('security.admin.sudo.actions.add', 'Add rule')}\n </Link>\n </Button>\n )}\n perspective={{ tableId: 'security.sudo.list' }}\n isLoading={loading}\n error={error ? <span>{error}</span> : null}\n />\n\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n\nexport default function SecuritySudoPage() {\n return (\n <SudoProvider>\n <SecuritySudoPageInner />\n </SudoProvider>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -118,7 +118,7 @@ function AllowedMethodsField({ value, setValue, disabled }) {
|
|
|
118
118
|
if (selectedMethods.includes(method)) return;
|
|
119
119
|
setValue([...selectedMethods, method]);
|
|
120
120
|
}, [disabled, selectedMethods, setValue]);
|
|
121
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-md border bg-muted/
|
|
121
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-md border bg-muted/30 p-3", children: [
|
|
122
122
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
123
123
|
/* @__PURE__ */ jsx(
|
|
124
124
|
Button,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/components/EnforcementPolicyForm.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { OrganizationSelect } from '@open-mercato/core/modules/directory/components/OrganizationSelect'\nimport { TenantSelect } from '@open-mercato/core/modules/directory/components/TenantSelect'\nimport { CrudForm, type CrudCustomFieldRenderProps, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\n\nexport type EnforcementScope = 'platform' | 'tenant' | 'organisation'\n\nexport type EnforcementPolicyFormValue = {\n id?: string\n scope: EnforcementScope\n tenantId: string\n organizationId: string\n isEnforced: boolean\n allowedMethods: string[]\n enforcementDeadline: string\n}\ntype EnforcementPolicyFormProps = {\n value?: Partial<EnforcementPolicyFormValue> | null\n submitting: boolean\n onSubmit: (value: EnforcementPolicyFormValue) => Promise<void> | void\n onCancel: () => void\n backHref: string\n}\n\nconst METHOD_OPTIONS = ['totp', 'passkey', 'otp_email'] as const\nconst METHOD_OPTION_VALUES = [...METHOD_OPTIONS] as [typeof METHOD_OPTIONS[number], ...typeof METHOD_OPTIONS[number][]]\nconst METHOD_DESCRIPTION_KEYS: Record<typeof METHOD_OPTIONS[number], { key: string; fallback: string }> = {\n totp: {\n key: 'security.admin.enforcement.methods.totpDescription',\n fallback: 'Authenticator app code (TOTP).',\n },\n passkey: {\n key: 'security.admin.enforcement.methods.passkeyDescription',\n fallback: 'Biometric or security key passkey.',\n },\n otp_email: {\n key: 'security.admin.enforcement.methods.otpEmailDescription',\n fallback: 'One-time verification code sent by email.',\n },\n}\n\nfunction toDatetimeLocal(value: string | null | undefined): string {\n if (!value) return ''\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return ''\n\n const year = parsed.getFullYear()\n const month = String(parsed.getMonth() + 1).padStart(2, '0')\n const day = String(parsed.getDate()).padStart(2, '0')\n const hours = String(parsed.getHours()).padStart(2, '0')\n const minutes = String(parsed.getMinutes()).padStart(2, '0')\n\n return `${year}-${month}-${day}T${hours}:${minutes}`\n}\n\nfunction ScopeTenantField({ value, setValue, values, disabled }: CrudCustomFieldRenderProps) {\n const t = useT()\n const scope = values?.scope\n if (scope !== 'tenant' && scope !== 'organisation') return null\n\n const normalizedValue = typeof value === 'string' && value.trim().length > 0 ? value : null\n\n return (\n <TenantSelect\n id=\"enforcement-tenant-id\"\n value={normalizedValue}\n onChange={(next) => setValue(next ?? '')}\n disabled={disabled}\n includeEmptyOption\n emptyOptionLabel={t('security.admin.enforcement.form.tenantPlaceholder', 'Select tenant')}\n className=\"h-9 w-full rounded-md border bg-background px-3 text-sm\"\n />\n )\n}\n\nfunction ScopeOrganizationField({ value, setValue, values, disabled }: CrudCustomFieldRenderProps) {\n const t = useT()\n const scope = values?.scope\n const organizationValue = typeof value === 'string' && value.trim().length > 0 ? value : null\n const tenantId = typeof values?.tenantId === 'string' && values?.tenantId?.trim().length > 0 ? values?.tenantId : null\n const previousTenantRef = React.useRef<string | null>(tenantId)\n const hydratedRef = React.useRef(false)\n\n React.useEffect(() => {\n if (!hydratedRef.current) {\n hydratedRef.current = true\n previousTenantRef.current = tenantId\n return\n }\n if (previousTenantRef.current !== tenantId) {\n previousTenantRef.current = tenantId\n setValue('')\n }\n }, [setValue, tenantId])\n\n if (scope !== 'organisation') return null\n\n return (\n <OrganizationSelect\n id=\"enforcement-organization-id\"\n value={organizationValue}\n onChange={(next) => setValue(next ?? '')}\n disabled={disabled || !tenantId}\n includeEmptyOption\n emptyOptionLabel={t('security.admin.enforcement.form.organizationPlaceholder', 'Select organization')}\n tenantId={tenantId}\n className=\"h-9 w-full rounded-md border bg-background px-3 text-sm\"\n />\n )\n}\n\nfunction AllowedMethodsField({ value, setValue, disabled }: CrudCustomFieldRenderProps) {\n const t = useT()\n const selectedMethods = React.useMemo(() => {\n if (!Array.isArray(value)) return []\n return value\n .filter((item): item is typeof METHOD_OPTIONS[number] => typeof item === 'string' && METHOD_OPTIONS.includes(item as typeof METHOD_OPTIONS[number]))\n }, [value])\n const [mode, setMode] = React.useState<'all' | 'restricted'>(() => (selectedMethods.length > 0 ? 'restricted' : 'all'))\n\n React.useEffect(() => {\n setMode(selectedMethods.length > 0 ? 'restricted' : 'all')\n }, [selectedMethods.length])\n\n const setAllMethods = React.useCallback(() => {\n if (disabled) return\n setMode('all')\n setValue([])\n }, [disabled, setValue])\n\n const setRestrictedMethods = React.useCallback(() => {\n if (disabled) return\n setMode('restricted')\n if (selectedMethods.length === 0) {\n setValue([METHOD_OPTIONS[0]])\n }\n }, [disabled, selectedMethods.length, setValue])\n\n const toggleMethod = React.useCallback((method: typeof METHOD_OPTIONS[number], checked: boolean) => {\n if (disabled) return\n if (!checked) {\n if (selectedMethods.length <= 1) return\n setValue(selectedMethods.filter((item) => item !== method))\n return\n }\n if (selectedMethods.includes(method)) return\n setValue([...selectedMethods, method])\n }, [disabled, selectedMethods, setValue])\n\n return (\n <div className=\"space-y-3 rounded-md border bg-muted/20 p-3\">\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant={mode === 'all' ? 'default' : 'outline'}\n onClick={setAllMethods}\n disabled={disabled}\n >\n {t('security.admin.enforcement.form.allowedMethodsAll', 'Allow all methods')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant={mode === 'restricted' ? 'default' : 'outline'}\n onClick={setRestrictedMethods}\n disabled={disabled}\n >\n {t('security.admin.enforcement.form.allowedMethodsRestricted', 'Restrict to selected methods')}\n </Button>\n </div>\n\n {mode === 'restricted' ? (\n <div className=\"space-y-2\">\n {METHOD_OPTIONS.map((method) => {\n const isChecked = selectedMethods.includes(method)\n const isLastSelected = isChecked && selectedMethods.length === 1\n return (\n <label\n key={method}\n className=\"flex items-start gap-3 rounded-md border bg-background px-3 py-2\"\n >\n <Checkbox\n checked={isChecked}\n onCheckedChange={(checked) => toggleMethod(method, checked === true)}\n disabled={disabled || isLastSelected}\n />\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium\">\n {t(`security.admin.enforcement.methods.${method}`, method)}\n </span>\n <span className=\"block text-xs text-muted-foreground\">\n {t(METHOD_DESCRIPTION_KEYS[method].key, METHOD_DESCRIPTION_KEYS[method].fallback)}\n </span>\n </span>\n </label>\n )\n })}\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'security.admin.enforcement.form.allowedMethodsHint',\n 'If no methods are selected, all available methods are allowed.',\n )}\n </p>\n )}\n </div>\n )\n}\n\nexport default function EnforcementPolicyForm({\n value,\n backHref,\n submitting = false,\n onSubmit,\n onCancel,\n}: EnforcementPolicyFormProps) {\n const t = useT()\n const formId = 'security-enforcement-policy-form'\n\n const initialValues = React.useMemo<EnforcementPolicyFormValue>(\n () => ({\n id: value?.id,\n scope: value?.scope === 'tenant' || value?.scope === 'organisation' ? value.scope : 'platform',\n tenantId: value?.tenantId ?? '',\n organizationId: value?.organizationId ?? '',\n isEnforced: value?.isEnforced ?? true,\n enforcementDeadline: toDatetimeLocal(value?.enforcementDeadline),\n allowedMethods: Array.isArray(value?.allowedMethods) ? value.allowedMethods : [],\n }),\n [value],\n )\n const [selectedScope, setSelectedScope] = React.useState<EnforcementScope>(initialValues.scope)\n\n React.useEffect(() => {\n setSelectedScope(initialValues.scope)\n }, [initialValues.scope])\n\n const schema = React.useMemo(\n () =>\n z\n .object({\n scope: z.enum(['platform', 'tenant', 'organisation']),\n tenantId: z.string().optional().default(''),\n organizationId: z.string().optional().default(''),\n isEnforced: z.boolean().default(true),\n allowedMethods: z.array(z.enum(METHOD_OPTION_VALUES)).default([]),\n enforcementDeadline: z.string().optional().default(''),\n })\n .superRefine((values, context) => {\n const tenantId = values.tenantId.trim()\n const organizationId = values.organizationId.trim()\n\n if (values.scope !== 'platform' && !tenantId) {\n context.addIssue({\n code: z.ZodIssueCode.custom,\n message: t(\n 'security.admin.enforcement.form.errors.tenantRequired',\n 'Tenant ID is required for tenant and organisation scopes.',\n ),\n path: ['tenantId'],\n })\n }\n\n if (values.scope === 'organisation' && !organizationId) {\n context.addIssue({\n code: z.ZodIssueCode.custom,\n message: t(\n 'security.admin.enforcement.form.errors.organizationRequired',\n 'Organization ID is required for organisation scope.',\n ),\n path: ['organizationId'],\n })\n }\n }),\n [t],\n )\n\n type EnforcementPolicyCrudValues = z.infer<typeof schema>\n\n const fields = React.useMemo<CrudField[]>(\n () => {\n const baseFields: CrudField[] = [\n {\n id: 'scope',\n label: t('security.admin.enforcement.form.scope', 'Scope'),\n type: 'custom',\n required: true,\n disabled: submitting,\n layout: 'half',\n component: ({ value, setValue, disabled, autoFocus }) => {\n const resolvedScope = value === 'tenant' || value === 'organisation' ? value : 'platform'\n return (\n <select\n className=\"w-full h-9 rounded border px-2 text-sm\"\n value={resolvedScope}\n onChange={(event) => {\n const next = event.target.value === 'tenant' || event.target.value === 'organisation'\n ? event.target.value\n : 'platform'\n setValue(next)\n setSelectedScope(next)\n }}\n autoFocus={autoFocus}\n disabled={disabled}\n data-crud-focus-target=\"\"\n >\n <option value=\"platform\">{t('security.admin.enforcement.scope.platform', 'Platform')}</option>\n <option value=\"tenant\">{t('security.admin.enforcement.scope.tenant', 'Tenant')}</option>\n <option value=\"organisation\">{t('security.admin.enforcement.scope.organisation', 'Organisation')}</option>\n </select>\n )\n },\n },\n {\n id: 'enforcementDeadline',\n label: t('security.admin.enforcement.form.deadline', 'Enforcement deadline'),\n type: 'datetime-local',\n disabled: submitting,\n layout: 'half',\n },\n ]\n\n if (selectedScope !== 'platform') {\n baseFields.push({\n id: 'tenantId',\n label: t('security.admin.enforcement.form.tenant', 'Tenant'),\n type: 'custom',\n component: ScopeTenantField,\n disabled: submitting,\n })\n }\n\n if (selectedScope === 'organisation') {\n baseFields.push({\n id: 'organizationId',\n label: t('security.admin.enforcement.form.organization', 'Organization'),\n type: 'custom',\n component: ScopeOrganizationField,\n disabled: submitting,\n })\n }\n\n baseFields.push(\n {\n id: 'isEnforced',\n label: t('security.admin.enforcement.form.enabled', 'Policy enforced'),\n type: 'checkbox',\n disabled: submitting,\n },\n {\n id: 'allowedMethods',\n label: t('security.admin.enforcement.form.allowedMethods', 'Allowed methods'),\n type: 'custom',\n component: AllowedMethodsField,\n disabled: submitting,\n },\n )\n\n return baseFields\n },\n [selectedScope, submitting, t],\n )\n\n const handleSubmit = React.useCallback(\n async (values: EnforcementPolicyCrudValues) => {\n const tenantId = values.tenantId.trim()\n const organizationId = values.organizationId.trim()\n\n await onSubmit({\n id: value?.id,\n scope: values.scope as EnforcementScope,\n tenantId,\n organizationId,\n isEnforced: values.isEnforced,\n enforcementDeadline: values.enforcementDeadline,\n allowedMethods: values.allowedMethods,\n })\n },\n [onSubmit, value?.id],\n )\n\n return (\n <CrudForm<EnforcementPolicyCrudValues>\n title={\n value?.id\n ? t('security.admin.enforcement.form.titleEdit', 'Edit enforcement policy')\n : t('security.admin.enforcement.form.titleCreate', 'Create enforcement policy')\n }\n backHref={backHref}\n schema={schema}\n formId={formId}\n fields={fields}\n initialValues={initialValues as Partial<EnforcementPolicyCrudValues>}\n hideFooterActions\n embedded={false}\n onSubmit={handleSubmit}\n extraActions={(\n <Button type=\"button\" variant=\"outline\" onClick={onCancel} disabled={submitting}>\n {t('ui.actions.cancel', 'Cancel')}\n </Button>\n )}\n />\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { OrganizationSelect } from '@open-mercato/core/modules/directory/components/OrganizationSelect'\nimport { TenantSelect } from '@open-mercato/core/modules/directory/components/TenantSelect'\nimport { CrudForm, type CrudCustomFieldRenderProps, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\n\nexport type EnforcementScope = 'platform' | 'tenant' | 'organisation'\n\nexport type EnforcementPolicyFormValue = {\n id?: string\n scope: EnforcementScope\n tenantId: string\n organizationId: string\n isEnforced: boolean\n allowedMethods: string[]\n enforcementDeadline: string\n}\ntype EnforcementPolicyFormProps = {\n value?: Partial<EnforcementPolicyFormValue> | null\n submitting: boolean\n onSubmit: (value: EnforcementPolicyFormValue) => Promise<void> | void\n onCancel: () => void\n backHref: string\n}\n\nconst METHOD_OPTIONS = ['totp', 'passkey', 'otp_email'] as const\nconst METHOD_OPTION_VALUES = [...METHOD_OPTIONS] as [typeof METHOD_OPTIONS[number], ...typeof METHOD_OPTIONS[number][]]\nconst METHOD_DESCRIPTION_KEYS: Record<typeof METHOD_OPTIONS[number], { key: string; fallback: string }> = {\n totp: {\n key: 'security.admin.enforcement.methods.totpDescription',\n fallback: 'Authenticator app code (TOTP).',\n },\n passkey: {\n key: 'security.admin.enforcement.methods.passkeyDescription',\n fallback: 'Biometric or security key passkey.',\n },\n otp_email: {\n key: 'security.admin.enforcement.methods.otpEmailDescription',\n fallback: 'One-time verification code sent by email.',\n },\n}\n\nfunction toDatetimeLocal(value: string | null | undefined): string {\n if (!value) return ''\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return ''\n\n const year = parsed.getFullYear()\n const month = String(parsed.getMonth() + 1).padStart(2, '0')\n const day = String(parsed.getDate()).padStart(2, '0')\n const hours = String(parsed.getHours()).padStart(2, '0')\n const minutes = String(parsed.getMinutes()).padStart(2, '0')\n\n return `${year}-${month}-${day}T${hours}:${minutes}`\n}\n\nfunction ScopeTenantField({ value, setValue, values, disabled }: CrudCustomFieldRenderProps) {\n const t = useT()\n const scope = values?.scope\n if (scope !== 'tenant' && scope !== 'organisation') return null\n\n const normalizedValue = typeof value === 'string' && value.trim().length > 0 ? value : null\n\n return (\n <TenantSelect\n id=\"enforcement-tenant-id\"\n value={normalizedValue}\n onChange={(next) => setValue(next ?? '')}\n disabled={disabled}\n includeEmptyOption\n emptyOptionLabel={t('security.admin.enforcement.form.tenantPlaceholder', 'Select tenant')}\n className=\"h-9 w-full rounded-md border bg-background px-3 text-sm\"\n />\n )\n}\n\nfunction ScopeOrganizationField({ value, setValue, values, disabled }: CrudCustomFieldRenderProps) {\n const t = useT()\n const scope = values?.scope\n const organizationValue = typeof value === 'string' && value.trim().length > 0 ? value : null\n const tenantId = typeof values?.tenantId === 'string' && values?.tenantId?.trim().length > 0 ? values?.tenantId : null\n const previousTenantRef = React.useRef<string | null>(tenantId)\n const hydratedRef = React.useRef(false)\n\n React.useEffect(() => {\n if (!hydratedRef.current) {\n hydratedRef.current = true\n previousTenantRef.current = tenantId\n return\n }\n if (previousTenantRef.current !== tenantId) {\n previousTenantRef.current = tenantId\n setValue('')\n }\n }, [setValue, tenantId])\n\n if (scope !== 'organisation') return null\n\n return (\n <OrganizationSelect\n id=\"enforcement-organization-id\"\n value={organizationValue}\n onChange={(next) => setValue(next ?? '')}\n disabled={disabled || !tenantId}\n includeEmptyOption\n emptyOptionLabel={t('security.admin.enforcement.form.organizationPlaceholder', 'Select organization')}\n tenantId={tenantId}\n className=\"h-9 w-full rounded-md border bg-background px-3 text-sm\"\n />\n )\n}\n\nfunction AllowedMethodsField({ value, setValue, disabled }: CrudCustomFieldRenderProps) {\n const t = useT()\n const selectedMethods = React.useMemo(() => {\n if (!Array.isArray(value)) return []\n return value\n .filter((item): item is typeof METHOD_OPTIONS[number] => typeof item === 'string' && METHOD_OPTIONS.includes(item as typeof METHOD_OPTIONS[number]))\n }, [value])\n const [mode, setMode] = React.useState<'all' | 'restricted'>(() => (selectedMethods.length > 0 ? 'restricted' : 'all'))\n\n React.useEffect(() => {\n setMode(selectedMethods.length > 0 ? 'restricted' : 'all')\n }, [selectedMethods.length])\n\n const setAllMethods = React.useCallback(() => {\n if (disabled) return\n setMode('all')\n setValue([])\n }, [disabled, setValue])\n\n const setRestrictedMethods = React.useCallback(() => {\n if (disabled) return\n setMode('restricted')\n if (selectedMethods.length === 0) {\n setValue([METHOD_OPTIONS[0]])\n }\n }, [disabled, selectedMethods.length, setValue])\n\n const toggleMethod = React.useCallback((method: typeof METHOD_OPTIONS[number], checked: boolean) => {\n if (disabled) return\n if (!checked) {\n if (selectedMethods.length <= 1) return\n setValue(selectedMethods.filter((item) => item !== method))\n return\n }\n if (selectedMethods.includes(method)) return\n setValue([...selectedMethods, method])\n }, [disabled, selectedMethods, setValue])\n\n return (\n <div className=\"space-y-3 rounded-md border bg-muted/30 p-3\">\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant={mode === 'all' ? 'default' : 'outline'}\n onClick={setAllMethods}\n disabled={disabled}\n >\n {t('security.admin.enforcement.form.allowedMethodsAll', 'Allow all methods')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant={mode === 'restricted' ? 'default' : 'outline'}\n onClick={setRestrictedMethods}\n disabled={disabled}\n >\n {t('security.admin.enforcement.form.allowedMethodsRestricted', 'Restrict to selected methods')}\n </Button>\n </div>\n\n {mode === 'restricted' ? (\n <div className=\"space-y-2\">\n {METHOD_OPTIONS.map((method) => {\n const isChecked = selectedMethods.includes(method)\n const isLastSelected = isChecked && selectedMethods.length === 1\n return (\n <label\n key={method}\n className=\"flex items-start gap-3 rounded-md border bg-background px-3 py-2\"\n >\n <Checkbox\n checked={isChecked}\n onCheckedChange={(checked) => toggleMethod(method, checked === true)}\n disabled={disabled || isLastSelected}\n />\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium\">\n {t(`security.admin.enforcement.methods.${method}`, method)}\n </span>\n <span className=\"block text-xs text-muted-foreground\">\n {t(METHOD_DESCRIPTION_KEYS[method].key, METHOD_DESCRIPTION_KEYS[method].fallback)}\n </span>\n </span>\n </label>\n )\n })}\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'security.admin.enforcement.form.allowedMethodsHint',\n 'If no methods are selected, all available methods are allowed.',\n )}\n </p>\n )}\n </div>\n )\n}\n\nexport default function EnforcementPolicyForm({\n value,\n backHref,\n submitting = false,\n onSubmit,\n onCancel,\n}: EnforcementPolicyFormProps) {\n const t = useT()\n const formId = 'security-enforcement-policy-form'\n\n const initialValues = React.useMemo<EnforcementPolicyFormValue>(\n () => ({\n id: value?.id,\n scope: value?.scope === 'tenant' || value?.scope === 'organisation' ? value.scope : 'platform',\n tenantId: value?.tenantId ?? '',\n organizationId: value?.organizationId ?? '',\n isEnforced: value?.isEnforced ?? true,\n enforcementDeadline: toDatetimeLocal(value?.enforcementDeadline),\n allowedMethods: Array.isArray(value?.allowedMethods) ? value.allowedMethods : [],\n }),\n [value],\n )\n const [selectedScope, setSelectedScope] = React.useState<EnforcementScope>(initialValues.scope)\n\n React.useEffect(() => {\n setSelectedScope(initialValues.scope)\n }, [initialValues.scope])\n\n const schema = React.useMemo(\n () =>\n z\n .object({\n scope: z.enum(['platform', 'tenant', 'organisation']),\n tenantId: z.string().optional().default(''),\n organizationId: z.string().optional().default(''),\n isEnforced: z.boolean().default(true),\n allowedMethods: z.array(z.enum(METHOD_OPTION_VALUES)).default([]),\n enforcementDeadline: z.string().optional().default(''),\n })\n .superRefine((values, context) => {\n const tenantId = values.tenantId.trim()\n const organizationId = values.organizationId.trim()\n\n if (values.scope !== 'platform' && !tenantId) {\n context.addIssue({\n code: z.ZodIssueCode.custom,\n message: t(\n 'security.admin.enforcement.form.errors.tenantRequired',\n 'Tenant ID is required for tenant and organisation scopes.',\n ),\n path: ['tenantId'],\n })\n }\n\n if (values.scope === 'organisation' && !organizationId) {\n context.addIssue({\n code: z.ZodIssueCode.custom,\n message: t(\n 'security.admin.enforcement.form.errors.organizationRequired',\n 'Organization ID is required for organisation scope.',\n ),\n path: ['organizationId'],\n })\n }\n }),\n [t],\n )\n\n type EnforcementPolicyCrudValues = z.infer<typeof schema>\n\n const fields = React.useMemo<CrudField[]>(\n () => {\n const baseFields: CrudField[] = [\n {\n id: 'scope',\n label: t('security.admin.enforcement.form.scope', 'Scope'),\n type: 'custom',\n required: true,\n disabled: submitting,\n layout: 'half',\n component: ({ value, setValue, disabled, autoFocus }) => {\n const resolvedScope = value === 'tenant' || value === 'organisation' ? value : 'platform'\n return (\n <select\n className=\"w-full h-9 rounded border px-2 text-sm\"\n value={resolvedScope}\n onChange={(event) => {\n const next = event.target.value === 'tenant' || event.target.value === 'organisation'\n ? event.target.value\n : 'platform'\n setValue(next)\n setSelectedScope(next)\n }}\n autoFocus={autoFocus}\n disabled={disabled}\n data-crud-focus-target=\"\"\n >\n <option value=\"platform\">{t('security.admin.enforcement.scope.platform', 'Platform')}</option>\n <option value=\"tenant\">{t('security.admin.enforcement.scope.tenant', 'Tenant')}</option>\n <option value=\"organisation\">{t('security.admin.enforcement.scope.organisation', 'Organisation')}</option>\n </select>\n )\n },\n },\n {\n id: 'enforcementDeadline',\n label: t('security.admin.enforcement.form.deadline', 'Enforcement deadline'),\n type: 'datetime-local',\n disabled: submitting,\n layout: 'half',\n },\n ]\n\n if (selectedScope !== 'platform') {\n baseFields.push({\n id: 'tenantId',\n label: t('security.admin.enforcement.form.tenant', 'Tenant'),\n type: 'custom',\n component: ScopeTenantField,\n disabled: submitting,\n })\n }\n\n if (selectedScope === 'organisation') {\n baseFields.push({\n id: 'organizationId',\n label: t('security.admin.enforcement.form.organization', 'Organization'),\n type: 'custom',\n component: ScopeOrganizationField,\n disabled: submitting,\n })\n }\n\n baseFields.push(\n {\n id: 'isEnforced',\n label: t('security.admin.enforcement.form.enabled', 'Policy enforced'),\n type: 'checkbox',\n disabled: submitting,\n },\n {\n id: 'allowedMethods',\n label: t('security.admin.enforcement.form.allowedMethods', 'Allowed methods'),\n type: 'custom',\n component: AllowedMethodsField,\n disabled: submitting,\n },\n )\n\n return baseFields\n },\n [selectedScope, submitting, t],\n )\n\n const handleSubmit = React.useCallback(\n async (values: EnforcementPolicyCrudValues) => {\n const tenantId = values.tenantId.trim()\n const organizationId = values.organizationId.trim()\n\n await onSubmit({\n id: value?.id,\n scope: values.scope as EnforcementScope,\n tenantId,\n organizationId,\n isEnforced: values.isEnforced,\n enforcementDeadline: values.enforcementDeadline,\n allowedMethods: values.allowedMethods,\n })\n },\n [onSubmit, value?.id],\n )\n\n return (\n <CrudForm<EnforcementPolicyCrudValues>\n title={\n value?.id\n ? t('security.admin.enforcement.form.titleEdit', 'Edit enforcement policy')\n : t('security.admin.enforcement.form.titleCreate', 'Create enforcement policy')\n }\n backHref={backHref}\n schema={schema}\n formId={formId}\n fields={fields}\n initialValues={initialValues as Partial<EnforcementPolicyCrudValues>}\n hideFooterActions\n embedded={false}\n onSubmit={handleSubmit}\n extraActions={(\n <Button type=\"button\" variant=\"outline\" onClick={onCancel} disabled={submitting}>\n {t('ui.actions.cancel', 'Cancel')}\n </Button>\n )}\n />\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["value"]
|
|
7
7
|
}
|
|
@@ -161,7 +161,7 @@ function SudoChallengeModal({
|
|
|
161
161
|
}, children: /* @__PURE__ */ jsxs(
|
|
162
162
|
DialogContent,
|
|
163
163
|
{
|
|
164
|
-
className: "sm:max-w-lg [&_[data-dialog-close]]:rounded-full [&_[data-dialog-close]]:border [&_[data-dialog-close]]:border-white/20 [&_[data-dialog-close]]:bg-white/5 [&_[data-dialog-close]]:opacity-100 [&_[data-dialog-close]]:transition-none [&_[data-dialog-close]]:hover:bg-white/10 [&_[data-dialog-close]]:hover:opacity-100 [&_[data-dialog-close]]:focus:ring-0 [&_[data-dialog-close]]:focus:ring-offset-0",
|
|
164
|
+
className: "sm:max-w-lg [&_[data-dialog-close]]:rounded-full [&_[data-dialog-close]]:border [&_[data-dialog-close]]:border-white/20 [&_[data-dialog-close]]:bg-white/5 [&_[data-dialog-close]]:opacity-100 [&_[data-dialog-close]]:transition-none [&_[data-dialog-close]]:hover:bg-white/10 [&_[data-dialog-close]]:hover:opacity-100 [&_[data-dialog-close]]:focus-visible:ring-0 [&_[data-dialog-close]]:focus-visible:ring-offset-0",
|
|
165
165
|
onKeyDown: (event) => {
|
|
166
166
|
if ((event.metaKey || event.ctrlKey) && event.key === "Enter" && challenge?.method === "password") {
|
|
167
167
|
event.preventDefault();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/components/SudoChallengeModal.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { AlertTriangle, ChevronDown, ChevronUp } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useProviderChallengeComponent } from './mfa-ui-registry'\n\nexport type SudoChallengeMethod = {\n type: string\n label: string\n icon: string\n components?: {\n list?: string\n details?: string\n challenge?: string\n }\n}\n\nexport type PendingSudoChallenge = {\n sessionId: string\n targetIdentifier: string\n method: 'password' | 'mfa'\n availableMfaMethods: SudoChallengeMethod[]\n}\n\ntype SudoChallengeModalProps = {\n open: boolean\n challenge: PendingSudoChallenge | null\n onResolve: (result: { sudoToken: string; expiresAt: string } | null) => void\n}\n\ntype PrepareResponse = {\n clientData?: Record<string, unknown>\n}\n\ntype VerifyResponse = {\n sudoToken: string\n expiresAt: string\n}\n\nfunction readErrorMessage(error: unknown, fallback: string): string {\n if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {\n return error.message\n }\n return fallback\n}\n\nexport default function SudoChallengeModal({\n open,\n challenge,\n onResolve,\n}: SudoChallengeModalProps) {\n const t = useT()\n const [password, setPassword] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [preparedMethods, setPreparedMethods] = React.useState<Record<string, true>>({})\n const [preparedClientData, setPreparedClientData] = React.useState<Record<string, Record<string, unknown> | undefined>>({})\n const [selectedMethod, setSelectedMethod] = React.useState<string>('')\n const [showMoreOptions, setShowMoreOptions] = React.useState(false)\n\n React.useEffect(() => {\n if (!open || !challenge) return\n setPassword('')\n setError(null)\n setPreparedMethods({})\n setPreparedClientData({})\n setSelectedMethod(challenge.availableMfaMethods[0]?.type ?? '')\n setShowMoreOptions(false)\n }, [challenge, open])\n\n const prepare = React.useCallback(async (methodType: string) => {\n if (!challenge) return undefined\n const result = await readApiResultOrThrow<PrepareResponse>('/api/security/sudo/prepare', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n sessionId: challenge.sessionId,\n methodType,\n }),\n })\n return result.clientData\n }, [challenge])\n\n React.useEffect(() => {\n if (!challenge || challenge.method !== 'mfa' || !selectedMethod) return\n if (preparedMethods[selectedMethod]) return\n\n let active = true\n setLoading(true)\n setError(null)\n\n prepare(selectedMethod)\n .then((clientData) => {\n if (!active) return\n setPreparedClientData((current) => ({ ...current, [selectedMethod]: clientData }))\n setPreparedMethods((current) => ({ ...current, [selectedMethod]: true }))\n })\n .catch((err) => {\n if (!active) return\n setError(\n readErrorMessage(\n err,\n t('security.admin.sudo.challenge.errors.prepare', 'Failed to prepare sudo challenge.'),\n ),\n )\n })\n .finally(() => {\n if (!active) return\n setLoading(false)\n })\n\n return () => {\n active = false\n }\n }, [challenge, prepare, preparedMethods, selectedMethod, t])\n\n const verify = React.useCallback(async (methodType: string, payload: Record<string, unknown>) => {\n if (!challenge) return\n const result = await readApiResultOrThrow<VerifyResponse>('/api/security/sudo/verify', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n sessionId: challenge.sessionId,\n targetIdentifier: challenge.targetIdentifier,\n methodType,\n payload,\n }),\n })\n onResolve(result)\n }, [challenge, onResolve])\n\n const handlePasswordVerify = React.useCallback(async () => {\n if (!challenge) return\n setLoading(true)\n setError(null)\n try {\n await verify('password', { password })\n } catch (err) {\n setError(\n readErrorMessage(\n err,\n t('security.admin.sudo.challenge.errors.verify', 'Failed to verify sudo challenge.'),\n ),\n )\n } finally {\n setLoading(false)\n }\n }, [challenge, password, t, verify])\n\n const handleMfaVerify = React.useCallback(async (payload: Record<string, unknown>) => {\n if (!selectedMethod) return\n setLoading(true)\n setError(null)\n try {\n await verify(selectedMethod, payload)\n } catch (err) {\n setError(\n readErrorMessage(\n err,\n t('security.admin.sudo.challenge.errors.verify', 'Failed to verify sudo challenge.'),\n ),\n )\n } finally {\n setLoading(false)\n }\n }, [selectedMethod, t, verify])\n\n const handlePrepare = React.useCallback(async () => {\n if (!selectedMethod) return\n const cached = preparedClientData[selectedMethod]\n if (preparedMethods[selectedMethod]) {\n return cached\n }\n\n setLoading(true)\n setError(null)\n try {\n const clientData = await prepare(selectedMethod)\n setPreparedClientData((current) => ({ ...current, [selectedMethod]: clientData }))\n setPreparedMethods((current) => ({ ...current, [selectedMethod]: true }))\n return clientData\n } catch (err) {\n setError(\n readErrorMessage(\n err,\n t('security.admin.sudo.challenge.errors.prepare', 'Failed to prepare sudo challenge.'),\n ),\n )\n } finally {\n setLoading(false)\n }\n return undefined\n }, [prepare, preparedClientData, preparedMethods, selectedMethod, t])\n\n const handleClose = React.useCallback(() => {\n onResolve(null)\n }, [onResolve])\n\n const selected = challenge?.availableMfaMethods.find((method) => method.type === selectedMethod) ?? null\n const alternativeMethods = React.useMemo(\n () => challenge?.availableMfaMethods.filter((method) => method.type !== selectedMethod) ?? [],\n [challenge, selectedMethod],\n )\n const ChallengeComponent = useProviderChallengeComponent(selected ?? { type: 'unknown' })\n\n return (\n <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) handleClose() }}>\n <DialogContent\n className=\"sm:max-w-lg [&_[data-dialog-close]]:rounded-full [&_[data-dialog-close]]:border [&_[data-dialog-close]]:border-white/20 [&_[data-dialog-close]]:bg-white/5 [&_[data-dialog-close]]:opacity-100 [&_[data-dialog-close]]:transition-none [&_[data-dialog-close]]:hover:bg-white/10 [&_[data-dialog-close]]:hover:opacity-100 [&_[data-dialog-close]]:focus:ring-0 [&_[data-dialog-close]]:focus:ring-offset-0\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter' && challenge?.method === 'password') {\n event.preventDefault()\n void handlePasswordVerify()\n }\n }}\n >\n <DialogHeader>\n <DialogTitle>{t('security.admin.sudo.challenge.title', 'Confirm sensitive action')}</DialogTitle>\n <DialogDescription>\n {t(\n 'security.admin.sudo.challenge.description',\n 'Re-authenticate to continue with this protected action.',\n )}\n </DialogDescription>\n </DialogHeader>\n\n {challenge?.method === 'password' ? (\n <div className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"sudo-password\">\n {t('security.admin.sudo.challenge.password.label', 'Password')}\n </label>\n <Input\n id=\"sudo-password\"\n type=\"password\"\n value={password}\n autoFocus\n onChange={(event) => setPassword(event.target.value)}\n placeholder={t('security.admin.sudo.challenge.password.placeholder', 'Enter your password')}\n />\n </div>\n\n {error ? <p className=\"text-sm text-red-500\" role=\"alert\">{error}</p> : null}\n\n <div className=\"space-y-3\">\n <Button\n type=\"button\"\n className=\"w-full\"\n onClick={() => void handlePasswordVerify()}\n disabled={loading || password.trim().length === 0}\n >\n {t('security.admin.sudo.challenge.actions.verify', 'Verify')}\n </Button>\n <div className=\"flex justify-center\">\n <Button type=\"button\" variant=\"outline\" className=\"min-w-28\" onClick={handleClose} disabled={loading}>\n {t('ui.actions.cancel', 'Cancel')}\n </Button>\n </div>\n </div>\n </div>\n ) : challenge?.method === 'mfa' && selected ? (\n <div className=\"space-y-4\">\n <ChallengeComponent\n method={selected}\n loading={loading}\n onVerify={handleMfaVerify}\n onPrepare={handlePrepare}\n onResend={handlePrepare}\n submitLabel={t('security.admin.sudo.challenge.actions.verify', 'Verify')}\n />\n\n {alternativeMethods.length > 0 ? (\n <div className=\"space-y-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"w-full justify-center\"\n onClick={() => setShowMoreOptions((current) => !current)}\n >\n {t('security.admin.sudo.challenge.actions.moreOptions', 'Use another method')}\n {showMoreOptions ? <ChevronUp className=\"ml-2 size-4\" /> : <ChevronDown className=\"ml-2 size-4\" />}\n </Button>\n {showMoreOptions ? (\n <div className=\"space-y-2\">\n {alternativeMethods.map((method) => (\n <Button\n key={`${challenge.sessionId}:${method.type}`}\n type=\"button\"\n variant=\"outline\"\n className=\"w-full justify-center\"\n onClick={() => {\n setSelectedMethod(method.type)\n setError(null)\n setShowMoreOptions(false)\n }}\n >\n {method.label}\n </Button>\n ))}\n </div>\n ) : null}\n </div>\n ) : null}\n\n {error ? <p className=\"text-sm text-red-500\" role=\"alert\">{error}</p> : null}\n\n </div>\n ) : (\n <div className=\"flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900\">\n <AlertTriangle className=\"size-4\" />\n <span>{t('security.admin.sudo.challenge.noMethods', 'No sudo authentication methods are available.')}</span>\n </div>\n )}\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { AlertTriangle, ChevronDown, ChevronUp } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useProviderChallengeComponent } from './mfa-ui-registry'\n\nexport type SudoChallengeMethod = {\n type: string\n label: string\n icon: string\n components?: {\n list?: string\n details?: string\n challenge?: string\n }\n}\n\nexport type PendingSudoChallenge = {\n sessionId: string\n targetIdentifier: string\n method: 'password' | 'mfa'\n availableMfaMethods: SudoChallengeMethod[]\n}\n\ntype SudoChallengeModalProps = {\n open: boolean\n challenge: PendingSudoChallenge | null\n onResolve: (result: { sudoToken: string; expiresAt: string } | null) => void\n}\n\ntype PrepareResponse = {\n clientData?: Record<string, unknown>\n}\n\ntype VerifyResponse = {\n sudoToken: string\n expiresAt: string\n}\n\nfunction readErrorMessage(error: unknown, fallback: string): string {\n if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {\n return error.message\n }\n return fallback\n}\n\nexport default function SudoChallengeModal({\n open,\n challenge,\n onResolve,\n}: SudoChallengeModalProps) {\n const t = useT()\n const [password, setPassword] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [preparedMethods, setPreparedMethods] = React.useState<Record<string, true>>({})\n const [preparedClientData, setPreparedClientData] = React.useState<Record<string, Record<string, unknown> | undefined>>({})\n const [selectedMethod, setSelectedMethod] = React.useState<string>('')\n const [showMoreOptions, setShowMoreOptions] = React.useState(false)\n\n React.useEffect(() => {\n if (!open || !challenge) return\n setPassword('')\n setError(null)\n setPreparedMethods({})\n setPreparedClientData({})\n setSelectedMethod(challenge.availableMfaMethods[0]?.type ?? '')\n setShowMoreOptions(false)\n }, [challenge, open])\n\n const prepare = React.useCallback(async (methodType: string) => {\n if (!challenge) return undefined\n const result = await readApiResultOrThrow<PrepareResponse>('/api/security/sudo/prepare', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n sessionId: challenge.sessionId,\n methodType,\n }),\n })\n return result.clientData\n }, [challenge])\n\n React.useEffect(() => {\n if (!challenge || challenge.method !== 'mfa' || !selectedMethod) return\n if (preparedMethods[selectedMethod]) return\n\n let active = true\n setLoading(true)\n setError(null)\n\n prepare(selectedMethod)\n .then((clientData) => {\n if (!active) return\n setPreparedClientData((current) => ({ ...current, [selectedMethod]: clientData }))\n setPreparedMethods((current) => ({ ...current, [selectedMethod]: true }))\n })\n .catch((err) => {\n if (!active) return\n setError(\n readErrorMessage(\n err,\n t('security.admin.sudo.challenge.errors.prepare', 'Failed to prepare sudo challenge.'),\n ),\n )\n })\n .finally(() => {\n if (!active) return\n setLoading(false)\n })\n\n return () => {\n active = false\n }\n }, [challenge, prepare, preparedMethods, selectedMethod, t])\n\n const verify = React.useCallback(async (methodType: string, payload: Record<string, unknown>) => {\n if (!challenge) return\n const result = await readApiResultOrThrow<VerifyResponse>('/api/security/sudo/verify', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n sessionId: challenge.sessionId,\n targetIdentifier: challenge.targetIdentifier,\n methodType,\n payload,\n }),\n })\n onResolve(result)\n }, [challenge, onResolve])\n\n const handlePasswordVerify = React.useCallback(async () => {\n if (!challenge) return\n setLoading(true)\n setError(null)\n try {\n await verify('password', { password })\n } catch (err) {\n setError(\n readErrorMessage(\n err,\n t('security.admin.sudo.challenge.errors.verify', 'Failed to verify sudo challenge.'),\n ),\n )\n } finally {\n setLoading(false)\n }\n }, [challenge, password, t, verify])\n\n const handleMfaVerify = React.useCallback(async (payload: Record<string, unknown>) => {\n if (!selectedMethod) return\n setLoading(true)\n setError(null)\n try {\n await verify(selectedMethod, payload)\n } catch (err) {\n setError(\n readErrorMessage(\n err,\n t('security.admin.sudo.challenge.errors.verify', 'Failed to verify sudo challenge.'),\n ),\n )\n } finally {\n setLoading(false)\n }\n }, [selectedMethod, t, verify])\n\n const handlePrepare = React.useCallback(async () => {\n if (!selectedMethod) return\n const cached = preparedClientData[selectedMethod]\n if (preparedMethods[selectedMethod]) {\n return cached\n }\n\n setLoading(true)\n setError(null)\n try {\n const clientData = await prepare(selectedMethod)\n setPreparedClientData((current) => ({ ...current, [selectedMethod]: clientData }))\n setPreparedMethods((current) => ({ ...current, [selectedMethod]: true }))\n return clientData\n } catch (err) {\n setError(\n readErrorMessage(\n err,\n t('security.admin.sudo.challenge.errors.prepare', 'Failed to prepare sudo challenge.'),\n ),\n )\n } finally {\n setLoading(false)\n }\n return undefined\n }, [prepare, preparedClientData, preparedMethods, selectedMethod, t])\n\n const handleClose = React.useCallback(() => {\n onResolve(null)\n }, [onResolve])\n\n const selected = challenge?.availableMfaMethods.find((method) => method.type === selectedMethod) ?? null\n const alternativeMethods = React.useMemo(\n () => challenge?.availableMfaMethods.filter((method) => method.type !== selectedMethod) ?? [],\n [challenge, selectedMethod],\n )\n const ChallengeComponent = useProviderChallengeComponent(selected ?? { type: 'unknown' })\n\n return (\n <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) handleClose() }}>\n <DialogContent\n className=\"sm:max-w-lg [&_[data-dialog-close]]:rounded-full [&_[data-dialog-close]]:border [&_[data-dialog-close]]:border-white/20 [&_[data-dialog-close]]:bg-white/5 [&_[data-dialog-close]]:opacity-100 [&_[data-dialog-close]]:transition-none [&_[data-dialog-close]]:hover:bg-white/10 [&_[data-dialog-close]]:hover:opacity-100 [&_[data-dialog-close]]:focus-visible:ring-0 [&_[data-dialog-close]]:focus-visible:ring-offset-0\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter' && challenge?.method === 'password') {\n event.preventDefault()\n void handlePasswordVerify()\n }\n }}\n >\n <DialogHeader>\n <DialogTitle>{t('security.admin.sudo.challenge.title', 'Confirm sensitive action')}</DialogTitle>\n <DialogDescription>\n {t(\n 'security.admin.sudo.challenge.description',\n 'Re-authenticate to continue with this protected action.',\n )}\n </DialogDescription>\n </DialogHeader>\n\n {challenge?.method === 'password' ? (\n <div className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"sudo-password\">\n {t('security.admin.sudo.challenge.password.label', 'Password')}\n </label>\n <Input\n id=\"sudo-password\"\n type=\"password\"\n value={password}\n autoFocus\n onChange={(event) => setPassword(event.target.value)}\n placeholder={t('security.admin.sudo.challenge.password.placeholder', 'Enter your password')}\n />\n </div>\n\n {error ? <p className=\"text-sm text-red-500\" role=\"alert\">{error}</p> : null}\n\n <div className=\"space-y-3\">\n <Button\n type=\"button\"\n className=\"w-full\"\n onClick={() => void handlePasswordVerify()}\n disabled={loading || password.trim().length === 0}\n >\n {t('security.admin.sudo.challenge.actions.verify', 'Verify')}\n </Button>\n <div className=\"flex justify-center\">\n <Button type=\"button\" variant=\"outline\" className=\"min-w-28\" onClick={handleClose} disabled={loading}>\n {t('ui.actions.cancel', 'Cancel')}\n </Button>\n </div>\n </div>\n </div>\n ) : challenge?.method === 'mfa' && selected ? (\n <div className=\"space-y-4\">\n <ChallengeComponent\n method={selected}\n loading={loading}\n onVerify={handleMfaVerify}\n onPrepare={handlePrepare}\n onResend={handlePrepare}\n submitLabel={t('security.admin.sudo.challenge.actions.verify', 'Verify')}\n />\n\n {alternativeMethods.length > 0 ? (\n <div className=\"space-y-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"w-full justify-center\"\n onClick={() => setShowMoreOptions((current) => !current)}\n >\n {t('security.admin.sudo.challenge.actions.moreOptions', 'Use another method')}\n {showMoreOptions ? <ChevronUp className=\"ml-2 size-4\" /> : <ChevronDown className=\"ml-2 size-4\" />}\n </Button>\n {showMoreOptions ? (\n <div className=\"space-y-2\">\n {alternativeMethods.map((method) => (\n <Button\n key={`${challenge.sessionId}:${method.type}`}\n type=\"button\"\n variant=\"outline\"\n className=\"w-full justify-center\"\n onClick={() => {\n setSelectedMethod(method.type)\n setError(null)\n setShowMoreOptions(false)\n }}\n >\n {method.label}\n </Button>\n ))}\n </div>\n ) : null}\n </div>\n ) : null}\n\n {error ? <p className=\"text-sm text-red-500\" role=\"alert\">{error}</p> : null}\n\n </div>\n ) : (\n <div className=\"flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900\">\n <AlertTriangle className=\"size-4\" />\n <span>{t('security.admin.sudo.challenge.noMethods', 'No sudo authentication methods are available.')}</span>\n </div>\n )}\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
3
|
+
"version": "0.5.1-develop.2874.77704bccbd",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@open-mercato/core": "0.5.1-develop.
|
|
68
|
-
"@open-mercato/ui": "0.5.1-develop.
|
|
67
|
+
"@open-mercato/core": "0.5.1-develop.2874.77704bccbd",
|
|
68
|
+
"@open-mercato/ui": "0.5.1-develop.2874.77704bccbd",
|
|
69
69
|
"@simplewebauthn/browser": "^13.3.0",
|
|
70
70
|
"@simplewebauthn/server": "^13.3.0",
|
|
71
71
|
"@simplewebauthn/types": "^12.0.0",
|
|
@@ -75,10 +75,10 @@
|
|
|
75
75
|
"qrcode": "^1.5.4"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
78
|
+
"@open-mercato/shared": "0.5.1-develop.2874.77704bccbd"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
81
|
+
"@open-mercato/shared": "0.5.1-develop.2874.77704bccbd",
|
|
82
82
|
"@types/jest": "^30.0.0",
|
|
83
83
|
"jest": "^30.3.0",
|
|
84
84
|
"ts-jest": "^29.4.9"
|
|
@@ -1367,7 +1367,7 @@ export default function RecordLockingWidget({
|
|
|
1367
1367
|
{participantEmails.map((email) => (
|
|
1368
1368
|
<span
|
|
1369
1369
|
key={email}
|
|
1370
|
-
className="inline-flex items-center gap-1 rounded-full border border-amber-300 bg-amber-100 px-2 py-0.5 text-
|
|
1370
|
+
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"
|
|
1371
1371
|
>
|
|
1372
1372
|
<Mail className="h-3 w-3" />
|
|
1373
1373
|
<span>{email}</span>
|
|
@@ -41,8 +41,8 @@ export function IncomingChangesRenderer({ notification }: NotificationRendererPr
|
|
|
41
41
|
return (
|
|
42
42
|
<div
|
|
43
43
|
className={cn(
|
|
44
|
-
'group relative px-4 py-3 hover:bg-muted/50 transition-colors border-l-4 border-l-
|
|
45
|
-
isUnread && 'bg-
|
|
44
|
+
'group relative px-4 py-3 hover:bg-muted/50 transition-colors border-l-4 border-l-status-info-border',
|
|
45
|
+
isUnread && 'bg-status-info-bg',
|
|
46
46
|
)}
|
|
47
47
|
>
|
|
48
48
|
{isUnread ? (
|
|
@@ -51,8 +51,8 @@ export function IncomingChangesRenderer({ notification }: NotificationRendererPr
|
|
|
51
51
|
|
|
52
52
|
<div className="flex gap-3">
|
|
53
53
|
<div className="mt-0.5 flex-shrink-0">
|
|
54
|
-
<div className="h-10 w-10 rounded-lg bg-
|
|
55
|
-
<GitPullRequestArrow className="h-5 w-5 text-
|
|
54
|
+
<div className="h-10 w-10 rounded-lg bg-status-info-bg flex items-center justify-center">
|
|
55
|
+
<GitPullRequestArrow className="h-5 w-5 text-status-info-icon" />
|
|
56
56
|
</div>
|
|
57
57
|
</div>
|
|
58
58
|
|
|
@@ -69,7 +69,7 @@ export function IncomingChangesRenderer({ notification }: NotificationRendererPr
|
|
|
69
69
|
|
|
70
70
|
<div className="mt-2 overflow-x-auto rounded border border-border/70">
|
|
71
71
|
<table className="w-full text-xs">
|
|
72
|
-
<thead className="bg-muted/
|
|
72
|
+
<thead className="bg-muted/50">
|
|
73
73
|
<tr>
|
|
74
74
|
<th className="px-2 py-1.5 text-left font-medium">{t('record_locks.conflict.field', 'Field')}</th>
|
|
75
75
|
<th className="px-2 py-1.5 text-left font-medium">{t('record_locks.conflict.incoming_label', 'Incoming')}</th>
|
|
@@ -78,7 +78,7 @@ export function IncomingChangesRenderer({ notification }: NotificationRendererPr
|
|
|
78
78
|
</thead>
|
|
79
79
|
<tbody>
|
|
80
80
|
{rows.length ? rows.map((row, index) => (
|
|
81
|
-
<tr key={`${row.field}-${index}`} className="border-t border-border/
|
|
81
|
+
<tr key={`${row.field}-${index}`} className="border-t border-border/70">
|
|
82
82
|
<td className="px-2 py-1.5 align-top text-foreground">{row.field}</td>
|
|
83
83
|
<td className="px-2 py-1.5 align-top text-muted-foreground">{row.incoming}</td>
|
|
84
84
|
<td className="px-2 py-1.5 align-top text-muted-foreground">{row.current}</td>
|
|
@@ -224,7 +224,7 @@ function SecuritySudoPageInner() {
|
|
|
224
224
|
return (
|
|
225
225
|
<Page>
|
|
226
226
|
<PageBody className="space-y-6">
|
|
227
|
-
<div className="rounded-xl border bg-muted/
|
|
227
|
+
<div className="rounded-xl border bg-muted/30 p-4">
|
|
228
228
|
<div className="flex items-start gap-3">
|
|
229
229
|
<ShieldAlert className="mt-0.5 size-5 text-amber-600" />
|
|
230
230
|
<div className="space-y-1">
|
|
@@ -154,7 +154,7 @@ function AllowedMethodsField({ value, setValue, disabled }: CrudCustomFieldRende
|
|
|
154
154
|
}, [disabled, selectedMethods, setValue])
|
|
155
155
|
|
|
156
156
|
return (
|
|
157
|
-
<div className="space-y-3 rounded-md border bg-muted/
|
|
157
|
+
<div className="space-y-3 rounded-md border bg-muted/30 p-3">
|
|
158
158
|
<div className="flex flex-wrap gap-2">
|
|
159
159
|
<Button
|
|
160
160
|
type="button"
|
|
@@ -211,7 +211,7 @@ export default function SudoChallengeModal({
|
|
|
211
211
|
return (
|
|
212
212
|
<Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) handleClose() }}>
|
|
213
213
|
<DialogContent
|
|
214
|
-
className="sm:max-w-lg [&_[data-dialog-close]]:rounded-full [&_[data-dialog-close]]:border [&_[data-dialog-close]]:border-white/20 [&_[data-dialog-close]]:bg-white/5 [&_[data-dialog-close]]:opacity-100 [&_[data-dialog-close]]:transition-none [&_[data-dialog-close]]:hover:bg-white/10 [&_[data-dialog-close]]:hover:opacity-100 [&_[data-dialog-close]]:focus:ring-0 [&_[data-dialog-close]]:focus:ring-offset-0"
|
|
214
|
+
className="sm:max-w-lg [&_[data-dialog-close]]:rounded-full [&_[data-dialog-close]]:border [&_[data-dialog-close]]:border-white/20 [&_[data-dialog-close]]:bg-white/5 [&_[data-dialog-close]]:opacity-100 [&_[data-dialog-close]]:transition-none [&_[data-dialog-close]]:hover:bg-white/10 [&_[data-dialog-close]]:hover:opacity-100 [&_[data-dialog-close]]:focus-visible:ring-0 [&_[data-dialog-close]]:focus-visible:ring-offset-0"
|
|
215
215
|
onKeyDown={(event) => {
|
|
216
216
|
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter' && challenge?.method === 'password') {
|
|
217
217
|
event.preventDefault()
|