@open-mercato/ui 0.6.5-develop.4516.1.88e6ab71a9 → 0.6.5-develop.4544.1.71c003c861
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -0
- package/dist/backend/AppShell.js +6 -0
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +120 -15
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +20 -8
- package/dist/backend/DataTable.js.map +3 -3
- package/dist/backend/PerspectiveSidebar.js +8 -1
- package/dist/backend/PerspectiveSidebar.js.map +2 -2
- package/dist/backend/RowActions.js +1 -1
- package/dist/backend/RowActions.js.map +2 -2
- package/dist/backend/conflicts/RecordConflictBanner.js +73 -0
- package/dist/backend/conflicts/RecordConflictBanner.js.map +7 -0
- package/dist/backend/conflicts/index.js +32 -0
- package/dist/backend/conflicts/index.js.map +7 -0
- package/dist/backend/conflicts/store.js +50 -0
- package/dist/backend/conflicts/store.js.map +7 -0
- package/dist/backend/injection/useGuardedMutation.js +8 -1
- package/dist/backend/injection/useGuardedMutation.js.map +2 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js +23 -7
- package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
- package/dist/backend/utils/optimisticLock.js +34 -0
- package/dist/backend/utils/optimisticLock.js.map +7 -0
- package/package.json +7 -3
- package/src/backend/AppShell.tsx +10 -0
- package/src/backend/CrudForm.tsx +189 -12
- package/src/backend/DataTable.tsx +22 -8
- package/src/backend/PerspectiveSidebar.tsx +8 -1
- package/src/backend/RowActions.tsx +1 -1
- package/src/backend/__tests__/CrudForm.dotPath.test.tsx +312 -0
- package/src/backend/__tests__/CrudForm.optimisticLock.test.tsx +288 -0
- package/src/backend/conflicts/RecordConflictBanner.tsx +77 -0
- package/src/backend/conflicts/__tests__/RecordConflictBanner.test.tsx +61 -0
- package/src/backend/conflicts/__tests__/store.test.ts +78 -0
- package/src/backend/conflicts/index.ts +49 -0
- package/src/backend/conflicts/store.ts +94 -0
- package/src/backend/injection/__tests__/useGuardedMutation.optimisticLock.test.tsx +118 -0
- package/src/backend/injection/useGuardedMutation.ts +16 -1
- package/src/backend/sidebar/SidebarCustomizationEditor.tsx +35 -11
- package/src/backend/utils/__tests__/optimisticLock.test.ts +126 -0
- package/src/backend/utils/optimisticLock.ts +70 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side helpers for the OSS opt-in optimistic-locking guard
|
|
3
|
+
* (spec: .ai/specs/2026-05-25-oss-optimistic-locking.md).
|
|
4
|
+
*
|
|
5
|
+
* These are deliberately small and dependency-light so they can be wired
|
|
6
|
+
* into any backend page without touching the shared CrudForm / useGuardedMutation
|
|
7
|
+
* components. A future PR may pull them into CrudForm directly once the
|
|
8
|
+
* reference rollout is broader.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
OPTIMISTIC_LOCK_CONFLICT_CODE,
|
|
12
|
+
OPTIMISTIC_LOCK_HEADER_NAME,
|
|
13
|
+
type OptimisticLockConflictBody,
|
|
14
|
+
} from '@open-mercato/shared/lib/crud/optimistic-lock-headers'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the extension-header bag for an `apiCall` request. Pass into
|
|
18
|
+
* `withScopedApiRequestHeaders(buildOptimisticLockHeader(updatedAt), ...)`
|
|
19
|
+
* when issuing a `PUT`/`PATCH`/`DELETE` for a record that exposes
|
|
20
|
+
* `updatedAt`. Returns an empty object when the input is missing/empty so
|
|
21
|
+
* the call site stays unconditional:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* await withScopedApiRequestHeaders(
|
|
25
|
+
* buildOptimisticLockHeader(record.updatedAt),
|
|
26
|
+
* () => updateCrud('customers/companies', id, { ... }),
|
|
27
|
+
* )
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function buildOptimisticLockHeader(
|
|
31
|
+
updatedAt: string | null | undefined,
|
|
32
|
+
): Record<string, string> {
|
|
33
|
+
if (typeof updatedAt !== 'string') return {}
|
|
34
|
+
const trimmed = updatedAt.trim()
|
|
35
|
+
if (!trimmed) return {}
|
|
36
|
+
return { [OPTIMISTIC_LOCK_HEADER_NAME]: trimmed }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect whether an error thrown by `raiseCrudError`/`apiCallOrThrow` is an
|
|
41
|
+
* optimistic-lock conflict (HTTP 409 with `code: 'optimistic_lock_conflict'`).
|
|
42
|
+
*
|
|
43
|
+
* Returns the typed conflict body when matched so the caller can show a
|
|
44
|
+
* UI that includes the server's `currentUpdatedAt`, or `null` otherwise.
|
|
45
|
+
*/
|
|
46
|
+
export function extractOptimisticLockConflict(
|
|
47
|
+
err: unknown,
|
|
48
|
+
): OptimisticLockConflictBody | null {
|
|
49
|
+
if (!err || typeof err !== 'object') return null
|
|
50
|
+
const candidate = err as Record<string, unknown>
|
|
51
|
+
const status = candidate.status
|
|
52
|
+
if (status !== 409) return null
|
|
53
|
+
const body = candidate.body && typeof candidate.body === 'object'
|
|
54
|
+
? candidate.body
|
|
55
|
+
: candidate
|
|
56
|
+
if (!body || typeof body !== 'object') return null
|
|
57
|
+
const bodyRecord = body as Record<string, unknown>
|
|
58
|
+
if (bodyRecord.code !== OPTIMISTIC_LOCK_CONFLICT_CODE) return null
|
|
59
|
+
const currentUpdatedAt = bodyRecord.currentUpdatedAt
|
|
60
|
+
const expectedUpdatedAt = bodyRecord.expectedUpdatedAt
|
|
61
|
+
if (typeof currentUpdatedAt !== 'string' || typeof expectedUpdatedAt !== 'string') return null
|
|
62
|
+
return {
|
|
63
|
+
error: typeof bodyRecord.error === 'string'
|
|
64
|
+
? (bodyRecord.error as OptimisticLockConflictBody['error'])
|
|
65
|
+
: 'record_modified',
|
|
66
|
+
code: OPTIMISTIC_LOCK_CONFLICT_CODE,
|
|
67
|
+
currentUpdatedAt,
|
|
68
|
+
expectedUpdatedAt,
|
|
69
|
+
}
|
|
70
|
+
}
|