@open-mercato/ui 0.6.6-develop.6317.1.b3be10ab84 → 0.6.6-develop.6331.1.a33b8e99b7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/backend/AppShell.js +13 -2
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +2 -1
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/OrganizationScopeBoundary.js +33 -0
- package/dist/backend/OrganizationScopeBoundary.js.map +7 -0
- package/dist/backend/conflicts/index.js +50 -13
- package/dist/backend/conflicts/index.js.map +2 -2
- package/dist/backend/detail/AddressesSection.js +9 -5
- package/dist/backend/detail/AddressesSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +11 -6
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/injection/recordContext.js +40 -0
- package/dist/backend/injection/recordContext.js.map +7 -0
- package/dist/backend/notifications/NotificationPanel.js +14 -28
- package/dist/backend/notifications/NotificationPanel.js.map +2 -2
- package/dist/backend/utils/optimisticLock.js +19 -1
- package/dist/backend/utils/optimisticLock.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +26 -2
- package/src/backend/CrudForm.tsx +4 -1
- package/src/backend/OrganizationScopeBoundary.tsx +54 -0
- package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +59 -0
- package/src/backend/__tests__/NotesSection.test.tsx +71 -0
- package/src/backend/__tests__/OrganizationScopeBoundary.test.tsx +74 -0
- package/src/backend/conflicts/__tests__/recordLockDeferral.test.ts +99 -0
- package/src/backend/conflicts/index.ts +96 -19
- package/src/backend/detail/AddressesSection.tsx +12 -7
- package/src/backend/detail/NotesSection.tsx +19 -8
- package/src/backend/injection/recordContext.tsx +101 -0
- package/src/backend/notifications/NotificationPanel.tsx +35 -38
- package/src/backend/notifications/__tests__/NotificationPanel.test.tsx +124 -0
- package/src/backend/utils/optimisticLock.ts +36 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
subscribeOrganizationScopeChanged
|
|
6
|
+
} from "@open-mercato/shared/lib/frontend/organizationEvents";
|
|
7
|
+
function OrganizationScopeBoundary({ active, children }) {
|
|
8
|
+
const [scopeKey, setScopeKey] = React.useState(0);
|
|
9
|
+
const lastScopeRef = React.useRef(null);
|
|
10
|
+
const hasInitializedScopeRef = React.useRef(false);
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
return subscribeOrganizationScopeChanged((detail) => {
|
|
13
|
+
const prev = lastScopeRef.current;
|
|
14
|
+
lastScopeRef.current = detail;
|
|
15
|
+
if (!hasInitializedScopeRef.current) {
|
|
16
|
+
hasInitializedScopeRef.current = true;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (prev && prev.organizationId === detail.organizationId && prev.tenantId === detail.tenantId) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
setScopeKey((current) => current + 1);
|
|
23
|
+
});
|
|
24
|
+
}, []);
|
|
25
|
+
if (!active) {
|
|
26
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
27
|
+
}
|
|
28
|
+
return /* @__PURE__ */ jsx(React.Fragment, { children }, scopeKey);
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
OrganizationScopeBoundary
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=OrganizationScopeBoundary.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/backend/OrganizationScopeBoundary.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport {\n subscribeOrganizationScopeChanged,\n type OrganizationScopeChangedDetail,\n} from '@open-mercato/shared/lib/frontend/organizationEvents'\n\nexport type OrganizationScopeBoundaryProps = {\n active: boolean\n children: React.ReactNode\n}\n\n/**\n * Remounts its children whenever the active organization/tenant scope changes.\n *\n * The org/tenant switcher already calls `router.refresh()` on a scope change,\n * which re-runs server components but does NOT remount client components \u2014 so\n * settings pages that fetch their data once in a mount `useEffect` keep showing\n * the previous scope's values until a manual reload. Wrapping those pages here\n * forces a fresh mount (and therefore a refetch) on a real scope change.\n *\n * Gated by `active` so only the settings surface remounts; list/CRUD pages keep\n * relying on the smoother `router.refresh()` path that preserves table state.\n */\nexport function OrganizationScopeBoundary({ active, children }: OrganizationScopeBoundaryProps) {\n const [scopeKey, setScopeKey] = React.useState(0)\n const lastScopeRef = React.useRef<OrganizationScopeChangedDetail | null>(null)\n const hasInitializedScopeRef = React.useRef(false)\n\n React.useEffect(() => {\n return subscribeOrganizationScopeChanged((detail) => {\n const prev = lastScopeRef.current\n lastScopeRef.current = detail\n if (!hasInitializedScopeRef.current) {\n hasInitializedScopeRef.current = true\n return\n }\n if (\n prev &&\n prev.organizationId === detail.organizationId &&\n prev.tenantId === detail.tenantId\n ) {\n return\n }\n setScopeKey((current) => current + 1)\n })\n }, [])\n\n if (!active) {\n return <>{children}</>\n }\n\n return <React.Fragment key={scopeKey}>{children}</React.Fragment>\n}\n"],
|
|
5
|
+
"mappings": ";AAiDW;AAhDX,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,OAEK;AAmBA,SAAS,0BAA0B,EAAE,QAAQ,SAAS,GAAmC;AAC9F,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,CAAC;AAChD,QAAM,eAAe,MAAM,OAA8C,IAAI;AAC7E,QAAM,yBAAyB,MAAM,OAAO,KAAK;AAEjD,QAAM,UAAU,MAAM;AACpB,WAAO,kCAAkC,CAAC,WAAW;AACnD,YAAM,OAAO,aAAa;AAC1B,mBAAa,UAAU;AACvB,UAAI,CAAC,uBAAuB,SAAS;AACnC,+BAAuB,UAAU;AACjC;AAAA,MACF;AACA,UACE,QACA,KAAK,mBAAmB,OAAO,kBAC/B,KAAK,aAAa,OAAO,UACzB;AACA;AAAA,MACF;AACA,kBAAY,CAAC,YAAY,UAAU,CAAC;AAAA,IACtC,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,MAAI,CAAC,QAAQ;AACX,WAAO,gCAAG,UAAS;AAAA,EACrB;AAEA,SAAO,oBAAC,MAAM,UAAN,EAA+B,YAAX,QAAoB;AAClD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
extractOptimisticLockConflict,
|
|
3
|
+
extractRecordLockConflict
|
|
4
|
+
} from "../utils/optimisticLock.js";
|
|
2
5
|
import { showRecordConflict } from "./store.js";
|
|
3
6
|
import {
|
|
4
7
|
showRecordConflict as showRecordConflict2,
|
|
@@ -7,24 +10,58 @@ import {
|
|
|
7
10
|
getRecordConflictForTest
|
|
8
11
|
} from "./store.js";
|
|
9
12
|
import { RecordConflictBanner } from "./RecordConflictBanner.js";
|
|
13
|
+
let recordLockConflictHandler = null;
|
|
14
|
+
function registerRecordLockConflictHandler(handler) {
|
|
15
|
+
recordLockConflictHandler = handler;
|
|
16
|
+
return () => {
|
|
17
|
+
if (recordLockConflictHandler === handler) recordLockConflictHandler = null;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function hasRecordLockConflictHandlerForTest() {
|
|
21
|
+
return recordLockConflictHandler !== null;
|
|
22
|
+
}
|
|
23
|
+
function resetRecordLockConflictHandlerForTest() {
|
|
24
|
+
recordLockConflictHandler = null;
|
|
25
|
+
}
|
|
10
26
|
function surfaceRecordConflict(error, t, options = {}) {
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
const recordLockConflict = extractRecordLockConflict(error);
|
|
28
|
+
if (recordLockConflict && recordLockConflictHandler) {
|
|
29
|
+
if (recordLockConflictHandler(recordLockConflict, error)) return true;
|
|
30
|
+
}
|
|
31
|
+
const ossConflict = extractOptimisticLockConflict(error);
|
|
32
|
+
if (ossConflict) {
|
|
33
|
+
showRecordConflict({
|
|
34
|
+
message: t(
|
|
35
|
+
"ui.forms.flash.recordModified",
|
|
36
|
+
"This record was modified by someone else. Refresh and try again."
|
|
37
|
+
),
|
|
38
|
+
title: options.title ?? null,
|
|
39
|
+
currentUpdatedAt: ossConflict.currentUpdatedAt,
|
|
40
|
+
onRefresh: options.onRefresh ?? null
|
|
41
|
+
});
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (recordLockConflict) {
|
|
45
|
+
showRecordConflict({
|
|
46
|
+
message: t(
|
|
47
|
+
"ui.forms.flash.recordModified",
|
|
48
|
+
"This record was modified by someone else. Refresh and try again."
|
|
49
|
+
),
|
|
50
|
+
title: options.title ?? null,
|
|
51
|
+
currentUpdatedAt: null,
|
|
52
|
+
onRefresh: options.onRefresh ?? null
|
|
53
|
+
});
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
23
57
|
}
|
|
24
58
|
export {
|
|
25
59
|
RecordConflictBanner,
|
|
26
60
|
dismissRecordConflict,
|
|
27
61
|
getRecordConflictForTest,
|
|
62
|
+
hasRecordLockConflictHandlerForTest,
|
|
63
|
+
registerRecordLockConflictHandler,
|
|
64
|
+
resetRecordLockConflictHandlerForTest,
|
|
28
65
|
showRecordConflict2 as showRecordConflict,
|
|
29
66
|
surfaceRecordConflict,
|
|
30
67
|
useRecordConflict
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/conflicts/index.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "AAAA,
|
|
4
|
+
"sourcesContent": ["import {\n extractOptimisticLockConflict,\n extractRecordLockConflict,\n type RecordLockConflictBody,\n} from '../utils/optimisticLock'\nimport { showRecordConflict } from './store'\n\nexport {\n showRecordConflict,\n dismissRecordConflict,\n useRecordConflict,\n getRecordConflictForTest,\n type RecordConflictEntry,\n type ShowRecordConflictInput,\n} from './store'\nexport { RecordConflictBanner } from './RecordConflictBanner'\n\ntype Translate = (key: string, fallback?: string) => string\n\nexport type SurfaceRecordConflictOptions = {\n /** Custom refresh handler. Omit to let the banner reload the page. */\n onRefresh?: (() => void) | null\n /** Localized title override; the banner falls back to a generic title. */\n title?: string | null\n}\n\n/**\n * Handler the enterprise `record_locks` merge-dialog widget registers (on mount)\n * so `surfaceRecordConflict` can defer the OSS conflict bar in favor of the\n * field-level merge dialog. Core/UI stays enterprise-free: enterprise calls\n * {@link registerRecordLockConflictHandler}; this module never imports it.\n *\n * MUST return `true` only when it actually owns/renders the surface for THIS\n * conflict (its widget is mounted for the conflicting record). Returning `false`\n * (different record / cannot render) makes `surfaceRecordConflict` fall through\n * to the OSS conflict bar, so a conflict is never swallowed and we never render\n * BOTH the merge dialog and the bar (single surface, S3).\n */\nexport type RecordLockConflictHandler = (conflict: RecordLockConflictBody, error: unknown) => boolean\n\nlet recordLockConflictHandler: RecordLockConflictHandler | null = null\n\n/**\n * Register the record-lock conflict handler (enterprise merge-dialog widget).\n * Returns an unregister function. When a handler is registered,\n * `surfaceRecordConflict` defers `record_lock_conflict` 409s to it instead of\n * rendering the OSS conflict bar (single surface, S3).\n *\n * SAFETY: when NO handler is registered (widget absent/removed),\n * `surfaceRecordConflict` still renders the OSS conflict bar for ANY 409 \u2014 a\n * conflict is never silently swallowed.\n */\nexport function registerRecordLockConflictHandler(handler: RecordLockConflictHandler): () => void {\n recordLockConflictHandler = handler\n return () => {\n if (recordLockConflictHandler === handler) recordLockConflictHandler = null\n }\n}\n\n/** Test-only: inspect whether a record-lock conflict handler is registered. */\nexport function hasRecordLockConflictHandlerForTest(): boolean {\n return recordLockConflictHandler !== null\n}\n\n/** Test-only: clear any registered record-lock conflict handler. */\nexport function resetRecordLockConflictHandlerForTest(): void {\n recordLockConflictHandler = null\n}\n\n/**\n * Single conflict surface (S3). Resolves a 409 to exactly one UI:\n *\n * - `record_lock_conflict` payload AND a merge-dialog handler registered \u2192\n * defer to the handler (no conflict bar) and return `true`.\n * - otherwise (OSS `optimistic_lock_conflict`, OR a `record_lock_conflict`\n * with NO handler registered) \u2192 render the OSS conflict bar and return\n * `true`. A conflict is ALWAYS surfaced; the worst case is plainer UX, never\n * a silently-swallowed conflict.\n * - not a recognized conflict \u2192 return `false` so callers fall back to their\n * normal error handling.\n */\nexport function surfaceRecordConflict(\n error: unknown,\n t: Translate,\n options: SurfaceRecordConflictOptions = {},\n): boolean {\n const recordLockConflict = extractRecordLockConflict(error)\n if (recordLockConflict && recordLockConflictHandler) {\n // The widget owns the surface ONLY when it handles this record's conflict\n // (returns true). If it declines (different record / not mounted for it),\n // fall through to the OSS bar so the conflict is never swallowed and we\n // never render both the merge dialog AND the bar.\n if (recordLockConflictHandler(recordLockConflict, error)) return true\n }\n\n const ossConflict = extractOptimisticLockConflict(error)\n if (ossConflict) {\n showRecordConflict({\n message: t(\n 'ui.forms.flash.recordModified',\n 'This record was modified by someone else. Refresh and try again.',\n ),\n title: options.title ?? null,\n currentUpdatedAt: ossConflict.currentUpdatedAt,\n onRefresh: options.onRefresh ?? null,\n })\n return true\n }\n\n // No handler is registered but the server returned a record-lock 409: still\n // render the OSS bar so the conflict is never swallowed.\n if (recordLockConflict) {\n showRecordConflict({\n message: t(\n 'ui.forms.flash.recordModified',\n 'This record was modified by someone else. Refresh and try again.',\n ),\n title: options.title ?? null,\n currentUpdatedAt: null,\n onRefresh: options.onRefresh ?? null,\n })\n return true\n }\n\n return false\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,0BAA0B;AAEnC;AAAA,EACE,sBAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,4BAA4B;AAyBrC,IAAI,4BAA8D;AAY3D,SAAS,kCAAkC,SAAgD;AAChG,8BAA4B;AAC5B,SAAO,MAAM;AACX,QAAI,8BAA8B,QAAS,6BAA4B;AAAA,EACzE;AACF;AAGO,SAAS,sCAA+C;AAC7D,SAAO,8BAA8B;AACvC;AAGO,SAAS,wCAA8C;AAC5D,8BAA4B;AAC9B;AAcO,SAAS,sBACd,OACA,GACA,UAAwC,CAAC,GAChC;AACT,QAAM,qBAAqB,0BAA0B,KAAK;AAC1D,MAAI,sBAAsB,2BAA2B;AAKnD,QAAI,0BAA0B,oBAAoB,KAAK,EAAG,QAAO;AAAA,EACnE;AAEA,QAAM,cAAc,8BAA8B,KAAK;AACvD,MAAI,aAAa;AACf,uBAAmB;AAAA,MACjB,SAAS;AAAA,QACP;AAAA,QACA;AAAA,MACF;AAAA,MACA,OAAO,QAAQ,SAAS;AAAA,MACxB,kBAAkB,YAAY;AAAA,MAC9B,WAAW,QAAQ,aAAa;AAAA,IAClC,CAAC;AACD,WAAO;AAAA,EACT;AAIA,MAAI,oBAAoB;AACtB,uBAAmB;AAAA,MACjB,SAAS;AAAA,QACP;AAAA,QACA;AAAA,MACF;AAAA,MACA,OAAO,QAAQ,SAAS;AAAA,MACxB,kBAAkB;AAAA,MAClB,WAAW,QAAQ,aAAa;AAAA,IAClC,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": ["showRecordConflict"]
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
5
|
+
import { surfaceRecordConflict } from "../conflicts/index.js";
|
|
5
6
|
import { LoadingMessage } from "@open-mercato/ui/backend/detail";
|
|
6
7
|
import { createTranslatorWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
7
8
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
@@ -134,7 +135,8 @@ function AddressesSectionImpl({
|
|
|
134
135
|
pushLoading();
|
|
135
136
|
setIsSubmitting(true);
|
|
136
137
|
try {
|
|
137
|
-
|
|
138
|
+
const addressUpdatedAt = addresses.find((address) => address.id === id)?.updatedAt ?? null;
|
|
139
|
+
await dataAdapter.update({ id, payload, updatedAt: addressUpdatedAt, context: dataContext });
|
|
138
140
|
setAddresses((prev) => {
|
|
139
141
|
return prev.map((address) => {
|
|
140
142
|
if (address.id !== id) {
|
|
@@ -159,6 +161,7 @@ function AddressesSectionImpl({
|
|
|
159
161
|
});
|
|
160
162
|
flash(label("success", "Address saved."), "success");
|
|
161
163
|
} catch (err) {
|
|
164
|
+
surfaceRecordConflict(err, t);
|
|
162
165
|
const message = err instanceof Error && err.message ? err.message : label("error", "Failed to save address.");
|
|
163
166
|
const error = new Error(message);
|
|
164
167
|
if (err && typeof err === "object" && err.details) {
|
|
@@ -170,7 +173,7 @@ function AddressesSectionImpl({
|
|
|
170
173
|
popLoading();
|
|
171
174
|
}
|
|
172
175
|
},
|
|
173
|
-
[dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading]
|
|
176
|
+
[addresses, dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading, t]
|
|
174
177
|
);
|
|
175
178
|
const handleDelete = React.useCallback(
|
|
176
179
|
async (id) => {
|
|
@@ -180,19 +183,20 @@ function AddressesSectionImpl({
|
|
|
180
183
|
pushLoading();
|
|
181
184
|
setIsSubmitting(true);
|
|
182
185
|
try {
|
|
183
|
-
|
|
186
|
+
const addressUpdatedAt = addresses.find((address) => address.id === id)?.updatedAt ?? null;
|
|
187
|
+
await dataAdapter.delete({ id, updatedAt: addressUpdatedAt, context: dataContext });
|
|
184
188
|
setAddresses((prev) => prev.filter((address) => address.id !== id));
|
|
185
189
|
flash(label("deleted", "Address deleted."), "success");
|
|
186
190
|
} catch (err) {
|
|
187
191
|
const message = err instanceof Error && err.message ? err.message : label("error", "Failed to delete address.");
|
|
188
|
-
flash(message, "error");
|
|
192
|
+
if (!surfaceRecordConflict(err, t)) flash(message, "error");
|
|
189
193
|
throw err;
|
|
190
194
|
} finally {
|
|
191
195
|
setIsSubmitting(false);
|
|
192
196
|
popLoading();
|
|
193
197
|
}
|
|
194
198
|
},
|
|
195
|
-
[dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading]
|
|
199
|
+
[addresses, dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading, t]
|
|
196
200
|
);
|
|
197
201
|
const displayAddresses = React.useMemo(() => {
|
|
198
202
|
return addresses.map((address) => ({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/detail/AddressesSection.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport AddressTiles, { type AddressInput, type AddressValue } from './AddressTiles'\nimport type { AddressTypesAdapter } from './AddressEditor'\nimport type { AddressFormatStrategy } from './addressFormat'\nimport { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '../injection/useRegisteredComponent'\n\ntype Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string\n\nexport type SectionAction = {\n label: React.ReactNode\n onClick: () => void\n disabled?: boolean\n icon?: React.ReactNode\n}\n\nexport type TabEmptyStateConfig = {\n title: string\n actionLabel: string\n description?: string\n}\n\nexport type AddressSummary = {\n id: string\n name?: string | null\n purpose?: string | null\n companyName?: string | null\n addressLine1: string\n addressLine2?: string | null\n buildingNumber?: string | null\n flatNumber?: string | null\n city?: string | null\n region?: string | null\n postalCode?: string | null\n country?: string | null\n isPrimary?: boolean\n}\n\nexport type AddressDataAdapter<C = unknown> = {\n list: (params: { entityId: string | null; context?: C }) => Promise<AddressSummary[]>\n create: (params: { entityId: string; payload: AddressInput; context?: C }) => Promise<{ id?: string } | void>\n update: (params: { id: string; payload: AddressInput; context?: C }) => Promise<void>\n delete: (params: { id: string; context?: C }) => Promise<void>\n}\n\nexport type AddressesSectionProps<C = unknown> = {\n entityId: string | null\n emptyLabel: string\n addActionLabel: string\n emptyState: TabEmptyStateConfig\n onActionChange?: (action: SectionAction | null) => void\n translator?: Translator\n onLoadingChange?: (isLoading: boolean) => void\n dataAdapter: AddressDataAdapter<C>\n dataContext?: C\n addressTypesAdapter?: AddressTypesAdapter<C>\n addressTypesContext?: C\n loadFormat?: (context?: C) => Promise<AddressFormatStrategy>\n formatContext?: C\n labelPrefix?: string\n}\n\nfunction generateTempId() {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID()\n return `tmp_${Math.random().toString(36).slice(2)}`\n}\n\nfunction AddressesSectionImpl<C = unknown>({\n entityId,\n emptyLabel,\n addActionLabel,\n emptyState,\n onActionChange,\n translator,\n onLoadingChange,\n dataAdapter,\n dataContext,\n addressTypesAdapter,\n addressTypesContext,\n loadFormat,\n formatContext,\n labelPrefix = 'customers.people.detail.addresses',\n}: AddressesSectionProps<C>) {\n const tHook = useT()\n const fallbackTranslator = React.useMemo<Translator>(() => createTranslatorWithFallback(tHook), [tHook])\n const t = translator ?? fallbackTranslator\n\n const label = React.useCallback(\n (suffix: string, fallback?: string, params?: Record<string, string | number>) =>\n t(`${labelPrefix}.${suffix}`, fallback, params),\n [labelPrefix, t],\n )\n\n const normalizedEntityId = React.useMemo(() => {\n if (typeof entityId !== 'string') return null\n const trimmed = entityId.trim()\n return trimmed.length ? trimmed : null\n }, [entityId])\n const [addresses, setAddresses] = React.useState<AddressSummary[]>([])\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState<boolean>(() => Boolean(normalizedEntityId))\n\n const loadingCounterRef = React.useRef(0)\n const pushLoading = React.useCallback(() => {\n loadingCounterRef.current += 1\n if (loadingCounterRef.current === 1) {\n onLoadingChange?.(true)\n }\n }, [onLoadingChange])\n const popLoading = React.useCallback(() => {\n loadingCounterRef.current = Math.max(0, loadingCounterRef.current - 1)\n if (loadingCounterRef.current === 0) {\n onLoadingChange?.(false)\n }\n }, [onLoadingChange])\n\n const loadAddresses = React.useCallback(async () => {\n if (!normalizedEntityId) {\n setAddresses([])\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n pushLoading()\n try {\n const items = await dataAdapter.list({ entityId: normalizedEntityId, context: dataContext })\n setAddresses(items)\n } catch (err) {\n const message =\n err instanceof Error && err.message\n ? err.message\n : label('error', 'Failed to load addresses.')\n flash(message, 'error')\n setAddresses([])\n } finally {\n setIsLoading(false)\n popLoading()\n }\n }, [dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading])\n\n React.useEffect(() => {\n void loadAddresses()\n }, [loadAddresses])\n\n const handleCreate = React.useCallback(\n async (payload: AddressInput) => {\n if (!normalizedEntityId) {\n throw new Error(label('error', 'Failed to save address.'))\n }\n pushLoading()\n setIsSubmitting(true)\n try {\n const response = await dataAdapter.create({\n entityId: normalizedEntityId,\n payload,\n context: dataContext,\n })\n const newAddress: AddressSummary = {\n id: typeof response === 'object' && response && typeof response.id === 'string' ? response.id : generateTempId(),\n name: payload.name ?? null,\n purpose: payload.purpose ?? null,\n companyName: payload.companyName ?? null,\n addressLine1: payload.addressLine1,\n addressLine2: payload.addressLine2 ?? null,\n buildingNumber: payload.buildingNumber ?? null,\n flatNumber: payload.flatNumber ?? null,\n city: payload.city ?? null,\n region: payload.region ?? null,\n postalCode: payload.postalCode ?? null,\n country: payload.country ? payload.country.toUpperCase() : null,\n isPrimary: payload.isPrimary ?? false,\n }\n setAddresses((prev) => {\n const existing = payload.isPrimary ? prev.map((addr) => ({ ...addr, isPrimary: false })) : prev\n return [newAddress, ...existing]\n })\n flash(label('success', 'Address saved.'), 'success')\n } catch (err) {\n const message =\n err instanceof Error && err.message ? err.message : label('error', 'Failed to save address.')\n const error = new Error(message) as Error & { details?: unknown }\n if (err && typeof err === 'object' && (err as { details?: unknown }).details) {\n error.details = (err as { details?: unknown }).details\n }\n throw error\n } finally {\n setIsSubmitting(false)\n popLoading()\n }\n },\n [dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading],\n )\n\n const handleUpdate = React.useCallback(\n async (id: string, payload: AddressInput) => {\n if (!normalizedEntityId) {\n throw new Error(label('error', 'Failed to save address.'))\n }\n pushLoading()\n setIsSubmitting(true)\n try {\n await dataAdapter.update({ id, payload, context: dataContext })\n setAddresses((prev) => {\n return prev.map((address) => {\n if (address.id !== id) {\n return payload.isPrimary ? { ...address, isPrimary: false } : address\n }\n return {\n ...address,\n name: payload.name ?? null,\n purpose: payload.purpose ?? null,\n companyName: payload.companyName ?? null,\n addressLine1: payload.addressLine1,\n addressLine2: payload.addressLine2 ?? null,\n buildingNumber: payload.buildingNumber ?? null,\n flatNumber: payload.flatNumber ?? null,\n city: payload.city ?? null,\n region: payload.region ?? null,\n postalCode: payload.postalCode ?? null,\n country: payload.country ? payload.country.toUpperCase() : null,\n isPrimary: payload.isPrimary ?? false,\n }\n })\n })\n flash(label('success', 'Address saved.'), 'success')\n } catch (err) {\n const message =\n err instanceof Error && err.message ? err.message : label('error', 'Failed to save address.')\n const error = new Error(message) as Error & { details?: unknown }\n if (err && typeof err === 'object' && (err as { details?: unknown }).details) {\n error.details = (err as { details?: unknown }).details\n }\n throw error\n } finally {\n setIsSubmitting(false)\n popLoading()\n }\n },\n [dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading],\n )\n\n const handleDelete = React.useCallback(\n async (id: string) => {\n if (!normalizedEntityId) {\n throw new Error(label('error', 'Failed to delete address.'))\n }\n pushLoading()\n setIsSubmitting(true)\n try {\n await dataAdapter.delete({ id, context: dataContext })\n setAddresses((prev) => prev.filter((address) => address.id !== id))\n flash(label('deleted', 'Address deleted.'), 'success')\n } catch (err) {\n const message =\n err instanceof Error && err.message ? err.message : label('error', 'Failed to delete address.')\n flash(message, 'error')\n throw err\n } finally {\n setIsSubmitting(false)\n popLoading()\n }\n },\n [dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading],\n )\n\n const displayAddresses = React.useMemo<AddressValue[]>(() => {\n return addresses.map((address) => ({\n id: address.id,\n name: address.name ?? undefined,\n purpose: address.purpose ?? undefined,\n companyName: address.companyName ?? undefined,\n addressLine1: address.addressLine1,\n addressLine2: address.addressLine2 ?? undefined,\n buildingNumber: address.buildingNumber ?? undefined,\n flatNumber: address.flatNumber ?? undefined,\n city: address.city ?? undefined,\n region: address.region ?? undefined,\n postalCode: address.postalCode ?? undefined,\n country: address.country ?? undefined,\n isPrimary: address.isPrimary ?? false,\n }))\n }, [addresses])\n\n const [addAction, setAddAction] = React.useState<{\n openCreateForm: () => void\n addDisabled: boolean\n } | null>(null)\n\n const handleAddActionChange = React.useCallback(\n (action: { openCreateForm: () => void; addDisabled: boolean } | null) => {\n setAddAction(action)\n },\n [],\n )\n\n React.useEffect(() => {\n if (!onActionChange) return\n if (!addAction || addresses.length === 0) {\n onActionChange(null)\n return\n }\n onActionChange({\n label: addActionLabel,\n onClick: addAction.openCreateForm,\n disabled: addAction.addDisabled,\n })\n }, [addAction, addActionLabel, addresses.length, onActionChange])\n\n return (\n <div className=\"mt-4\">\n {isLoading ? (\n <div className=\"flex justify-center\">\n <LoadingMessage\n label={label('loading', 'Loading addresses\u2026')}\n className=\"min-h-[120px] w-full justify-center border-0 bg-transparent p-0\"\n />\n </div>\n ) : (\n <AddressTiles\n addresses={displayAddresses}\n onCreate={handleCreate}\n onUpdate={handleUpdate}\n onDelete={handleDelete}\n isSubmitting={isSubmitting}\n emptyLabel={emptyLabel}\n t={t}\n hideAddButton\n onAddActionChange={handleAddActionChange}\n emptyStateTitle={emptyState.title}\n emptyStateActionLabel={emptyState.actionLabel}\n labelPrefix={labelPrefix}\n addressTypesAdapter={addressTypesAdapter}\n addressTypesContext={addressTypesContext}\n loadFormat={loadFormat}\n formatContext={formatContext}\n />\n )}\n </div>\n )\n}\n\nexport function AddressesSection<C = unknown>(props: AddressesSectionProps<C>) {\n const handle = ComponentReplacementHandles.section('ui.detail', 'AddressesSection')\n const Resolved = useRegisteredComponent<AddressesSectionProps<C>>(\n handle,\n AddressesSectionImpl as React.ComponentType<AddressesSectionProps<C>>,\n )\n\n return (\n <div data-component-handle={handle}>\n <Resolved {...props} />\n </div>\n )\n}\n\nexport default AddressesSection\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { surfaceRecordConflict } from '../conflicts'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport AddressTiles, { type AddressInput, type AddressValue } from './AddressTiles'\nimport type { AddressTypesAdapter } from './AddressEditor'\nimport type { AddressFormatStrategy } from './addressFormat'\nimport { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '../injection/useRegisteredComponent'\n\ntype Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string\n\nexport type SectionAction = {\n label: React.ReactNode\n onClick: () => void\n disabled?: boolean\n icon?: React.ReactNode\n}\n\nexport type TabEmptyStateConfig = {\n title: string\n actionLabel: string\n description?: string\n}\n\nexport type AddressSummary = {\n id: string\n name?: string | null\n purpose?: string | null\n companyName?: string | null\n addressLine1: string\n addressLine2?: string | null\n buildingNumber?: string | null\n flatNumber?: string | null\n city?: string | null\n region?: string | null\n postalCode?: string | null\n country?: string | null\n isPrimary?: boolean\n updatedAt?: string | null\n}\n\nexport type AddressDataAdapter<C = unknown> = {\n list: (params: { entityId: string | null; context?: C }) => Promise<AddressSummary[]>\n create: (params: { entityId: string; payload: AddressInput; context?: C }) => Promise<{ id?: string } | void>\n update: (params: { id: string; payload: AddressInput; updatedAt?: string | null; context?: C }) => Promise<void>\n delete: (params: { id: string; updatedAt?: string | null; context?: C }) => Promise<void>\n}\n\nexport type AddressesSectionProps<C = unknown> = {\n entityId: string | null\n emptyLabel: string\n addActionLabel: string\n emptyState: TabEmptyStateConfig\n onActionChange?: (action: SectionAction | null) => void\n translator?: Translator\n onLoadingChange?: (isLoading: boolean) => void\n dataAdapter: AddressDataAdapter<C>\n dataContext?: C\n addressTypesAdapter?: AddressTypesAdapter<C>\n addressTypesContext?: C\n loadFormat?: (context?: C) => Promise<AddressFormatStrategy>\n formatContext?: C\n labelPrefix?: string\n}\n\nfunction generateTempId() {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID()\n return `tmp_${Math.random().toString(36).slice(2)}`\n}\n\nfunction AddressesSectionImpl<C = unknown>({\n entityId,\n emptyLabel,\n addActionLabel,\n emptyState,\n onActionChange,\n translator,\n onLoadingChange,\n dataAdapter,\n dataContext,\n addressTypesAdapter,\n addressTypesContext,\n loadFormat,\n formatContext,\n labelPrefix = 'customers.people.detail.addresses',\n}: AddressesSectionProps<C>) {\n const tHook = useT()\n const fallbackTranslator = React.useMemo<Translator>(() => createTranslatorWithFallback(tHook), [tHook])\n const t = translator ?? fallbackTranslator\n\n const label = React.useCallback(\n (suffix: string, fallback?: string, params?: Record<string, string | number>) =>\n t(`${labelPrefix}.${suffix}`, fallback, params),\n [labelPrefix, t],\n )\n\n const normalizedEntityId = React.useMemo(() => {\n if (typeof entityId !== 'string') return null\n const trimmed = entityId.trim()\n return trimmed.length ? trimmed : null\n }, [entityId])\n const [addresses, setAddresses] = React.useState<AddressSummary[]>([])\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState<boolean>(() => Boolean(normalizedEntityId))\n\n const loadingCounterRef = React.useRef(0)\n const pushLoading = React.useCallback(() => {\n loadingCounterRef.current += 1\n if (loadingCounterRef.current === 1) {\n onLoadingChange?.(true)\n }\n }, [onLoadingChange])\n const popLoading = React.useCallback(() => {\n loadingCounterRef.current = Math.max(0, loadingCounterRef.current - 1)\n if (loadingCounterRef.current === 0) {\n onLoadingChange?.(false)\n }\n }, [onLoadingChange])\n\n const loadAddresses = React.useCallback(async () => {\n if (!normalizedEntityId) {\n setAddresses([])\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n pushLoading()\n try {\n const items = await dataAdapter.list({ entityId: normalizedEntityId, context: dataContext })\n setAddresses(items)\n } catch (err) {\n const message =\n err instanceof Error && err.message\n ? err.message\n : label('error', 'Failed to load addresses.')\n flash(message, 'error')\n setAddresses([])\n } finally {\n setIsLoading(false)\n popLoading()\n }\n }, [dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading])\n\n React.useEffect(() => {\n void loadAddresses()\n }, [loadAddresses])\n\n const handleCreate = React.useCallback(\n async (payload: AddressInput) => {\n if (!normalizedEntityId) {\n throw new Error(label('error', 'Failed to save address.'))\n }\n pushLoading()\n setIsSubmitting(true)\n try {\n const response = await dataAdapter.create({\n entityId: normalizedEntityId,\n payload,\n context: dataContext,\n })\n const newAddress: AddressSummary = {\n id: typeof response === 'object' && response && typeof response.id === 'string' ? response.id : generateTempId(),\n name: payload.name ?? null,\n purpose: payload.purpose ?? null,\n companyName: payload.companyName ?? null,\n addressLine1: payload.addressLine1,\n addressLine2: payload.addressLine2 ?? null,\n buildingNumber: payload.buildingNumber ?? null,\n flatNumber: payload.flatNumber ?? null,\n city: payload.city ?? null,\n region: payload.region ?? null,\n postalCode: payload.postalCode ?? null,\n country: payload.country ? payload.country.toUpperCase() : null,\n isPrimary: payload.isPrimary ?? false,\n }\n setAddresses((prev) => {\n const existing = payload.isPrimary ? prev.map((addr) => ({ ...addr, isPrimary: false })) : prev\n return [newAddress, ...existing]\n })\n flash(label('success', 'Address saved.'), 'success')\n } catch (err) {\n const message =\n err instanceof Error && err.message ? err.message : label('error', 'Failed to save address.')\n const error = new Error(message) as Error & { details?: unknown }\n if (err && typeof err === 'object' && (err as { details?: unknown }).details) {\n error.details = (err as { details?: unknown }).details\n }\n throw error\n } finally {\n setIsSubmitting(false)\n popLoading()\n }\n },\n [dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading],\n )\n\n const handleUpdate = React.useCallback(\n async (id: string, payload: AddressInput) => {\n if (!normalizedEntityId) {\n throw new Error(label('error', 'Failed to save address.'))\n }\n pushLoading()\n setIsSubmitting(true)\n try {\n const addressUpdatedAt = addresses.find((address) => address.id === id)?.updatedAt ?? null\n await dataAdapter.update({ id, payload, updatedAt: addressUpdatedAt, context: dataContext })\n setAddresses((prev) => {\n return prev.map((address) => {\n if (address.id !== id) {\n return payload.isPrimary ? { ...address, isPrimary: false } : address\n }\n return {\n ...address,\n name: payload.name ?? null,\n purpose: payload.purpose ?? null,\n companyName: payload.companyName ?? null,\n addressLine1: payload.addressLine1,\n addressLine2: payload.addressLine2 ?? null,\n buildingNumber: payload.buildingNumber ?? null,\n flatNumber: payload.flatNumber ?? null,\n city: payload.city ?? null,\n region: payload.region ?? null,\n postalCode: payload.postalCode ?? null,\n country: payload.country ? payload.country.toUpperCase() : null,\n isPrimary: payload.isPrimary ?? false,\n }\n })\n })\n flash(label('success', 'Address saved.'), 'success')\n } catch (err) {\n surfaceRecordConflict(err, t)\n const message =\n err instanceof Error && err.message ? err.message : label('error', 'Failed to save address.')\n const error = new Error(message) as Error & { details?: unknown }\n if (err && typeof err === 'object' && (err as { details?: unknown }).details) {\n error.details = (err as { details?: unknown }).details\n }\n throw error\n } finally {\n setIsSubmitting(false)\n popLoading()\n }\n },\n [addresses, dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading, t],\n )\n\n const handleDelete = React.useCallback(\n async (id: string) => {\n if (!normalizedEntityId) {\n throw new Error(label('error', 'Failed to delete address.'))\n }\n pushLoading()\n setIsSubmitting(true)\n try {\n const addressUpdatedAt = addresses.find((address) => address.id === id)?.updatedAt ?? null\n await dataAdapter.delete({ id, updatedAt: addressUpdatedAt, context: dataContext })\n setAddresses((prev) => prev.filter((address) => address.id !== id))\n flash(label('deleted', 'Address deleted.'), 'success')\n } catch (err) {\n const message =\n err instanceof Error && err.message ? err.message : label('error', 'Failed to delete address.')\n if (!surfaceRecordConflict(err, t)) flash(message, 'error')\n throw err\n } finally {\n setIsSubmitting(false)\n popLoading()\n }\n },\n [addresses, dataAdapter, dataContext, label, normalizedEntityId, popLoading, pushLoading, t],\n )\n\n const displayAddresses = React.useMemo<AddressValue[]>(() => {\n return addresses.map((address) => ({\n id: address.id,\n name: address.name ?? undefined,\n purpose: address.purpose ?? undefined,\n companyName: address.companyName ?? undefined,\n addressLine1: address.addressLine1,\n addressLine2: address.addressLine2 ?? undefined,\n buildingNumber: address.buildingNumber ?? undefined,\n flatNumber: address.flatNumber ?? undefined,\n city: address.city ?? undefined,\n region: address.region ?? undefined,\n postalCode: address.postalCode ?? undefined,\n country: address.country ?? undefined,\n isPrimary: address.isPrimary ?? false,\n }))\n }, [addresses])\n\n const [addAction, setAddAction] = React.useState<{\n openCreateForm: () => void\n addDisabled: boolean\n } | null>(null)\n\n const handleAddActionChange = React.useCallback(\n (action: { openCreateForm: () => void; addDisabled: boolean } | null) => {\n setAddAction(action)\n },\n [],\n )\n\n React.useEffect(() => {\n if (!onActionChange) return\n if (!addAction || addresses.length === 0) {\n onActionChange(null)\n return\n }\n onActionChange({\n label: addActionLabel,\n onClick: addAction.openCreateForm,\n disabled: addAction.addDisabled,\n })\n }, [addAction, addActionLabel, addresses.length, onActionChange])\n\n return (\n <div className=\"mt-4\">\n {isLoading ? (\n <div className=\"flex justify-center\">\n <LoadingMessage\n label={label('loading', 'Loading addresses\u2026')}\n className=\"min-h-[120px] w-full justify-center border-0 bg-transparent p-0\"\n />\n </div>\n ) : (\n <AddressTiles\n addresses={displayAddresses}\n onCreate={handleCreate}\n onUpdate={handleUpdate}\n onDelete={handleDelete}\n isSubmitting={isSubmitting}\n emptyLabel={emptyLabel}\n t={t}\n hideAddButton\n onAddActionChange={handleAddActionChange}\n emptyStateTitle={emptyState.title}\n emptyStateActionLabel={emptyState.actionLabel}\n labelPrefix={labelPrefix}\n addressTypesAdapter={addressTypesAdapter}\n addressTypesContext={addressTypesContext}\n loadFormat={loadFormat}\n formatContext={formatContext}\n />\n )}\n </div>\n )\n}\n\nexport function AddressesSection<C = unknown>(props: AddressesSectionProps<C>) {\n const handle = ComponentReplacementHandles.section('ui.detail', 'AddressesSection')\n const Resolved = useRegisteredComponent<AddressesSectionProps<C>>(\n handle,\n AddressesSectionImpl as React.ComponentType<AddressesSectionProps<C>>,\n )\n\n return (\n <div data-component-handle={handle}>\n <Resolved {...props} />\n </div>\n )\n}\n\nexport default AddressesSection\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
SelectValue
|
|
14
14
|
} from "@open-mercato/ui/primitives/select";
|
|
15
15
|
import { flash } from "../FlashMessages.js";
|
|
16
|
+
import { surfaceRecordConflict } from "../conflicts/index.js";
|
|
16
17
|
import { SwitchableMarkdownInput } from "../inputs/SwitchableMarkdownInput.js";
|
|
17
18
|
import { ErrorMessage } from "./ErrorMessage.js";
|
|
18
19
|
import { LoadingMessage } from "./LoadingMessage.js";
|
|
@@ -86,6 +87,7 @@ function mapCommentSummary(input) {
|
|
|
86
87
|
const id = typeof data.id === "string" ? data.id : generateTempId();
|
|
87
88
|
const body = typeof data.body === "string" ? data.body : "";
|
|
88
89
|
const createdAt = typeof data.createdAt === "string" ? data.createdAt : typeof data.created_at === "string" ? data.created_at : (/* @__PURE__ */ new Date()).toISOString();
|
|
90
|
+
const updatedAt = typeof data.updatedAt === "string" ? data.updatedAt : typeof data.updated_at === "string" ? data.updated_at : null;
|
|
89
91
|
const authorUserId = typeof data.authorUserId === "string" ? data.authorUserId : typeof data.author_user_id === "string" ? data.author_user_id : null;
|
|
90
92
|
const authorName = typeof data.authorName === "string" ? data.authorName : typeof data.author_name === "string" ? data.author_name : null;
|
|
91
93
|
const authorEmail = typeof data.authorEmail === "string" ? data.authorEmail : typeof data.author_email === "string" ? data.author_email : null;
|
|
@@ -97,6 +99,7 @@ function mapCommentSummary(input) {
|
|
|
97
99
|
id,
|
|
98
100
|
body,
|
|
99
101
|
createdAt,
|
|
102
|
+
updatedAt,
|
|
100
103
|
authorUserId,
|
|
101
104
|
authorName,
|
|
102
105
|
authorEmail,
|
|
@@ -463,7 +466,7 @@ function NotesSectionImpl({
|
|
|
463
466
|
return true;
|
|
464
467
|
} catch (err) {
|
|
465
468
|
const message = err instanceof Error ? err.message : label("error");
|
|
466
|
-
flash(message, "error");
|
|
469
|
+
if (!surfaceRecordConflict(err, t)) flash(message, "error");
|
|
467
470
|
return false;
|
|
468
471
|
} finally {
|
|
469
472
|
setIsSubmitting(false);
|
|
@@ -477,6 +480,7 @@ function NotesSectionImpl({
|
|
|
477
480
|
const sanitizedBody = patch.body;
|
|
478
481
|
const sanitizedIcon = patch.appearanceIcon !== void 0 && patch.appearanceIcon !== null && patch.appearanceIcon.trim().length ? patch.appearanceIcon.trim() : patch.appearanceIcon === null ? null : void 0;
|
|
479
482
|
const sanitizedColor = patch.appearanceColor !== void 0 ? sanitizeHexColor(patch.appearanceColor ?? null) : void 0;
|
|
483
|
+
const noteUpdatedAt = notes.find((comment) => comment.id === noteId)?.updatedAt ?? null;
|
|
480
484
|
try {
|
|
481
485
|
await dataAdapter.update({
|
|
482
486
|
id: noteId,
|
|
@@ -485,6 +489,7 @@ function NotesSectionImpl({
|
|
|
485
489
|
appearanceIcon: sanitizedIcon,
|
|
486
490
|
appearanceColor: sanitizedColor
|
|
487
491
|
},
|
|
492
|
+
updatedAt: noteUpdatedAt,
|
|
488
493
|
context: dataContext
|
|
489
494
|
});
|
|
490
495
|
setNotes((prev) => {
|
|
@@ -501,11 +506,11 @@ function NotesSectionImpl({
|
|
|
501
506
|
flash(label("updateSuccess"), "success");
|
|
502
507
|
} catch (error) {
|
|
503
508
|
const message = error instanceof Error ? error.message : label("updateError");
|
|
504
|
-
flash(message, "error");
|
|
509
|
+
if (!surfaceRecordConflict(error, t)) flash(message, "error");
|
|
505
510
|
throw error instanceof Error ? error : new Error(message);
|
|
506
511
|
}
|
|
507
512
|
},
|
|
508
|
-
[dataAdapter, dataContext, t]
|
|
513
|
+
[dataAdapter, dataContext, notes, t]
|
|
509
514
|
);
|
|
510
515
|
const handleDeleteNote = React.useCallback(
|
|
511
516
|
async (note) => {
|
|
@@ -517,7 +522,7 @@ function NotesSectionImpl({
|
|
|
517
522
|
setDeletingNoteId(note.id);
|
|
518
523
|
pushLoading();
|
|
519
524
|
try {
|
|
520
|
-
await dataAdapter.delete({ id: note.id, context: dataContext });
|
|
525
|
+
await dataAdapter.delete({ id: note.id, updatedAt: note.updatedAt ?? null, context: dataContext });
|
|
521
526
|
setNotes((prev) => prev.filter((existing) => existing.id !== note.id));
|
|
522
527
|
if (pagedMode) {
|
|
523
528
|
setVisibleCount((prev) => Math.max(0, prev - 1));
|
|
@@ -525,13 +530,13 @@ function NotesSectionImpl({
|
|
|
525
530
|
flash(label("deleteSuccess", "Note deleted"), "success");
|
|
526
531
|
} catch (err) {
|
|
527
532
|
const message = err instanceof Error ? err.message : label("deleteError", "Failed to delete note");
|
|
528
|
-
flash(message, "error");
|
|
533
|
+
if (!surfaceRecordConflict(err, t)) flash(message, "error");
|
|
529
534
|
} finally {
|
|
530
535
|
setDeletingNoteId(null);
|
|
531
536
|
popLoading();
|
|
532
537
|
}
|
|
533
538
|
},
|
|
534
|
-
[confirm, dataAdapter, dataContext, label, popLoading, pushLoading]
|
|
539
|
+
[confirm, dataAdapter, dataContext, label, popLoading, pushLoading, t]
|
|
535
540
|
);
|
|
536
541
|
const handleSubmit = React.useCallback(
|
|
537
542
|
async (event) => {
|