@open-mercato/ui 0.6.5-develop.4534.1.b459babe6d → 0.6.5-develop.4559.1.839e136509
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 +57 -11
- 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 +99 -9
- 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.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
package/dist/backend/CrudForm.js
CHANGED
|
@@ -32,6 +32,7 @@ import { FormFooter } from "./forms/FormFooter.js";
|
|
|
32
32
|
import { Button } from "../primitives/button.js";
|
|
33
33
|
import { IconButton } from "../primitives/icon-button.js";
|
|
34
34
|
import {
|
|
35
|
+
Check,
|
|
35
36
|
Settings,
|
|
36
37
|
Layers,
|
|
37
38
|
Tag,
|
|
@@ -80,6 +81,8 @@ import { TimePicker } from "./inputs/TimePicker.js";
|
|
|
80
81
|
import { DatePicker } from "./inputs/DatePicker.js";
|
|
81
82
|
import { mapCrudServerErrorToFormErrors, parseServerMessage } from "./utils/serverErrors.js";
|
|
82
83
|
import { withScopedApiRequestHeaders } from "./utils/apiCall.js";
|
|
84
|
+
import { buildOptimisticLockHeader, extractOptimisticLockConflict } from "./utils/optimisticLock.js";
|
|
85
|
+
import { surfaceRecordConflict } from "./conflicts/index.js";
|
|
83
86
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../primitives/dialog.js";
|
|
84
87
|
import { FieldDefinitionsManager } from "./custom-fields/FieldDefinitionsManager.js";
|
|
85
88
|
import { useConfirmDialog } from "./confirm-dialog/index.js";
|
|
@@ -351,6 +354,8 @@ function CrudForm({
|
|
|
351
354
|
onSubmit,
|
|
352
355
|
onDelete,
|
|
353
356
|
deleteVisible,
|
|
357
|
+
optimisticLockUpdatedAt,
|
|
358
|
+
disableOptimisticLock = false,
|
|
354
359
|
twoColumn = false,
|
|
355
360
|
title,
|
|
356
361
|
backHref,
|
|
@@ -458,6 +463,16 @@ function CrudForm({
|
|
|
458
463
|
}, [values]);
|
|
459
464
|
const fallbackRecordId = recordId || (versionHistory?.resourceId === void 0 || versionHistory.resourceId === null ? void 0 : String(versionHistory.resourceId).trim() || void 0);
|
|
460
465
|
const operation = recordId ? "update" : "create";
|
|
466
|
+
const resolvedOptimisticLockUpdatedAt = React.useMemo(() => {
|
|
467
|
+
if (disableOptimisticLock) return null;
|
|
468
|
+
if (optimisticLockUpdatedAt !== void 0) return optimisticLockUpdatedAt;
|
|
469
|
+
const source = initialValues;
|
|
470
|
+
const camel = source?.updatedAt;
|
|
471
|
+
if (typeof camel === "string" && camel.length > 0) return camel;
|
|
472
|
+
const snake = source?.updated_at;
|
|
473
|
+
if (typeof snake === "string" && snake.length > 0) return snake;
|
|
474
|
+
return null;
|
|
475
|
+
}, [disableOptimisticLock, optimisticLockUpdatedAt, initialValues]);
|
|
461
476
|
const injectionContext = React.useMemo(() => ({
|
|
462
477
|
formId,
|
|
463
478
|
entityId: primaryEntityId,
|
|
@@ -883,8 +898,13 @@ function CrudForm({
|
|
|
883
898
|
return;
|
|
884
899
|
}
|
|
885
900
|
}
|
|
886
|
-
|
|
887
|
-
|
|
901
|
+
const optimisticLockHeader = buildOptimisticLockHeader(resolvedOptimisticLockUpdatedAt);
|
|
902
|
+
const mergedDeleteHeaders = {
|
|
903
|
+
...injectionRequestHeaders ?? {},
|
|
904
|
+
...optimisticLockHeader
|
|
905
|
+
};
|
|
906
|
+
if (Object.keys(mergedDeleteHeaders).length > 0) {
|
|
907
|
+
await withScopedApiRequestHeaders(mergedDeleteHeaders, async () => {
|
|
888
908
|
await onDelete();
|
|
889
909
|
});
|
|
890
910
|
} else {
|
|
@@ -928,10 +948,18 @@ function CrudForm({
|
|
|
928
948
|
}
|
|
929
949
|
} catch {
|
|
930
950
|
}
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
951
|
+
const optimisticLockConflict = extractOptimisticLockConflict(err);
|
|
952
|
+
if (optimisticLockConflict) {
|
|
953
|
+
try {
|
|
954
|
+
surfaceRecordConflict(err, t);
|
|
955
|
+
} catch {
|
|
956
|
+
}
|
|
957
|
+
} else {
|
|
958
|
+
const message = err instanceof Error && err.message ? err.message : deleteErrorMessage;
|
|
959
|
+
try {
|
|
960
|
+
flash(message, "error");
|
|
961
|
+
} catch {
|
|
962
|
+
}
|
|
935
963
|
}
|
|
936
964
|
} finally {
|
|
937
965
|
deletingRef.current = false;
|
|
@@ -2041,8 +2069,13 @@ function CrudForm({
|
|
|
2041
2069
|
}
|
|
2042
2070
|
try {
|
|
2043
2071
|
submitNavigationBypassRef.current = true;
|
|
2044
|
-
|
|
2045
|
-
|
|
2072
|
+
const optimisticLockHeader = buildOptimisticLockHeader(resolvedOptimisticLockUpdatedAt);
|
|
2073
|
+
const mergedSubmitHeaders = {
|
|
2074
|
+
...injectionRequestHeaders ?? {},
|
|
2075
|
+
...optimisticLockHeader
|
|
2076
|
+
};
|
|
2077
|
+
if (Object.keys(mergedSubmitHeaders).length > 0) {
|
|
2078
|
+
await withScopedApiRequestHeaders(mergedSubmitHeaders, async () => {
|
|
2046
2079
|
await onSubmit?.(coreSubmitValues, submitContext);
|
|
2047
2080
|
});
|
|
2048
2081
|
} else {
|
|
@@ -2093,7 +2126,8 @@ function CrudForm({
|
|
|
2093
2126
|
console.debug("[crud-form] Submission failed with field errors", transformedErrors);
|
|
2094
2127
|
}
|
|
2095
2128
|
}
|
|
2096
|
-
|
|
2129
|
+
const optimisticLockConflict = extractOptimisticLockConflict(err);
|
|
2130
|
+
let displayMessage = optimisticLockConflict ? t("ui.forms.flash.recordModified", "This record was modified by someone else. Refresh and try again.") : typeof helperMessage === "string" && helperMessage.trim() ? helperMessage.trim() : "";
|
|
2097
2131
|
if (hasFieldErrors) {
|
|
2098
2132
|
const lowered = displayMessage.toLowerCase();
|
|
2099
2133
|
const highlightedLower = highlightedMessage.toLowerCase();
|
|
@@ -2108,7 +2142,12 @@ function CrudForm({
|
|
|
2108
2142
|
displayMessage = hasFieldErrors ? highlightedMessage : saveErrorMessage;
|
|
2109
2143
|
}
|
|
2110
2144
|
displayMessage = parseServerMessage(displayMessage);
|
|
2111
|
-
if (
|
|
2145
|
+
if (optimisticLockConflict) {
|
|
2146
|
+
try {
|
|
2147
|
+
surfaceRecordConflict(err, t);
|
|
2148
|
+
} catch {
|
|
2149
|
+
}
|
|
2150
|
+
} else if (!hasFieldErrors) {
|
|
2112
2151
|
flash(displayMessage, "error");
|
|
2113
2152
|
}
|
|
2114
2153
|
setFormError(displayMessage);
|
|
@@ -3195,7 +3234,14 @@ const ListboxMultiSelect = React.memo(function ListboxMultiSelect2({
|
|
|
3195
3234
|
onClick: () => toggle(opt.value),
|
|
3196
3235
|
className: `w-full justify-start rounded-none font-normal px-3 py-2 ${isSel ? "bg-muted" : ""}`,
|
|
3197
3236
|
children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
|
|
3198
|
-
/* @__PURE__ */ jsx(
|
|
3237
|
+
/* @__PURE__ */ jsx(
|
|
3238
|
+
"span",
|
|
3239
|
+
{
|
|
3240
|
+
"aria-hidden": "true",
|
|
3241
|
+
className: `inline-flex size-4 shrink-0 items-center justify-center rounded-[4px] border transition-colors ${isSel ? "border-accent-indigo bg-accent-indigo text-accent-indigo-foreground" : "border-input bg-background"}`,
|
|
3242
|
+
children: isSel ? /* @__PURE__ */ jsx(Check, { className: "size-3.5", "aria-hidden": "true" }) : null
|
|
3243
|
+
}
|
|
3244
|
+
),
|
|
3199
3245
|
/* @__PURE__ */ jsx("span", { children: opt.label })
|
|
3200
3246
|
] })
|
|
3201
3247
|
},
|