@open-mercato/core 0.10.1-develop.7994.1.c9f31972aa → 0.10.1-develop.8022.1.b6753de892
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/audit_logs/components/AuditLogsActions.js +80 -50
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +3 -3
- package/dist/modules/audit_logs/lib/rowPresentation.js +82 -0
- package/dist/modules/audit_logs/lib/rowPresentation.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +89 -16
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +7 -0
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +2 -2
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +10 -21
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260825140000_customers.js +17 -10
- package/dist/modules/customers/migrations/Migration20260825140000_customers.js.map +2 -2
- package/dist/modules/entities/cli.js +138 -66
- package/dist/modules/entities/cli.js.map +3 -3
- package/dist/modules/integrations/lib/state-service.js +1 -1
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/promotions/commands/codes.js +18 -3
- package/dist/modules/promotions/commands/codes.js.map +2 -2
- package/dist/modules/query_index/cli.js +17 -1
- package/dist/modules/query_index/cli.js.map +2 -2
- package/dist/modules/query_index/lib/reindexer.js +23 -5
- package/dist/modules/query_index/lib/reindexer.js.map +2 -2
- package/dist/modules/resources/commands/resource-types.js +42 -15
- package/dist/modules/resources/commands/resource-types.js.map +2 -2
- package/dist/modules/resources/commands/resources.js +54 -3
- package/dist/modules/resources/commands/resources.js.map +2 -2
- package/dist/modules/resources/lib/resourceTypeLock.js +15 -0
- package/dist/modules/resources/lib/resourceTypeLock.js.map +7 -0
- package/dist/modules/sales/commands/configuration.js +30 -16
- package/dist/modules/sales/commands/configuration.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +36 -15
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +35 -6
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +85 -48
- package/src/modules/audit_logs/i18n/de.json +16 -0
- package/src/modules/audit_logs/i18n/en.json +16 -0
- package/src/modules/audit_logs/i18n/es.json +16 -0
- package/src/modules/audit_logs/i18n/ko.json +16 -0
- package/src/modules/audit_logs/i18n/pl.json +16 -0
- package/src/modules/audit_logs/lib/rowPresentation.ts +92 -0
- package/src/modules/catalog/commands/variants.ts +96 -17
- package/src/modules/catalog/seed/examples.ts +7 -0
- package/src/modules/customers/api/interactions/route.ts +2 -2
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +9 -12
- package/src/modules/customers/i18n/de.json +2 -1
- package/src/modules/customers/i18n/en.json +2 -1
- package/src/modules/customers/i18n/es.json +2 -1
- package/src/modules/customers/i18n/ko.json +2 -1
- package/src/modules/customers/i18n/pl.json +2 -1
- package/src/modules/customers/migrations/Migration20260825140000_customers.ts +21 -10
- package/src/modules/entities/cli.ts +144 -65
- package/src/modules/integrations/lib/state-service.ts +1 -1
- package/src/modules/promotions/commands/codes.ts +19 -4
- package/src/modules/query_index/cli.ts +17 -1
- package/src/modules/query_index/lib/reindexer.ts +55 -6
- package/src/modules/resources/commands/resource-types.ts +47 -15
- package/src/modules/resources/commands/resources.ts +74 -3
- package/src/modules/resources/i18n/de.json +3 -0
- package/src/modules/resources/i18n/en.json +3 -0
- package/src/modules/resources/i18n/es.json +3 -0
- package/src/modules/resources/i18n/ko.json +3 -0
- package/src/modules/resources/i18n/pl.json +3 -0
- package/src/modules/resources/lib/resourceTypeLock.ts +14 -0
- package/src/modules/sales/commands/configuration.ts +35 -16
- package/src/modules/sales/components/documents/AddressesSection.tsx +51 -20
- package/src/modules/sales/i18n/de.json +3 -0
- package/src/modules/sales/i18n/en.json +3 -0
- package/src/modules/sales/i18n/es.json +3 -0
- package/src/modules/sales/i18n/ko.json +3 -0
- package/src/modules/sales/i18n/pl.json +4 -1
- package/src/modules/sales/seed/examples.ts +41 -6
package/.turbo/turbo-build.log
CHANGED
|
@@ -4,6 +4,8 @@ import * as React from "react";
|
|
|
4
4
|
import { extensionPoints } from "@open-mercato/core/modules/audit_logs/extension-points";
|
|
5
5
|
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
6
6
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
7
|
+
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
8
|
+
import { auditRecordName, auditChangeSummary, auditExecutionState } from "../lib/rowPresentation.js";
|
|
7
9
|
import { apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
8
10
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
9
11
|
import { ActionLogDetailsDialog } from "./ActionLogDetailsDialog.js";
|
|
@@ -13,6 +15,9 @@ import { useAuditPermissions, canUndoEntry, canRedoEntry } from "@open-mercato/u
|
|
|
13
15
|
import { Alert, AlertDescription } from "@open-mercato/ui/primitives/alert";
|
|
14
16
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
15
17
|
const logger = createLogger("audit_logs").child({ component: "AuditLogsActions" });
|
|
18
|
+
function AuditLogRowActions({ children }) {
|
|
19
|
+
return /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-1", "data-actions-cell": true, children });
|
|
20
|
+
}
|
|
16
21
|
function resolveServerReason(err) {
|
|
17
22
|
if (!(err instanceof Error)) return void 0;
|
|
18
23
|
const status = err.status;
|
|
@@ -107,12 +112,27 @@ function AuditLogsActions({
|
|
|
107
112
|
{
|
|
108
113
|
accessorKey: "actionLabel",
|
|
109
114
|
header: t("audit_logs.actions.columns.action"),
|
|
110
|
-
cell: (info) =>
|
|
115
|
+
cell: (info) => {
|
|
116
|
+
const item = info.row.original;
|
|
117
|
+
const summary = auditChangeSummary(item, t);
|
|
118
|
+
return /* @__PURE__ */ jsxs("div", { className: "max-w-72", children: [
|
|
119
|
+
/* @__PURE__ */ jsx("div", { children: item.actionLabel || item.commandId }),
|
|
120
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-xs text-muted-foreground", title: summary, children: summary })
|
|
121
|
+
] });
|
|
122
|
+
}
|
|
111
123
|
},
|
|
112
124
|
{
|
|
113
125
|
accessorKey: "resourceKind",
|
|
114
126
|
header: t("audit_logs.actions.columns.resource"),
|
|
115
|
-
cell: (info) =>
|
|
127
|
+
cell: (info) => {
|
|
128
|
+
const item = info.row.original;
|
|
129
|
+
const name = auditRecordName(item);
|
|
130
|
+
const resource = formatResource(item, noneLabel);
|
|
131
|
+
return /* @__PURE__ */ jsxs("div", { className: "max-w-72", children: [
|
|
132
|
+
name ? /* @__PURE__ */ jsx("div", { className: "truncate font-medium", title: name, children: name }) : null,
|
|
133
|
+
/* @__PURE__ */ jsx("div", { className: name ? "truncate text-xs text-muted-foreground" : "truncate", title: resource, children: resource })
|
|
134
|
+
] });
|
|
135
|
+
}
|
|
116
136
|
},
|
|
117
137
|
{
|
|
118
138
|
accessorKey: "actorUserId",
|
|
@@ -123,13 +143,13 @@ function AuditLogsActions({
|
|
|
123
143
|
{
|
|
124
144
|
accessorKey: "tenantId",
|
|
125
145
|
header: t("audit_logs.actions.columns.tenant"),
|
|
126
|
-
cell: (info) => info.row.original.tenantName || info.
|
|
146
|
+
cell: (info) => info.row.original.tenantName || info.row.original.tenantId || noneLabel,
|
|
127
147
|
meta: { priority: 4 }
|
|
128
148
|
},
|
|
129
149
|
{
|
|
130
150
|
accessorKey: "organizationId",
|
|
131
151
|
header: t("audit_logs.actions.columns.organization"),
|
|
132
|
-
cell: (info) => info.row.original.organizationName || info.
|
|
152
|
+
cell: (info) => info.row.original.organizationName || info.row.original.organizationId || noneLabel,
|
|
133
153
|
meta: { priority: 4 }
|
|
134
154
|
},
|
|
135
155
|
{
|
|
@@ -139,55 +159,63 @@ function AuditLogsActions({
|
|
|
139
159
|
},
|
|
140
160
|
{
|
|
141
161
|
accessorKey: "executionState",
|
|
142
|
-
header: t("audit_logs.actions.columns.status")
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
id: "controls",
|
|
146
|
-
header: t("audit_logs.actions.columns.controls"),
|
|
147
|
-
enableSorting: false,
|
|
148
|
-
cell: (info) => {
|
|
149
|
-
const item = info.row.original;
|
|
150
|
-
const itemCanUndo = canUndoEntry(permissions, item.actorUserId);
|
|
151
|
-
const itemCanRedo = canRedoEntry(permissions, item.actorUserId);
|
|
152
|
-
const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === "done" && isLatestUndoableForItem(item);
|
|
153
|
-
const showRedo = itemCanRedo && item.executionState === "undone";
|
|
154
|
-
const canRedo = showRedo && isRedoCandidate(item);
|
|
155
|
-
if (!canUndo && !showRedo) return null;
|
|
156
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1", "data-actions-cell": true, children: [
|
|
157
|
-
canUndo ? /* @__PURE__ */ jsx(
|
|
158
|
-
Button,
|
|
159
|
-
{
|
|
160
|
-
variant: "ghost",
|
|
161
|
-
size: "icon",
|
|
162
|
-
"aria-label": t("audit_logs.actions.undo"),
|
|
163
|
-
onClick: () => {
|
|
164
|
-
void handleUndo(item.undoToken);
|
|
165
|
-
},
|
|
166
|
-
disabled: undoingToken === item.undoToken || Boolean(redoingId),
|
|
167
|
-
children: /* @__PURE__ */ jsx(Undo2, { className: "size-4", "aria-hidden": "true" })
|
|
168
|
-
}
|
|
169
|
-
) : null,
|
|
170
|
-
showRedo ? /* @__PURE__ */ jsx(
|
|
171
|
-
Button,
|
|
172
|
-
{
|
|
173
|
-
variant: "ghost",
|
|
174
|
-
size: "icon",
|
|
175
|
-
"aria-label": t("audit_logs.actions.redo"),
|
|
176
|
-
onClick: () => {
|
|
177
|
-
void handleRedo(item.id);
|
|
178
|
-
},
|
|
179
|
-
disabled: !canRedo || redoingId === item.id || Boolean(undoingToken),
|
|
180
|
-
children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4", "aria-hidden": "true" })
|
|
181
|
-
}
|
|
182
|
-
) : null
|
|
183
|
-
] });
|
|
184
|
-
},
|
|
185
|
-
meta: { align: "right" }
|
|
162
|
+
header: t("audit_logs.actions.columns.status"),
|
|
163
|
+
cell: (info) => auditExecutionState(info.row.original.executionState, t)
|
|
186
164
|
}
|
|
187
|
-
], [t, noneLabel
|
|
165
|
+
], [t, noneLabel]);
|
|
166
|
+
const rowActions = React.useCallback((item) => {
|
|
167
|
+
const itemCanUndo = canUndoEntry(permissions, item.actorUserId);
|
|
168
|
+
const itemCanRedo = canRedoEntry(permissions, item.actorUserId);
|
|
169
|
+
const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === "done" && isLatestUndoableForItem(item);
|
|
170
|
+
const showRedo = itemCanRedo && item.executionState === "undone";
|
|
171
|
+
const canRedo = showRedo && isRedoCandidate(item);
|
|
172
|
+
if (!canUndo && !showRedo) return null;
|
|
173
|
+
const undoDisabled = undoingToken === item.undoToken || Boolean(redoingId);
|
|
174
|
+
const redoDisabled = !canRedo || redoingId === item.id || Boolean(undoingToken);
|
|
175
|
+
const undoLabel = t("audit_logs.actions.undo_this");
|
|
176
|
+
const redoLabel = t("audit_logs.actions.redo_this");
|
|
177
|
+
const onUndo = () => {
|
|
178
|
+
void handleUndo(item.undoToken);
|
|
179
|
+
};
|
|
180
|
+
const onRedo = () => {
|
|
181
|
+
void handleRedo(item.id);
|
|
182
|
+
};
|
|
183
|
+
const items2 = [];
|
|
184
|
+
if (canUndo && !undoDisabled) items2.push({ id: "undo", label: undoLabel, onSelect: onUndo });
|
|
185
|
+
if (canRedo && !redoDisabled) items2.push({ id: "redo", label: redoLabel, onSelect: onRedo });
|
|
186
|
+
return /* @__PURE__ */ jsxs(AuditLogRowActions, { items: items2, children: [
|
|
187
|
+
canUndo ? /* @__PURE__ */ jsx(
|
|
188
|
+
IconButton,
|
|
189
|
+
{
|
|
190
|
+
variant: "ghost",
|
|
191
|
+
size: "lg",
|
|
192
|
+
type: "button",
|
|
193
|
+
"aria-label": undoLabel,
|
|
194
|
+
title: undoLabel,
|
|
195
|
+
onClick: onUndo,
|
|
196
|
+
disabled: undoDisabled,
|
|
197
|
+
children: /* @__PURE__ */ jsx(Undo2, { className: "size-4", "aria-hidden": "true" })
|
|
198
|
+
}
|
|
199
|
+
) : null,
|
|
200
|
+
showRedo ? /* @__PURE__ */ jsx(
|
|
201
|
+
IconButton,
|
|
202
|
+
{
|
|
203
|
+
variant: "ghost",
|
|
204
|
+
size: "lg",
|
|
205
|
+
type: "button",
|
|
206
|
+
"aria-label": redoLabel,
|
|
207
|
+
title: redoLabel,
|
|
208
|
+
onClick: onRedo,
|
|
209
|
+
disabled: redoDisabled,
|
|
210
|
+
children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4", "aria-hidden": "true" })
|
|
211
|
+
}
|
|
212
|
+
) : null
|
|
213
|
+
] });
|
|
214
|
+
}, [t, handleUndo, handleRedo, isLatestUndoableForItem, isRedoCandidate, undoingToken, redoingId, permissions]);
|
|
188
215
|
const undoButton = latestUndoable?.undoToken && canUndoEntry(permissions, latestUndoable.actorUserId) ? /* @__PURE__ */ jsx(
|
|
189
216
|
Button,
|
|
190
217
|
{
|
|
218
|
+
type: "button",
|
|
191
219
|
variant: "secondary",
|
|
192
220
|
size: "sm",
|
|
193
221
|
onClick: () => {
|
|
@@ -211,6 +239,8 @@ function AuditLogsActions({
|
|
|
211
239
|
titleHeadingLevel: 2,
|
|
212
240
|
data: actionItems,
|
|
213
241
|
columns,
|
|
242
|
+
rowActions,
|
|
243
|
+
stickyActionsColumn: true,
|
|
214
244
|
actions: combinedActions,
|
|
215
245
|
perspective: { tableId: extensionPoints.hosts.actionsTable.tableId },
|
|
216
246
|
isLoading: Boolean(isLoading) || Boolean(undoingToken) || Boolean(redoingId),
|
|
@@ -242,7 +272,7 @@ function formatDate(value) {
|
|
|
242
272
|
if (Number.isNaN(date.getTime())) return value;
|
|
243
273
|
return new Intl.DateTimeFormat(void 0, {
|
|
244
274
|
dateStyle: "medium",
|
|
245
|
-
timeStyle: "
|
|
275
|
+
timeStyle: "medium"
|
|
246
276
|
}).format(date);
|
|
247
277
|
}
|
|
248
278
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/audit_logs/components/AuditLogsActions.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/audit_logs/extension-points'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable, type PaginationProps } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ActionLogDetailsDialog } from './ActionLogDetailsDialog'\nimport { Undo2, RotateCcw } from 'lucide-react'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from '@open-mercato/ui/backend/version-history'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'AuditLogsActions' })\n\n/**\n * `raiseCrudError` prefers the server's `error` body over the fallback message and stamps\n * `status` on anything it builds from an HTTP response, so a numeric `status` is what\n * distinguishes a route's refusal from a client-side failure. Without that check an\n * offline or aborted request would put a raw \"Failed to fetch\" into the operator's\n * banner; those keep the generic fallback instead.\n */\nfunction resolveServerReason(err: unknown): string | undefined {\n if (!(err instanceof Error)) return undefined\n const status = (err as Error & { status?: unknown }).status\n if (typeof status !== 'number') return undefined\n const message = err.message?.trim()\n return message ? message : undefined\n}\n\nexport type ActionLogItem = {\n id: string\n commandId: string\n actionLabel: string | null\n executionState: string\n actorUserId: string | null\n actorUserName: string | null\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n resourceKind: string | null\n resourceId: string | null\n undoToken: string | null\n createdAt: string\n updatedAt: string\n snapshotBefore?: unknown | null\n snapshotAfter?: unknown | null\n changes?: Record<string, unknown> | null\n context?: Record<string, unknown> | null\n}\n\nexport function AuditLogsActions({\n items,\n onRefresh,\n isLoading,\n headerExtras,\n onUndoError,\n onRedoError,\n pagination,\n}: {\n items: ActionLogItem[] | undefined\n onRefresh: () => Promise<void>\n isLoading?: boolean\n headerExtras?: React.ReactNode\n /** Receives the server's reason when it sent one, so the page can show why the undo was refused. */\n onUndoError?: (reason?: string) => void\n onRedoError?: (reason?: string) => void\n pagination?: PaginationProps\n}) {\n const t = useT()\n const permissions = useAuditPermissions(true)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const [selected, setSelected] = React.useState<ActionLogItem | null>(null)\n const actionItems = Array.isArray(items) ? items : []\n const latestUndoable = React.useMemo(() => actionItems.find((item) => !!item.undoToken && item.executionState === 'done'), [actionItems])\n const noneLabel = t('audit_logs.common.none')\n const latestPerResource = React.useMemo(() => {\n const map = new Map<string, string>()\n let fallback: string | null = null\n for (const item of actionItems) {\n if (!item.undoToken || item.executionState !== 'done') continue\n const key = buildResourceKey(item)\n if (key) {\n if (!map.has(key)) map.set(key, item.id)\n } else if (!fallback) {\n fallback = item.id\n }\n }\n return { map, fallback }\n }, [actionItems])\n const latestUndoneId = React.useMemo(() => {\n const undone = actionItems.filter((item) => item.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [actionItems])\n\n const isLatestUndoableForItem = React.useCallback((item: ActionLogItem) => {\n const key = buildResourceKey(item)\n if (key) return latestPerResource.map.get(key) === item.id\n return latestPerResource.fallback === item.id\n }, [latestPerResource])\n\n const isRedoCandidate = React.useCallback((item: ActionLogItem) => item.executionState === 'undone' && latestUndoneId === item.id, [latestUndoneId])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n await onRefresh()\n } catch (err) {\n logger.error('Undo action failed', { err })\n onUndoError?.(resolveServerReason(err))\n } finally {\n setUndoingToken(null)\n }\n }, [onRefresh, onUndoError, t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n await onRefresh()\n } catch (err) {\n logger.error('Redo action failed', { err })\n onRedoError?.(resolveServerReason(err))\n } finally {\n setRedoingId(null)\n }\n }, [onRefresh, onRedoError, t])\n\n const columns = React.useMemo<ColumnDef<ActionLogItem, any>[]>(() => [\n {\n accessorKey: 'actionLabel',\n header: t('audit_logs.actions.columns.action'),\n cell: (info) => info.row.original.actionLabel || info.row.original.commandId,\n },\n {\n accessorKey: 'resourceKind',\n header: t('audit_logs.actions.columns.resource'),\n cell: (info) => formatResource(info.row.original, noneLabel),\n },\n {\n accessorKey: 'actorUserId',\n header: t('audit_logs.actions.columns.user'),\n cell: (info) => info.row.original.actorUserName || noneLabel,\n meta: { priority: 3 },\n },\n {\n accessorKey: 'tenantId',\n header: t('audit_logs.actions.columns.tenant'),\n cell: (info) => info.row.original.tenantName || info.getValue() || noneLabel,\n meta: { priority: 4 },\n },\n {\n accessorKey: 'organizationId',\n header: t('audit_logs.actions.columns.organization'),\n cell: (info) => info.row.original.organizationName || info.getValue() || noneLabel,\n meta: { priority: 4 },\n },\n {\n accessorKey: 'createdAt',\n header: t('audit_logs.actions.columns.when'),\n cell: (info) => formatDate(info.getValue() as string),\n },\n {\n accessorKey: 'executionState',\n header: t('audit_logs.actions.columns.status'),\n },\n {\n id: 'controls',\n header: t('audit_logs.actions.columns.controls'),\n enableSorting: false,\n cell: (info) => {\n const item = info.row.original\n const itemCanUndo = canUndoEntry(permissions, item.actorUserId)\n const itemCanRedo = canRedoEntry(permissions, item.actorUserId)\n const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === 'done' && isLatestUndoableForItem(item)\n const showRedo = itemCanRedo && item.executionState === 'undone'\n const canRedo = showRedo && isRedoCandidate(item)\n if (!canUndo && !showRedo) return null\n return (\n <div className=\"flex justify-end gap-1\" data-actions-cell>\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={() => { void handleUndo(item.undoToken) }}\n disabled={undoingToken === item.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={() => { void handleRedo(item.id) }}\n disabled={!canRedo || redoingId === item.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n </Button>\n ) : null}\n </div>\n )\n },\n meta: { align: 'right' },\n },\n ], [t, noneLabel, handleUndo, handleRedo, isLatestUndoableForItem, isRedoCandidate, undoingToken, redoingId, permissions])\n\n const undoButton = latestUndoable?.undoToken && canUndoEntry(permissions, latestUndoable.actorUserId) ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => { void handleUndo(latestUndoable.undoToken) }}\n disabled={Boolean(undoingToken) || Boolean(redoingId)}\n >\n {undoingToken ? t('audit_logs.actions.undoing') : t('audit_logs.actions.undo')}\n </Button>\n ) : null\n\n const combinedActions = undoButton || headerExtras\n ? <div className=\"flex items-center gap-2\">{headerExtras}{undoButton}</div>\n : undefined\n\n const showSelfOnlyHint = !permissions.isLoading && !permissions.canViewTenant && !!permissions.currentUserId\n\n return (\n <>\n {showSelfOnlyHint ? (\n <Alert status=\"information\" className=\"mb-4\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n <DataTable<ActionLogItem>\n title={t('audit_logs.actions.title')}\n titleHeadingLevel={2}\n data={actionItems}\n columns={columns}\n actions={combinedActions}\n perspective={{ tableId: extensionPoints.hosts.actionsTable.tableId }}\n isLoading={Boolean(isLoading) || Boolean(undoingToken) || Boolean(redoingId)}\n onRowClick={(item) => setSelected(item)}\n pagination={pagination}\n />\n {selected ? (\n <ActionLogDetailsDialog\n item={selected}\n onClose={() => setSelected(null)}\n />\n ) : null}\n </>\n )\n}\n\nfunction formatResource(item: { resourceKind?: string | null; resourceId?: string | null }, fallback: string) {\n if (!item.resourceKind && !item.resourceId) return fallback\n return [item.resourceKind, item.resourceId].filter(Boolean).join(' \u00B7 ')\n}\n\nfunction buildResourceKey(item: { resourceKind?: string | null; resourceId?: string | null }) {\n const kind = typeof item.resourceKind === 'string' ? item.resourceKind.trim() : ''\n const id = typeof item.resourceId === 'string' ? item.resourceId.trim() : ''\n if (!kind && !id) return null\n return `${kind}::${id}`\n}\n\nfunction formatDate(value: string) {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return value\n return new Intl.DateTimeFormat(undefined, {\n dateStyle: 'medium',\n timeStyle: 'short',\n }).format(date)\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/audit_logs/extension-points'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable, type PaginationProps } from '@open-mercato/ui/backend/DataTable'\nimport type { RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { auditRecordName, auditChangeSummary, auditExecutionState } from '../lib/rowPresentation'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ActionLogDetailsDialog } from './ActionLogDetailsDialog'\nimport { Undo2, RotateCcw } from 'lucide-react'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from '@open-mercato/ui/backend/version-history'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'AuditLogsActions' })\n\nfunction AuditLogRowActions({ children }: { items: RowActionItem[]; children: React.ReactNode }) {\n return <div className=\"flex justify-end gap-1\" data-actions-cell>{children}</div>\n}\n\n/**\n * `raiseCrudError` prefers the server's `error` body over the fallback message and stamps\n * `status` on anything it builds from an HTTP response, so a numeric `status` is what\n * distinguishes a route's refusal from a client-side failure. Without that check an\n * offline or aborted request would put a raw \"Failed to fetch\" into the operator's\n * banner; those keep the generic fallback instead.\n */\nfunction resolveServerReason(err: unknown): string | undefined {\n if (!(err instanceof Error)) return undefined\n const status = (err as Error & { status?: unknown }).status\n if (typeof status !== 'number') return undefined\n const message = err.message?.trim()\n return message ? message : undefined\n}\n\nexport type ActionLogItem = {\n id: string\n commandId: string\n actionLabel: string | null\n executionState: string\n actorUserId: string | null\n actorUserName: string | null\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n resourceKind: string | null\n resourceId: string | null\n undoToken: string | null\n createdAt: string\n updatedAt: string\n snapshotBefore?: unknown | null\n snapshotAfter?: unknown | null\n changes?: Record<string, unknown> | null\n context?: Record<string, unknown> | null\n}\n\nexport function AuditLogsActions({\n items,\n onRefresh,\n isLoading,\n headerExtras,\n onUndoError,\n onRedoError,\n pagination,\n}: {\n items: ActionLogItem[] | undefined\n onRefresh: () => Promise<void>\n isLoading?: boolean\n headerExtras?: React.ReactNode\n /** Receives the server's reason when it sent one, so the page can show why the undo was refused. */\n onUndoError?: (reason?: string) => void\n onRedoError?: (reason?: string) => void\n pagination?: PaginationProps\n}) {\n const t = useT()\n const permissions = useAuditPermissions(true)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const [selected, setSelected] = React.useState<ActionLogItem | null>(null)\n const actionItems = Array.isArray(items) ? items : []\n const latestUndoable = React.useMemo(() => actionItems.find((item) => !!item.undoToken && item.executionState === 'done'), [actionItems])\n const noneLabel = t('audit_logs.common.none')\n const latestPerResource = React.useMemo(() => {\n const map = new Map<string, string>()\n let fallback: string | null = null\n for (const item of actionItems) {\n if (!item.undoToken || item.executionState !== 'done') continue\n const key = buildResourceKey(item)\n if (key) {\n if (!map.has(key)) map.set(key, item.id)\n } else if (!fallback) {\n fallback = item.id\n }\n }\n return { map, fallback }\n }, [actionItems])\n const latestUndoneId = React.useMemo(() => {\n const undone = actionItems.filter((item) => item.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [actionItems])\n\n const isLatestUndoableForItem = React.useCallback((item: ActionLogItem) => {\n const key = buildResourceKey(item)\n if (key) return latestPerResource.map.get(key) === item.id\n return latestPerResource.fallback === item.id\n }, [latestPerResource])\n\n const isRedoCandidate = React.useCallback((item: ActionLogItem) => item.executionState === 'undone' && latestUndoneId === item.id, [latestUndoneId])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n await onRefresh()\n } catch (err) {\n logger.error('Undo action failed', { err })\n onUndoError?.(resolveServerReason(err))\n } finally {\n setUndoingToken(null)\n }\n }, [onRefresh, onUndoError, t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n await onRefresh()\n } catch (err) {\n logger.error('Redo action failed', { err })\n onRedoError?.(resolveServerReason(err))\n } finally {\n setRedoingId(null)\n }\n }, [onRefresh, onRedoError, t])\n\n const columns = React.useMemo<ColumnDef<ActionLogItem, unknown>[]>(() => [\n {\n accessorKey: 'actionLabel',\n header: t('audit_logs.actions.columns.action'),\n cell: (info) => {\n const item = info.row.original\n const summary = auditChangeSummary(item, t)\n return (\n <div className=\"max-w-72\">\n <div>{item.actionLabel || item.commandId}</div>\n <div className=\"truncate text-xs text-muted-foreground\" title={summary}>{summary}</div>\n </div>\n )\n },\n },\n {\n accessorKey: 'resourceKind',\n header: t('audit_logs.actions.columns.resource'),\n cell: (info) => {\n const item = info.row.original\n const name = auditRecordName(item)\n const resource = formatResource(item, noneLabel)\n return (\n <div className=\"max-w-72\">\n {name ? <div className=\"truncate font-medium\" title={name}>{name}</div> : null}\n <div className={name ? 'truncate text-xs text-muted-foreground' : 'truncate'} title={resource}>{resource}</div>\n </div>\n )\n },\n },\n {\n accessorKey: 'actorUserId',\n header: t('audit_logs.actions.columns.user'),\n cell: (info) => info.row.original.actorUserName || noneLabel,\n meta: { priority: 3 },\n },\n {\n accessorKey: 'tenantId',\n header: t('audit_logs.actions.columns.tenant'),\n cell: (info) => info.row.original.tenantName || info.row.original.tenantId || noneLabel,\n meta: { priority: 4 },\n },\n {\n accessorKey: 'organizationId',\n header: t('audit_logs.actions.columns.organization'),\n cell: (info) => info.row.original.organizationName || info.row.original.organizationId || noneLabel,\n meta: { priority: 4 },\n },\n {\n accessorKey: 'createdAt',\n header: t('audit_logs.actions.columns.when'),\n cell: (info) => formatDate(info.getValue() as string),\n },\n {\n accessorKey: 'executionState',\n header: t('audit_logs.actions.columns.status'),\n cell: (info) => auditExecutionState(info.row.original.executionState, t),\n },\n ], [t, noneLabel])\n\n const rowActions = React.useCallback((item: ActionLogItem) => {\n const itemCanUndo = canUndoEntry(permissions, item.actorUserId)\n const itemCanRedo = canRedoEntry(permissions, item.actorUserId)\n const canUndo = itemCanUndo && Boolean(item.undoToken) && item.executionState === 'done' && isLatestUndoableForItem(item)\n const showRedo = itemCanRedo && item.executionState === 'undone'\n const canRedo = showRedo && isRedoCandidate(item)\n if (!canUndo && !showRedo) return null\n const undoDisabled = undoingToken === item.undoToken || Boolean(redoingId)\n const redoDisabled = !canRedo || redoingId === item.id || Boolean(undoingToken)\n const undoLabel = t('audit_logs.actions.undo_this')\n const redoLabel = t('audit_logs.actions.redo_this')\n const onUndo = () => { void handleUndo(item.undoToken) }\n const onRedo = () => { void handleRedo(item.id) }\n const items: RowActionItem[] = []\n if (canUndo && !undoDisabled) items.push({ id: 'undo', label: undoLabel, onSelect: onUndo })\n if (canRedo && !redoDisabled) items.push({ id: 'redo', label: redoLabel, onSelect: onRedo })\n return (\n <AuditLogRowActions items={items}>\n {canUndo ? (\n <IconButton\n variant=\"ghost\"\n size=\"lg\"\n type=\"button\"\n aria-label={undoLabel}\n title={undoLabel}\n onClick={onUndo}\n disabled={undoDisabled}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n </IconButton>\n ) : null}\n {showRedo ? (\n <IconButton\n variant=\"ghost\"\n size=\"lg\"\n type=\"button\"\n aria-label={redoLabel}\n title={redoLabel}\n onClick={onRedo}\n disabled={redoDisabled}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n </IconButton>\n ) : null}\n </AuditLogRowActions>\n )\n }, [t, handleUndo, handleRedo, isLatestUndoableForItem, isRedoCandidate, undoingToken, redoingId, permissions])\n\n const undoButton = latestUndoable?.undoToken && canUndoEntry(permissions, latestUndoable.actorUserId) ? (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => { void handleUndo(latestUndoable.undoToken) }}\n disabled={Boolean(undoingToken) || Boolean(redoingId)}\n >\n {undoingToken ? t('audit_logs.actions.undoing') : t('audit_logs.actions.undo')}\n </Button>\n ) : null\n\n const combinedActions = undoButton || headerExtras\n ? <div className=\"flex items-center gap-2\">{headerExtras}{undoButton}</div>\n : undefined\n\n const showSelfOnlyHint = !permissions.isLoading && !permissions.canViewTenant && !!permissions.currentUserId\n\n return (\n <>\n {showSelfOnlyHint ? (\n <Alert status=\"information\" className=\"mb-4\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n <DataTable<ActionLogItem>\n title={t('audit_logs.actions.title')}\n titleHeadingLevel={2}\n data={actionItems}\n columns={columns}\n rowActions={rowActions}\n stickyActionsColumn\n actions={combinedActions}\n perspective={{ tableId: extensionPoints.hosts.actionsTable.tableId }}\n isLoading={Boolean(isLoading) || Boolean(undoingToken) || Boolean(redoingId)}\n onRowClick={(item) => setSelected(item)}\n pagination={pagination}\n />\n {selected ? (\n <ActionLogDetailsDialog\n item={selected}\n onClose={() => setSelected(null)}\n />\n ) : null}\n </>\n )\n}\n\nfunction formatResource(item: { resourceKind?: string | null; resourceId?: string | null }, fallback: string) {\n if (!item.resourceKind && !item.resourceId) return fallback\n return [item.resourceKind, item.resourceId].filter(Boolean).join(' \u00B7 ')\n}\n\nfunction buildResourceKey(item: { resourceKind?: string | null; resourceId?: string | null }) {\n const kind = typeof item.resourceKind === 'string' ? item.resourceKind.trim() : ''\n const id = typeof item.resourceId === 'string' ? item.resourceId.trim() : ''\n if (!kind && !id) return null\n return `${kind}::${id}`\n}\n\nfunction formatDate(value: string) {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return value\n return new Intl.DateTimeFormat(undefined, {\n dateStyle: 'medium',\n timeStyle: 'medium',\n }).format(date)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["items"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isRecord } from "./changeRows.js";
|
|
2
|
+
const previewFields = [
|
|
3
|
+
["displayName", "audit_logs.summary.name"],
|
|
4
|
+
["display_name", "audit_logs.summary.name"],
|
|
5
|
+
["name", "audit_logs.summary.name"],
|
|
6
|
+
["title", "audit_logs.summary.title"],
|
|
7
|
+
["label", "audit_logs.summary.label"],
|
|
8
|
+
["parentId", "audit_logs.summary.parent"],
|
|
9
|
+
["parent_id", "audit_logs.summary.parent"],
|
|
10
|
+
["status", "audit_logs.summary.status"],
|
|
11
|
+
["isActive", "audit_logs.summary.active"],
|
|
12
|
+
["is_active", "audit_logs.summary.active"],
|
|
13
|
+
["slug", "audit_logs.summary.slug"]
|
|
14
|
+
];
|
|
15
|
+
function previewText(value) {
|
|
16
|
+
if (typeof value !== "string") return void 0;
|
|
17
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
18
|
+
if (!text || text.endsWith(":v1")) return void 0;
|
|
19
|
+
return text.length > 120 ? `${text.slice(0, 119)}\u2026` : text;
|
|
20
|
+
}
|
|
21
|
+
function snapshotRecord(snapshot) {
|
|
22
|
+
if (!isRecord(snapshot)) return void 0;
|
|
23
|
+
return isRecord(snapshot.view) ? snapshot.view : snapshot;
|
|
24
|
+
}
|
|
25
|
+
function auditRecordName(item) {
|
|
26
|
+
for (const snapshot of [item.snapshotAfter, item.snapshotBefore]) {
|
|
27
|
+
const record = snapshotRecord(snapshot);
|
|
28
|
+
if (!record) continue;
|
|
29
|
+
for (const field of ["displayName", "display_name", "name", "title", "label"]) {
|
|
30
|
+
const name = previewText(record[field]);
|
|
31
|
+
if (name) return name;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
function summaryValue(value, field, t) {
|
|
37
|
+
if (value === null) {
|
|
38
|
+
return t(field === "parentId" || field === "parent_id" ? "audit_logs.summary.root" : "audit_logs.common.none");
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "boolean") return t(value ? "audit_logs.summary.yes" : "audit_logs.summary.no");
|
|
41
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
42
|
+
return previewText(value);
|
|
43
|
+
}
|
|
44
|
+
function auditChangeSummary(item, t) {
|
|
45
|
+
if (!isRecord(item.changes)) return t("audit_logs.actions.details.no_changes");
|
|
46
|
+
const count = Object.keys(item.changes).length;
|
|
47
|
+
if (!count) return t("audit_logs.actions.details.no_changes");
|
|
48
|
+
for (const [field, labelKey] of previewFields) {
|
|
49
|
+
if (!Object.hasOwn(item.changes, field)) continue;
|
|
50
|
+
const change = item.changes[field];
|
|
51
|
+
const before = snapshotRecord(item.snapshotBefore)?.[field];
|
|
52
|
+
const from = isRecord(change) ? Object.hasOwn(change, "from") ? change.from : Object.hasOwn(change, "old") ? change.old : before : before;
|
|
53
|
+
const to = isRecord(change) ? Object.hasOwn(change, "to") ? change.to : change.new : change;
|
|
54
|
+
const fromText = summaryValue(from, field, t);
|
|
55
|
+
const toText = summaryValue(to, field, t);
|
|
56
|
+
if (fromText === void 0 || toText === void 0) continue;
|
|
57
|
+
const fieldLabel = labelKey === "audit_logs.summary.parent" && item.resourceKind === "directory.organization" ? t("audit_logs.summary.parent_organization") : t(labelKey);
|
|
58
|
+
const summary = t("audit_logs.summary.change", { field: fieldLabel, from: fromText, to: toText });
|
|
59
|
+
return count > 1 ? t("audit_logs.summary.more", { summary, count: count - 1 }) : summary;
|
|
60
|
+
}
|
|
61
|
+
return t("audit_logs.summary.count", { count });
|
|
62
|
+
}
|
|
63
|
+
function auditExecutionState(state, t) {
|
|
64
|
+
switch (state) {
|
|
65
|
+
case "done":
|
|
66
|
+
return t("audit_logs.version_history.status.done");
|
|
67
|
+
case "undone":
|
|
68
|
+
return t("audit_logs.version_history.status.undone");
|
|
69
|
+
case "failed":
|
|
70
|
+
return t("audit_logs.version_history.status.failed");
|
|
71
|
+
case "redone":
|
|
72
|
+
return t("audit_logs.version_history.status.redone");
|
|
73
|
+
default:
|
|
74
|
+
return state;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
auditChangeSummary,
|
|
79
|
+
auditExecutionState,
|
|
80
|
+
auditRecordName
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=rowPresentation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/audit_logs/lib/rowPresentation.ts"],
|
|
4
|
+
"sourcesContent": ["import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { isRecord } from './changeRows'\n\ntype AuditRow = {\n resourceKind?: string | null\n snapshotBefore?: unknown\n snapshotAfter?: unknown\n changes?: Record<string, unknown> | null\n}\n\nconst previewFields = [\n ['displayName', 'audit_logs.summary.name'],\n ['display_name', 'audit_logs.summary.name'],\n ['name', 'audit_logs.summary.name'],\n ['title', 'audit_logs.summary.title'],\n ['label', 'audit_logs.summary.label'],\n ['parentId', 'audit_logs.summary.parent'],\n ['parent_id', 'audit_logs.summary.parent'],\n ['status', 'audit_logs.summary.status'],\n ['isActive', 'audit_logs.summary.active'],\n ['is_active', 'audit_logs.summary.active'],\n ['slug', 'audit_logs.summary.slug'],\n] as const\n\nfunction previewText(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const text = value.replace(/\\s+/g, ' ').trim()\n if (!text || text.endsWith(':v1')) return undefined\n return text.length > 120 ? `${text.slice(0, 119)}\u2026` : text\n}\n\nfunction snapshotRecord(snapshot: unknown): Record<string, unknown> | undefined {\n if (!isRecord(snapshot)) return undefined\n return isRecord(snapshot.view) ? snapshot.view : snapshot\n}\n\nexport function auditRecordName(item: AuditRow): string | undefined {\n for (const snapshot of [item.snapshotAfter, item.snapshotBefore]) {\n const record = snapshotRecord(snapshot)\n if (!record) continue\n for (const field of ['displayName', 'display_name', 'name', 'title', 'label']) {\n const name = previewText(record[field])\n if (name) return name\n }\n }\n return undefined\n}\n\nfunction summaryValue(value: unknown, field: string, t: TranslateFn): string | undefined {\n if (value === null) {\n return t(field === 'parentId' || field === 'parent_id' ? 'audit_logs.summary.root' : 'audit_logs.common.none')\n }\n if (typeof value === 'boolean') return t(value ? 'audit_logs.summary.yes' : 'audit_logs.summary.no')\n if (typeof value === 'number' && Number.isFinite(value)) return String(value)\n return previewText(value)\n}\n\nexport function auditChangeSummary(item: AuditRow, t: TranslateFn): string {\n if (!isRecord(item.changes)) return t('audit_logs.actions.details.no_changes')\n const count = Object.keys(item.changes).length\n if (!count) return t('audit_logs.actions.details.no_changes')\n for (const [field, labelKey] of previewFields) {\n if (!Object.hasOwn(item.changes, field)) continue\n const change = item.changes[field]\n const before = snapshotRecord(item.snapshotBefore)?.[field]\n const from = isRecord(change)\n ? Object.hasOwn(change, 'from') ? change.from : Object.hasOwn(change, 'old') ? change.old : before\n : before\n const to = isRecord(change)\n ? Object.hasOwn(change, 'to') ? change.to : change.new\n : change\n const fromText = summaryValue(from, field, t)\n const toText = summaryValue(to, field, t)\n if (fromText === undefined || toText === undefined) continue\n const fieldLabel = labelKey === 'audit_logs.summary.parent' && item.resourceKind === 'directory.organization'\n ? t('audit_logs.summary.parent_organization')\n : t(labelKey)\n const summary = t('audit_logs.summary.change', { field: fieldLabel, from: fromText, to: toText })\n return count > 1 ? t('audit_logs.summary.more', { summary, count: count - 1 }) : summary\n }\n return t('audit_logs.summary.count', { count })\n}\n\nexport function auditExecutionState(state: string, t: TranslateFn): string {\n switch (state) {\n case 'done': return t('audit_logs.version_history.status.done')\n case 'undone': return t('audit_logs.version_history.status.undone')\n case 'failed': return t('audit_logs.version_history.status.failed')\n case 'redone': return t('audit_logs.version_history.status.redone')\n default: return state\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
buildChanges,
|
|
4
|
+
requireId,
|
|
5
|
+
parseWithCustomFields,
|
|
6
|
+
setCustomFieldsIfAny,
|
|
7
|
+
emitCrudSideEffects,
|
|
8
|
+
emitCrudUndoSideEffects
|
|
9
|
+
} from "@open-mercato/shared/lib/commands/helpers";
|
|
10
|
+
import { LockMode, UniqueConstraintViolationException } from "@mikro-orm/core";
|
|
4
11
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
12
|
+
import { invalidateCrudCache } from "@open-mercato/shared/lib/crud/cache";
|
|
5
13
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
6
14
|
import { loadCustomFieldSnapshot, buildCustomFieldResetMap } from "@open-mercato/shared/lib/commands/customFieldSnapshots";
|
|
7
15
|
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
@@ -48,6 +56,19 @@ const variantCrudEvents = {
|
|
|
48
56
|
tenantId: ctx.identifiers.tenantId
|
|
49
57
|
})
|
|
50
58
|
};
|
|
59
|
+
const variantPriceCrudEvents = {
|
|
60
|
+
module: "catalog",
|
|
61
|
+
entity: "price",
|
|
62
|
+
persistent: true,
|
|
63
|
+
buildPayload: (ctx) => ({
|
|
64
|
+
id: ctx.identifiers.id,
|
|
65
|
+
organizationId: ctx.identifiers.organizationId,
|
|
66
|
+
tenantId: ctx.identifiers.tenantId
|
|
67
|
+
})
|
|
68
|
+
};
|
|
69
|
+
const variantPriceCrudIndexer = {
|
|
70
|
+
entityType: E.catalog.catalog_product_price
|
|
71
|
+
};
|
|
51
72
|
const VARIANT_CHANGE_KEYS = [
|
|
52
73
|
"name",
|
|
53
74
|
"sku",
|
|
@@ -241,6 +262,20 @@ async function loadVariantPriceSnapshots(em, variantId, scope) {
|
|
|
241
262
|
}
|
|
242
263
|
return snapshots;
|
|
243
264
|
}
|
|
265
|
+
async function resetVariantPriceCustomFields(dataEngine, snapshots) {
|
|
266
|
+
for (const snapshot of snapshots) {
|
|
267
|
+
const resetValues = buildCustomFieldResetMap(snapshot.custom ?? void 0, void 0);
|
|
268
|
+
if (!Object.keys(resetValues).length) continue;
|
|
269
|
+
await setCustomFieldsIfAny({
|
|
270
|
+
dataEngine,
|
|
271
|
+
entityId: E.catalog.catalog_product_price,
|
|
272
|
+
recordId: snapshot.id,
|
|
273
|
+
organizationId: snapshot.organizationId,
|
|
274
|
+
tenantId: snapshot.tenantId,
|
|
275
|
+
values: resetValues
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
244
279
|
async function restoreVariantPricesFromSnapshots(em, variant, snapshots, dataEngine) {
|
|
245
280
|
if (!snapshots.length) return;
|
|
246
281
|
const productRef = typeof variant.product === "string" ? await requireProduct(em, variant.product, {
|
|
@@ -566,8 +601,57 @@ const createVariantCommand = {
|
|
|
566
601
|
if (!record) return;
|
|
567
602
|
ensureTenantScope(ctx, record.tenantId);
|
|
568
603
|
ensureOrganizationScope(ctx, record.organizationId);
|
|
569
|
-
|
|
570
|
-
|
|
604
|
+
const variantScope = { tenantId: record.tenantId, organizationId: record.organizationId };
|
|
605
|
+
let priceSnapshots = [];
|
|
606
|
+
let variantLocked = false;
|
|
607
|
+
await withAtomicFlush(
|
|
608
|
+
em,
|
|
609
|
+
[
|
|
610
|
+
async () => {
|
|
611
|
+
const locked = await findOneWithDecryption(
|
|
612
|
+
em,
|
|
613
|
+
CatalogProductVariant,
|
|
614
|
+
{ id: record.id },
|
|
615
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
616
|
+
variantScope
|
|
617
|
+
);
|
|
618
|
+
if (!locked) return;
|
|
619
|
+
variantLocked = true;
|
|
620
|
+
priceSnapshots = await loadVariantPriceSnapshots(em.fork({ keepTransactionContext: true }), record.id, variantScope);
|
|
621
|
+
if (priceSnapshots.length) {
|
|
622
|
+
await em.nativeDelete(CatalogProductPrice, { id: { $in: priceSnapshots.map((price) => price.id) } });
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
() => {
|
|
626
|
+
if (variantLocked) em.remove(record);
|
|
627
|
+
}
|
|
628
|
+
],
|
|
629
|
+
{ transaction: true }
|
|
630
|
+
);
|
|
631
|
+
if (!variantLocked) return;
|
|
632
|
+
await capturePriceHistoryEntries(em, priceSnapshots, "delete", {
|
|
633
|
+
metadata: { undoneCommand: "catalog.variants.create" },
|
|
634
|
+
cache: resolveOmnibusCache(ctx.container)
|
|
635
|
+
});
|
|
636
|
+
const dataEngine = ctx.container.resolve("dataEngine");
|
|
637
|
+
await resetVariantPriceCustomFields(dataEngine, priceSnapshots);
|
|
638
|
+
for (const price of priceSnapshots) {
|
|
639
|
+
await emitCrudUndoSideEffects({
|
|
640
|
+
dataEngine,
|
|
641
|
+
action: "deleted",
|
|
642
|
+
entity: price,
|
|
643
|
+
identifiers: { id: price.id, organizationId: price.organizationId, tenantId: price.tenantId },
|
|
644
|
+
events: variantPriceCrudEvents,
|
|
645
|
+
indexer: variantPriceCrudIndexer
|
|
646
|
+
});
|
|
647
|
+
await invalidateCrudCache(
|
|
648
|
+
ctx.container,
|
|
649
|
+
"catalog.price",
|
|
650
|
+
{ id: price.id, organizationId: price.organizationId, tenantId: price.tenantId },
|
|
651
|
+
price.tenantId,
|
|
652
|
+
"command:catalog.variants.create:undo"
|
|
653
|
+
);
|
|
654
|
+
}
|
|
571
655
|
if (payload?.previousDefaultVariantId) {
|
|
572
656
|
const previousDefault = await em.findOne(CatalogProductVariant, { id: payload.previousDefaultVariantId });
|
|
573
657
|
if (previousDefault) {
|
|
@@ -915,18 +999,7 @@ const deleteVariantCommand = {
|
|
|
915
999
|
em.remove(record);
|
|
916
1000
|
await em.flush();
|
|
917
1001
|
await capturePriceHistoryEntries(em, priceSnapshots, "delete", { cache: resolveOmnibusCache(ctx.container) });
|
|
918
|
-
|
|
919
|
-
const resetValues = buildCustomFieldResetMap(priceSnapshot.custom ?? void 0, void 0);
|
|
920
|
-
if (!Object.keys(resetValues).length) continue;
|
|
921
|
-
await setCustomFieldsIfAny({
|
|
922
|
-
dataEngine,
|
|
923
|
-
entityId: E.catalog.catalog_product_price,
|
|
924
|
-
recordId: priceSnapshot.id,
|
|
925
|
-
organizationId: priceSnapshot.organizationId,
|
|
926
|
-
tenantId: priceSnapshot.tenantId,
|
|
927
|
-
values: resetValues
|
|
928
|
-
});
|
|
929
|
-
}
|
|
1002
|
+
await resetVariantPriceCustomFields(dataEngine, priceSnapshots);
|
|
930
1003
|
if (snapshot?.custom && Object.keys(snapshot.custom).length) {
|
|
931
1004
|
const resetValues = buildCustomFieldResetMap(snapshot.custom, void 0);
|
|
932
1005
|
if (Object.keys(resetValues).length) {
|