@open-mercato/ui 0.5.1-develop.2681.c559bb2bc3 → 0.5.1-develop.2683.4878a05b8e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/version-history/VersionHistoryPanel.js +2 -2
- package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
- package/dist/primitives/ErrorNotice.js +9 -5
- package/dist/primitives/ErrorNotice.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/version-history/VersionHistoryPanel.tsx +6 -4
- package/src/primitives/ErrorNotice.tsx +8 -4
- package/src/primitives/__tests__/ErrorNotice.test.tsx +54 -0
- package/src/primitives/__tests__/no-deprecated-notice.test.ts +71 -0
|
@@ -9,7 +9,7 @@ import { apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
|
9
9
|
import { markRedoConsumed, markUndoSuccess } from "@open-mercato/ui/backend/operations/store";
|
|
10
10
|
import { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from "./labels.js";
|
|
11
11
|
import { useAuditPermissions, canUndoEntry, canRedoEntry } from "./useAuditPermissions.js";
|
|
12
|
-
import {
|
|
12
|
+
import { Alert, AlertDescription } from "@open-mercato/ui/primitives/alert";
|
|
13
13
|
import { humanizeResourceKind } from "./labels.js";
|
|
14
14
|
function VersionHistoryPanel({
|
|
15
15
|
open,
|
|
@@ -154,7 +154,7 @@ function VersionHistoryPanel({
|
|
|
154
154
|
)
|
|
155
155
|
] }),
|
|
156
156
|
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: selectedEntry ? /* @__PURE__ */ jsx(VersionHistoryDetail, { entry: selectedEntry, t }) : /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
157
|
-
shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? /* @__PURE__ */ jsx(
|
|
157
|
+
shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? /* @__PURE__ */ jsx(Alert, { variant: "info", children: /* @__PURE__ */ jsx(AlertDescription, { children: t("audit_logs.hint.view_self_only", "Showing only your own changes. Contact an administrator for broader access.") }) }) : null,
|
|
158
158
|
isInitialLoading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-muted-foreground", children: [
|
|
159
159
|
/* @__PURE__ */ jsx(Loader2, { className: "mb-2 h-8 w-8 animate-spin" }),
|
|
160
160
|
/* @__PURE__ */ jsx("p", { children: t("audit_logs.version_history.loading") })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/version-history/VersionHistoryPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronLeft, Clock, Loader2, RotateCcw, Undo2, X } from 'lucide-react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport type { VersionHistoryEntry } from './types'\nimport { VersionHistoryDetail } from './VersionHistoryDetail'\nimport { formatDate } from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from './labels'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from './useAuditPermissions'\nimport { Notice } from '@open-mercato/ui/primitives/Notice'\nimport { humanizeResourceKind } from './labels'\n\nexport type VersionHistoryPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n entries: VersionHistoryEntry[]\n isLoading: boolean\n error: string | null\n hasMore: boolean\n onLoadMore: () => void\n t: TranslateFn\n /** Explicit override \u2014 when provided, skips auto-check and uses this value directly. */\n canUndoRedo?: boolean\n /** When true (default), auto-checks audit_logs features for the current user. Ignored when canUndoRedo is provided. */\n autoCheckAcl?: boolean\n}\n\nexport function VersionHistoryPanel({\n open,\n onOpenChange,\n entries,\n isLoading,\n error,\n hasMore,\n onLoadMore,\n t,\n canUndoRedo,\n autoCheckAcl = true,\n}: VersionHistoryPanelProps) {\n const shouldAutoCheck = canUndoRedo === undefined && autoCheckAcl\n const permissions = useAuditPermissions(shouldAutoCheck && open)\n\n const visibleEntries = React.useMemo(() => {\n if (canUndoRedo !== undefined || !shouldAutoCheck) return entries\n if (permissions.isLoading) return entries\n if (permissions.canViewTenant) return entries\n if (!permissions.currentUserId) return entries\n return entries.filter((entry) => entry.actorUserId === permissions.currentUserId)\n }, [entries, canUndoRedo, shouldAutoCheck, permissions])\n\n const [selectedEntry, setSelectedEntry] = React.useState<VersionHistoryEntry | null>(null)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const latestUndoableId = React.useMemo(() => {\n const latest = visibleEntries.find((entry) => entry.undoToken && entry.executionState === 'done')\n return latest?.id ?? null\n }, [visibleEntries])\n const latestUndoneId = React.useMemo(() => {\n const undone = visibleEntries.filter((entry) => entry.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 }, [visibleEntries])\n\n React.useEffect(() => {\n if (!open) setSelectedEntry(null)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [open])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n onOpenChange(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, onOpenChange])\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 if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n console.error(t('audit_logs.actions.undo'), err)\n } finally {\n setUndoingToken(null)\n }\n }, [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 if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n console.error(t('audit_logs.actions.redo'), err)\n } finally {\n setRedoingId(null)\n }\n }, [t])\n\n if (!open) return null\n\n const isEmpty = visibleEntries.length === 0 && !isLoading && !error\n const isInitialLoading = visibleEntries.length === 0 && isLoading\n const isInitialError = visibleEntries.length === 0 && !!error\n\n return (\n <>\n <div\n className=\"fixed inset-0 z-40 bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-50 h-full w-full max-w-md border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('audit_logs.version_history.title')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {selectedEntry ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setSelectedEntry(null)}\n aria-label={t('audit_logs.version_history.detail.back')}\n >\n <ChevronLeft className=\"h-5 w-5\" />\n </Button>\n ) : (\n <Clock className=\"h-5 w-5\" />\n )}\n <h2 className=\"font-semibold\">\n {selectedEntry\n ? t('audit_logs.version_history.detail.title')\n : t('audit_logs.version_history.title')}\n </h2>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => onOpenChange(false)}\n aria-label={t('audit_logs.version_history.close')}\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">\n {selectedEntry ? (\n <VersionHistoryDetail entry={selectedEntry} t={t} />\n ) : (\n <div className=\"space-y-3\">\n {shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? (\n <Notice compact>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </Notice>\n ) : null}\n\n {isInitialLoading ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Loader2 className=\"mb-2 h-8 w-8 animate-spin\" />\n <p>{t('audit_logs.version_history.loading')}</p>\n </div>\n ) : null}\n\n {isInitialError ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground\">\n <p>{t('audit_logs.version_history.error')}</p>\n <Button variant=\"ghost\" size=\"sm\" onClick={onLoadMore}>\n {t('audit_logs.common.refresh')}\n </Button>\n </div>\n ) : null}\n\n {isEmpty ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Clock className=\"mb-2 h-8 w-8 opacity-50\" />\n <p>{t('audit_logs.version_history.empty')}</p>\n </div>\n ) : null}\n\n {visibleEntries.length > 0 ? (\n <div className=\"divide-y rounded-lg border\">\n {visibleEntries.map((entry) => {\n const statusLabel = getVersionHistoryStatusLabel(entry.executionState, t)\n const actionLabel = getVersionHistoryActionLabel(entry, t)\n const isRelatedEntry = entry.parentResourceKind != null\n const entryCanUndo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canUndoEntry(permissions, entry.actorUserId) : true)\n const entryCanRedo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canRedoEntry(permissions, entry.actorUserId) : true)\n const canUndo = entryCanUndo\n && Boolean(entry.undoToken)\n && entry.executionState === 'done'\n && entry.id === latestUndoableId\n const showRedo = entryCanRedo && entry.executionState === 'undone'\n const canRedo = showRedo && entry.id === latestUndoneId\n return (\n <div\n key={entry.id}\n className={`flex items-start justify-between gap-3 py-3 transition-colors hover:bg-muted/40 ${isRelatedEntry ? 'pl-8 pr-4 border-l-2 border-l-muted-foreground/20' : 'px-4'}`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"h-auto min-w-0 flex-1 flex-col items-start justify-start gap-1 whitespace-normal px-0 py-0 text-left hover:bg-transparent\"\n onClick={() => setSelectedEntry(entry)}\n >\n {isRelatedEntry ? (\n <span className=\"text-[10px] uppercase tracking-wider text-muted-foreground/70 font-medium\">\n {humanizeResourceKind(entry.resourceKind, t)}\n </span>\n ) : null}\n <div className=\"break-words text-sm font-medium\">\n {actionLabel}\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 text-xs text-muted-foreground\">\n <span>{entry.actorUserName || entry.actorUserId || t('audit_logs.common.none')}</span>\n <span>\u2022</span>\n <span>{formatDate(entry.createdAt)}</span>\n <span>\u2022</span>\n <span>{statusLabel}</span>\n </div>\n </Button>\n <div className=\"flex items-center gap-1 pt-1\">\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleUndo(entry.undoToken)\n }}\n disabled={undoingToken === entry.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.undo')}</span>\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleRedo(entry.id)\n }}\n disabled={!canRedo || redoingId === entry.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.redo')}</span>\n </Button>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : null}\n\n {error && visibleEntries.length > 0 ? (\n <div className=\"text-xs text-red-500\">{error}</div>\n ) : null}\n\n {hasMore ? (\n <div className=\"pt-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onLoadMore}\n disabled={isLoading}\n >\n {isLoading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : null}\n {t('audit_logs.version_history.load_more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AA8II,mBACE,KAaM,YAdR;AA5IJ,YAAY,WAAW;AACvB,SAAS,aAAa,OAAO,SAAS,WAAW,OAAO,SAAS;AAEjE,SAAS,cAAc;AAEvB,SAAS,4BAA4B;AACrC,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB,uBAAuB;AAClD,SAAS,8BAA8B,oCAAoC;AAC3E,SAAS,qBAAqB,cAAc,oBAAoB;AAChE,SAAS,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronLeft, Clock, Loader2, RotateCcw, Undo2, X } from 'lucide-react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport type { VersionHistoryEntry } from './types'\nimport { VersionHistoryDetail } from './VersionHistoryDetail'\nimport { formatDate } from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from './labels'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from './useAuditPermissions'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { humanizeResourceKind } from './labels'\n\nexport type VersionHistoryPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n entries: VersionHistoryEntry[]\n isLoading: boolean\n error: string | null\n hasMore: boolean\n onLoadMore: () => void\n t: TranslateFn\n /** Explicit override \u2014 when provided, skips auto-check and uses this value directly. */\n canUndoRedo?: boolean\n /** When true (default), auto-checks audit_logs features for the current user. Ignored when canUndoRedo is provided. */\n autoCheckAcl?: boolean\n}\n\nexport function VersionHistoryPanel({\n open,\n onOpenChange,\n entries,\n isLoading,\n error,\n hasMore,\n onLoadMore,\n t,\n canUndoRedo,\n autoCheckAcl = true,\n}: VersionHistoryPanelProps) {\n const shouldAutoCheck = canUndoRedo === undefined && autoCheckAcl\n const permissions = useAuditPermissions(shouldAutoCheck && open)\n\n const visibleEntries = React.useMemo(() => {\n if (canUndoRedo !== undefined || !shouldAutoCheck) return entries\n if (permissions.isLoading) return entries\n if (permissions.canViewTenant) return entries\n if (!permissions.currentUserId) return entries\n return entries.filter((entry) => entry.actorUserId === permissions.currentUserId)\n }, [entries, canUndoRedo, shouldAutoCheck, permissions])\n\n const [selectedEntry, setSelectedEntry] = React.useState<VersionHistoryEntry | null>(null)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const latestUndoableId = React.useMemo(() => {\n const latest = visibleEntries.find((entry) => entry.undoToken && entry.executionState === 'done')\n return latest?.id ?? null\n }, [visibleEntries])\n const latestUndoneId = React.useMemo(() => {\n const undone = visibleEntries.filter((entry) => entry.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 }, [visibleEntries])\n\n React.useEffect(() => {\n if (!open) setSelectedEntry(null)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [open])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n onOpenChange(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, onOpenChange])\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 if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n console.error(t('audit_logs.actions.undo'), err)\n } finally {\n setUndoingToken(null)\n }\n }, [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 if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n console.error(t('audit_logs.actions.redo'), err)\n } finally {\n setRedoingId(null)\n }\n }, [t])\n\n if (!open) return null\n\n const isEmpty = visibleEntries.length === 0 && !isLoading && !error\n const isInitialLoading = visibleEntries.length === 0 && isLoading\n const isInitialError = visibleEntries.length === 0 && !!error\n\n return (\n <>\n <div\n className=\"fixed inset-0 z-40 bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-50 h-full w-full max-w-md border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('audit_logs.version_history.title')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {selectedEntry ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setSelectedEntry(null)}\n aria-label={t('audit_logs.version_history.detail.back')}\n >\n <ChevronLeft className=\"h-5 w-5\" />\n </Button>\n ) : (\n <Clock className=\"h-5 w-5\" />\n )}\n <h2 className=\"font-semibold\">\n {selectedEntry\n ? t('audit_logs.version_history.detail.title')\n : t('audit_logs.version_history.title')}\n </h2>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => onOpenChange(false)}\n aria-label={t('audit_logs.version_history.close')}\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">\n {selectedEntry ? (\n <VersionHistoryDetail entry={selectedEntry} t={t} />\n ) : (\n <div className=\"space-y-3\">\n {shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? (\n <Alert variant=\"info\">\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\n {isInitialLoading ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Loader2 className=\"mb-2 h-8 w-8 animate-spin\" />\n <p>{t('audit_logs.version_history.loading')}</p>\n </div>\n ) : null}\n\n {isInitialError ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground\">\n <p>{t('audit_logs.version_history.error')}</p>\n <Button variant=\"ghost\" size=\"sm\" onClick={onLoadMore}>\n {t('audit_logs.common.refresh')}\n </Button>\n </div>\n ) : null}\n\n {isEmpty ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Clock className=\"mb-2 h-8 w-8 opacity-50\" />\n <p>{t('audit_logs.version_history.empty')}</p>\n </div>\n ) : null}\n\n {visibleEntries.length > 0 ? (\n <div className=\"divide-y rounded-lg border\">\n {visibleEntries.map((entry) => {\n const statusLabel = getVersionHistoryStatusLabel(entry.executionState, t)\n const actionLabel = getVersionHistoryActionLabel(entry, t)\n const isRelatedEntry = entry.parentResourceKind != null\n const entryCanUndo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canUndoEntry(permissions, entry.actorUserId) : true)\n const entryCanRedo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canRedoEntry(permissions, entry.actorUserId) : true)\n const canUndo = entryCanUndo\n && Boolean(entry.undoToken)\n && entry.executionState === 'done'\n && entry.id === latestUndoableId\n const showRedo = entryCanRedo && entry.executionState === 'undone'\n const canRedo = showRedo && entry.id === latestUndoneId\n return (\n <div\n key={entry.id}\n className={`flex items-start justify-between gap-3 py-3 transition-colors hover:bg-muted/40 ${isRelatedEntry ? 'pl-8 pr-4 border-l-2 border-l-muted-foreground/20' : 'px-4'}`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"h-auto min-w-0 flex-1 flex-col items-start justify-start gap-1 whitespace-normal px-0 py-0 text-left hover:bg-transparent\"\n onClick={() => setSelectedEntry(entry)}\n >\n {isRelatedEntry ? (\n <span className=\"text-[10px] uppercase tracking-wider text-muted-foreground/70 font-medium\">\n {humanizeResourceKind(entry.resourceKind, t)}\n </span>\n ) : null}\n <div className=\"break-words text-sm font-medium\">\n {actionLabel}\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 text-xs text-muted-foreground\">\n <span>{entry.actorUserName || entry.actorUserId || t('audit_logs.common.none')}</span>\n <span>\u2022</span>\n <span>{formatDate(entry.createdAt)}</span>\n <span>\u2022</span>\n <span>{statusLabel}</span>\n </div>\n </Button>\n <div className=\"flex items-center gap-1 pt-1\">\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleUndo(entry.undoToken)\n }}\n disabled={undoingToken === entry.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.undo')}</span>\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleRedo(entry.id)\n }}\n disabled={!canRedo || redoingId === entry.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.redo')}</span>\n </Button>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : null}\n\n {error && visibleEntries.length > 0 ? (\n <div className=\"text-xs text-red-500\">{error}</div>\n ) : null}\n\n {hasMore ? (\n <div className=\"pt-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onLoadMore}\n disabled={isLoading}\n >\n {isLoading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : null}\n {t('audit_logs.version_history.load_more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
|
-
import {
|
|
4
|
+
import { Alert, AlertDescription, AlertTitle } from "./alert.js";
|
|
5
5
|
function ErrorNotice({ title, message, action }) {
|
|
6
6
|
const t = useT();
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
return /* @__PURE__ */
|
|
7
|
+
const resolvedTitle = title ?? t("ui.errors.defaultTitle", "Something went wrong");
|
|
8
|
+
const resolvedMessage = message ?? t("ui.errors.defaultMessage", "Unable to load data. Please try again.");
|
|
9
|
+
return /* @__PURE__ */ jsxs(Alert, { variant: "destructive", children: [
|
|
10
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: resolvedTitle }),
|
|
11
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: resolvedMessage }),
|
|
12
|
+
action ? /* @__PURE__ */ jsx("div", { className: "mt-2", children: action }) : null
|
|
13
|
+
] });
|
|
10
14
|
}
|
|
11
15
|
export {
|
|
12
16
|
ErrorNotice
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/ErrorNotice.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {
|
|
5
|
-
"mappings": ";AAcI;AAZJ,SAAS,YAAY;AACrB,SAAS,
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from './alert'\n\nexport function ErrorNotice({ title, message, action }: {\n title?: string\n message?: string\n action?: React.ReactNode\n}) {\n const t = useT()\n const resolvedTitle = title ?? t('ui.errors.defaultTitle', 'Something went wrong')\n const resolvedMessage = message ?? t('ui.errors.defaultMessage', 'Unable to load data. Please try again.')\n return (\n <Alert variant=\"destructive\">\n <AlertTitle>{resolvedTitle}</AlertTitle>\n <AlertDescription>{resolvedMessage}</AlertDescription>\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </Alert>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAcI,SACE,KADF;AAZJ,SAAS,YAAY;AACrB,SAAS,OAAO,kBAAkB,kBAAkB;AAE7C,SAAS,YAAY,EAAE,OAAO,SAAS,OAAO,GAIlD;AACD,QAAM,IAAI,KAAK;AACf,QAAM,gBAAgB,SAAS,EAAE,0BAA0B,sBAAsB;AACjF,QAAM,kBAAkB,WAAW,EAAE,4BAA4B,wCAAwC;AACzG,SACE,qBAAC,SAAM,SAAQ,eACb;AAAA,wBAAC,cAAY,yBAAc;AAAA,IAC3B,oBAAC,oBAAkB,2BAAgB;AAAA,IAClC,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,KACnD;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
3
|
+
"version": "0.5.1-develop.2683.4878a05b8e",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -132,12 +132,12 @@
|
|
|
132
132
|
"recharts": "^3.8.1"
|
|
133
133
|
},
|
|
134
134
|
"peerDependencies": {
|
|
135
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
135
|
+
"@open-mercato/shared": "0.5.1-develop.2683.4878a05b8e",
|
|
136
136
|
"react": ">=18.0.0",
|
|
137
137
|
"react-dom": ">=18.0.0"
|
|
138
138
|
},
|
|
139
139
|
"devDependencies": {
|
|
140
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
140
|
+
"@open-mercato/shared": "0.5.1-develop.2683.4878a05b8e",
|
|
141
141
|
"@testing-library/dom": "^10.4.1",
|
|
142
142
|
"@testing-library/jest-dom": "^6.9.1",
|
|
143
143
|
"@testing-library/react": "^16.3.1",
|
|
@@ -11,7 +11,7 @@ import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
|
11
11
|
import { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'
|
|
12
12
|
import { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from './labels'
|
|
13
13
|
import { useAuditPermissions, canUndoEntry, canRedoEntry } from './useAuditPermissions'
|
|
14
|
-
import {
|
|
14
|
+
import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
15
15
|
import { humanizeResourceKind } from './labels'
|
|
16
16
|
|
|
17
17
|
export type VersionHistoryPanelProps = {
|
|
@@ -189,9 +189,11 @@ export function VersionHistoryPanel({
|
|
|
189
189
|
) : (
|
|
190
190
|
<div className="space-y-3">
|
|
191
191
|
{shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? (
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
<Alert variant="info">
|
|
193
|
+
<AlertDescription>
|
|
194
|
+
{t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}
|
|
195
|
+
</AlertDescription>
|
|
196
|
+
</Alert>
|
|
195
197
|
) : null}
|
|
196
198
|
|
|
197
199
|
{isInitialLoading ? (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
4
|
-
import {
|
|
4
|
+
import { Alert, AlertDescription, AlertTitle } from './alert'
|
|
5
5
|
|
|
6
6
|
export function ErrorNotice({ title, message, action }: {
|
|
7
7
|
title?: string
|
|
@@ -9,9 +9,13 @@ export function ErrorNotice({ title, message, action }: {
|
|
|
9
9
|
action?: React.ReactNode
|
|
10
10
|
}) {
|
|
11
11
|
const t = useT()
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const resolvedTitle = title ?? t('ui.errors.defaultTitle', 'Something went wrong')
|
|
13
|
+
const resolvedMessage = message ?? t('ui.errors.defaultMessage', 'Unable to load data. Please try again.')
|
|
14
14
|
return (
|
|
15
|
-
<
|
|
15
|
+
<Alert variant="destructive">
|
|
16
|
+
<AlertTitle>{resolvedTitle}</AlertTitle>
|
|
17
|
+
<AlertDescription>{resolvedMessage}</AlertDescription>
|
|
18
|
+
{action ? <div className="mt-2">{action}</div> : null}
|
|
19
|
+
</Alert>
|
|
16
20
|
)
|
|
17
21
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
4
|
+
import { ErrorNotice } from '../ErrorNotice'
|
|
5
|
+
|
|
6
|
+
function renderWithI18n(ui: React.ReactElement) {
|
|
7
|
+
return render(
|
|
8
|
+
<I18nProvider locale="en" dict={{}}>{ui}</I18nProvider>,
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('ErrorNotice', () => {
|
|
13
|
+
it('renders as an Alert with the destructive variant and default copy', () => {
|
|
14
|
+
renderWithI18n(<ErrorNotice />)
|
|
15
|
+
const alert = screen.getByRole('alert')
|
|
16
|
+
expect(alert).toBeInTheDocument()
|
|
17
|
+
expect(alert.className).toMatch(/status-error/)
|
|
18
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
|
|
19
|
+
expect(screen.getByText('Unable to load data. Please try again.')).toBeInTheDocument()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('surfaces the provided title and message', () => {
|
|
23
|
+
renderWithI18n(<ErrorNotice title="Custom title" message="Custom message" />)
|
|
24
|
+
expect(screen.getByText('Custom title')).toBeInTheDocument()
|
|
25
|
+
expect(screen.getByText('Custom message')).toBeInTheDocument()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('renders the action node when provided', () => {
|
|
29
|
+
renderWithI18n(
|
|
30
|
+
<ErrorNotice
|
|
31
|
+
title="Oops"
|
|
32
|
+
message="Try again"
|
|
33
|
+
action={<button type="button">Retry</button>}
|
|
34
|
+
/>,
|
|
35
|
+
)
|
|
36
|
+
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('does not emit the deprecated Notice console.warn in dev', () => {
|
|
40
|
+
const originalEnv = process.env.NODE_ENV
|
|
41
|
+
process.env.NODE_ENV = 'development'
|
|
42
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
43
|
+
try {
|
|
44
|
+
renderWithI18n(<ErrorNotice />)
|
|
45
|
+
const match = warn.mock.calls.some((call) =>
|
|
46
|
+
typeof call[0] === 'string' && call[0].includes('<Notice> is deprecated'),
|
|
47
|
+
)
|
|
48
|
+
expect(match).toBe(false)
|
|
49
|
+
} finally {
|
|
50
|
+
warn.mockRestore()
|
|
51
|
+
process.env.NODE_ENV = originalEnv
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..', '..')
|
|
5
|
+
|
|
6
|
+
const ALLOWED_FILES = new Set(
|
|
7
|
+
[
|
|
8
|
+
'packages/ui/src/primitives/Notice.tsx',
|
|
9
|
+
'packages/ui/src/primitives/ErrorNotice.tsx',
|
|
10
|
+
'packages/ui/src/primitives/__tests__/no-deprecated-notice.test.ts',
|
|
11
|
+
].map((relative) => path.join(REPO_ROOT, relative)),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const IGNORED_DIRS = new Set([
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.next',
|
|
17
|
+
'dist',
|
|
18
|
+
'build',
|
|
19
|
+
'.turbo',
|
|
20
|
+
'.yarn',
|
|
21
|
+
'.git',
|
|
22
|
+
'.ai',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
function collectSourceFiles(start: string, out: string[]) {
|
|
26
|
+
const entries = fs.readdirSync(start, { withFileTypes: true })
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (IGNORED_DIRS.has(entry.name)) continue
|
|
29
|
+
const full = path.join(start, entry.name)
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
collectSourceFiles(full, out)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
if (!entry.isFile()) continue
|
|
35
|
+
if (!/\.(tsx?|jsx?)$/.test(entry.name)) continue
|
|
36
|
+
out.push(full)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('Deprecated <Notice> JSX guard', () => {
|
|
41
|
+
it('has no direct <Notice ...> JSX usages outside the allow-list', () => {
|
|
42
|
+
const files: string[] = []
|
|
43
|
+
for (const dir of ['apps', 'packages']) {
|
|
44
|
+
const fullDir = path.join(REPO_ROOT, dir)
|
|
45
|
+
if (fs.existsSync(fullDir)) collectSourceFiles(fullDir, files)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const noticeUsageRegex = /<Notice\b/
|
|
49
|
+
|
|
50
|
+
const violations: Array<{ file: string; line: number; snippet: string }> = []
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
if (ALLOWED_FILES.has(file)) continue
|
|
53
|
+
if (/__tests__/.test(file)) continue
|
|
54
|
+
const contents = fs.readFileSync(file, 'utf8')
|
|
55
|
+
if (!noticeUsageRegex.test(contents)) continue
|
|
56
|
+
|
|
57
|
+
const lines = contents.split('\n')
|
|
58
|
+
lines.forEach((lineContent, index) => {
|
|
59
|
+
if (noticeUsageRegex.test(lineContent)) {
|
|
60
|
+
violations.push({
|
|
61
|
+
file: path.relative(REPO_ROOT, file),
|
|
62
|
+
line: index + 1,
|
|
63
|
+
snippet: lineContent.trim().slice(0, 160),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(violations).toEqual([])
|
|
70
|
+
})
|
|
71
|
+
})
|