@open-mercato/ui 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7-develop.6869.1.d1ce9999ea
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/ai/AiChat.js +5 -5
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/LoopDisabledBanner.js +1 -1
- package/dist/ai/LoopDisabledBanner.js.map +2 -2
- package/dist/ai/parts/ConfirmationCard.js +4 -4
- package/dist/ai/parts/ConfirmationCard.js.map +2 -2
- package/dist/ai/parts/FieldDiffCard.js +2 -2
- package/dist/ai/parts/FieldDiffCard.js.map +2 -2
- package/dist/ai/parts/MutationResultCard.js +3 -3
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js +1 -1
- package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
- package/dist/backend/PerspectiveSidebar.js +1 -1
- package/dist/backend/PerspectiveSidebar.js.map +2 -2
- package/dist/backend/custom-fields/FieldDefinitionsManager.js +1 -1
- package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +2 -2
- package/dist/backend/dashboard/DashboardScreen.js +3 -3
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +1 -1
- package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js.map +1 -1
- package/dist/backend/messages/EmailThreadsPanel.js +1 -1
- package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
- package/dist/backend/version-history/VersionHistoryPanel.js +1 -1
- package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
- package/dist/primitives/ErrorNotice.js +1 -1
- package/dist/primitives/ErrorNotice.js.map +2 -2
- package/dist/primitives/Notice.js +1 -1
- package/dist/primitives/Notice.js.map +1 -1
- package/package.json +3 -3
- package/src/ai/AiChat.tsx +6 -6
- package/src/ai/LoopDisabledBanner.tsx +1 -1
- package/src/ai/parts/ConfirmationCard.tsx +4 -4
- package/src/ai/parts/FieldDiffCard.tsx +2 -2
- package/src/ai/parts/MutationResultCard.tsx +3 -3
- package/src/ai/ui-parts/pending-phase3-placeholder.tsx +2 -2
- package/src/backend/PerspectiveSidebar.tsx +1 -1
- package/src/backend/custom-fields/FieldDefinitionsManager.tsx +1 -1
- package/src/backend/dashboard/DashboardScreen.tsx +3 -3
- package/src/backend/filters/AdvancedFilterPanel.tsx +1 -1
- package/src/backend/inputs/ComboboxInput.tsx +1 -1
- package/src/backend/messages/EmailThreadsPanel.tsx +1 -1
- package/src/backend/version-history/VersionHistoryPanel.tsx +1 -1
- package/src/primitives/ErrorNotice.tsx +1 -1
- package/src/primitives/Notice.tsx +5 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/ComboboxInput.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n clearable?: boolean\n clearLabel?: string\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nfunction areOptionsEqual(a: ComboboxOption[], b: ComboboxOption[]): boolean {\n if (a.length !== b.length) return false\n return a.every((option, index) => {\n const next = b[index]\n return option.value === next.value\n && option.label === next.label\n && (option.description ?? null) === (next.description ?? null)\n })\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n clearable = false,\n clearLabel,\n}: ComboboxInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')\n const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions\u2026')\n const resolvedClearLabel = clearLabel ?? t('ui.inputs.comboboxInput.clear', 'Clear value')\n const blurCloseDelayMs = 250\n const blurCloseMaxDelayMs = 1000\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const loadingRef = React.useRef(false)\n const blurCloseTimerRef = React.useRef<number | null>(null)\n const blurClosePendingRef = React.useRef(false)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n const eagerFallbackLoadedValueRef = React.useRef<string | null>(null)\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Single pass over all option sources to build both coverage sets at once.\n // knownLabelValues: values with a genuine label (not a self-mapping placeholder) \u2014\n // used to decide whether eager resolution still needs to run.\n // coveredOptionValues: all values present in any source (even self-mapped).\n const { knownLabelValues, coveredOptionValues } = React.useMemo(() => {\n const known = new Set<string>()\n const covered = new Set<string>()\n for (const opt of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n covered.add(opt.value)\n if (opt.label && opt.label !== opt.value) known.add(opt.value)\n }\n return { knownLabelValues: known, coveredOptionValues: covered }\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n map.set(value, {\n value,\n label: value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n React.useEffect(() => {\n loadingRef.current = loading\n }, [loading])\n\n const clearBlurCloseTimer = React.useCallback(() => {\n if (blurCloseTimerRef.current === null) return\n window.clearTimeout(blurCloseTimerRef.current)\n blurCloseTimerRef.current = null\n }, [])\n\n const resetBlurCloseState = React.useCallback(() => {\n blurClosePendingRef.current = false\n clearBlurCloseTimer()\n }, [clearBlurCloseTimer])\n\n React.useEffect(() => resetBlurCloseState, [resetBlurCloseState])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(() => {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions(query))\n .then((items) => {\n if (!cancelled) {\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n }\n })\n .catch(() => {})\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n if (knownLabelValues.has(value)) return\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n if (coveredOptionValues.has(value)) return\n if (eagerFallbackLoadedValueRef.current === value) return\n eagerFallbackLoadedValueRef.current = value\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n // Note: if the loader is paginated and the value falls outside the first page,\n // the fallback silently fails and the raw value remains visible.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps \u2014 resolveDescription intentionally excluded:\n // including it would re-run the effect on every render when the prop is an inline function\n }, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with value when value changes externally and input is not focused.\n React.useEffect(() => {\n if (document.activeElement !== inputRef.current) {\n const option = optionMap.get(value)\n setInput(option?.label ?? value ?? '')\n }\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n resetBlurCloseState()\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n },\n [disabled, onChange, optionMap, resetBlurCloseState]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n if (clearable && raw.trim() === '') {\n selectValue('')\n return\n }\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, clearable, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleClear = React.useCallback(() => {\n if (disabled) return\n selectValue('')\n inputRef.current?.focus()\n }, [disabled, selectValue])\n\n const closeAfterBlur = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n blurClosePendingRef.current = false\n confirmSelection(input)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }, [confirmSelection, disabled, input])\n\n const attemptBlurClose = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n closeAfterBlur()\n }, [blurCloseMaxDelayMs, closeAfterBlur, disabled])\n\n React.useEffect(() => {\n if (!blurClosePendingRef.current) return\n if (loading) return\n clearBlurCloseTimer()\n closeAfterBlur()\n }, [clearBlurCloseTimer, closeAfterBlur, loading])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n const showClearButton = clearable && !disabled && (value !== '' || input !== '')\n\n return (\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className={[\n 'w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed',\n showClearButton ? 'pr-9' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n value={input}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n resetBlurCloseState()\n if (loadSuggestions && availableOptions.length === 0) {\n setLoading(true)\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay closing so clicks on the popup can resolve first. If async\n // suggestions are still loading, keep the dropdown open instead of\n // closing before the first payload arrives.\n blurClosePendingRef.current = true\n clearBlurCloseTimer()\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs)\n }}\n />\n\n {showClearButton ? (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={resolvedClearLabel}\n className=\"absolute right-1 top-1/2 -translate-y-1/2\"\n onMouseDown={(event) => event.preventDefault()}\n onClick={handleClear}\n >\n <X className=\"size-3\" />\n </IconButton>\n ) : null}\n\n {showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && (\n <div className=\"absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto\">\n {loading && touched ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">{loadingLabel}</div>\n ) : (\n <div className=\"flex flex-col gap-1\">\n {filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',\n index === selectedIndex ? 'bg-muted' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => {\n resetBlurCloseState()\n selectValue(option.value)\n }}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium text-foreground\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n clearable?: boolean\n clearLabel?: string\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nfunction areOptionsEqual(a: ComboboxOption[], b: ComboboxOption[]): boolean {\n if (a.length !== b.length) return false\n return a.every((option, index) => {\n const next = b[index]\n return option.value === next.value\n && option.label === next.label\n && (option.description ?? null) === (next.description ?? null)\n })\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n clearable = false,\n clearLabel,\n}: ComboboxInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')\n const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions\u2026')\n const resolvedClearLabel = clearLabel ?? t('ui.inputs.comboboxInput.clear', 'Clear value')\n const blurCloseDelayMs = 250\n const blurCloseMaxDelayMs = 1000\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const loadingRef = React.useRef(false)\n const blurCloseTimerRef = React.useRef<number | null>(null)\n const blurClosePendingRef = React.useRef(false)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n const eagerFallbackLoadedValueRef = React.useRef<string | null>(null)\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Single pass over all option sources to build both coverage sets at once.\n // knownLabelValues: values with a genuine label (not a self-mapping placeholder) \u2014\n // used to decide whether eager resolution still needs to run.\n // coveredOptionValues: all values present in any source (even self-mapped).\n const { knownLabelValues, coveredOptionValues } = React.useMemo(() => {\n const known = new Set<string>()\n const covered = new Set<string>()\n for (const opt of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n covered.add(opt.value)\n if (opt.label && opt.label !== opt.value) known.add(opt.value)\n }\n return { knownLabelValues: known, coveredOptionValues: covered }\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n map.set(value, {\n value,\n label: value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n React.useEffect(() => {\n loadingRef.current = loading\n }, [loading])\n\n const clearBlurCloseTimer = React.useCallback(() => {\n if (blurCloseTimerRef.current === null) return\n window.clearTimeout(blurCloseTimerRef.current)\n blurCloseTimerRef.current = null\n }, [])\n\n const resetBlurCloseState = React.useCallback(() => {\n blurClosePendingRef.current = false\n clearBlurCloseTimer()\n }, [clearBlurCloseTimer])\n\n React.useEffect(() => resetBlurCloseState, [resetBlurCloseState])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(() => {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions(query))\n .then((items) => {\n if (!cancelled) {\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n }\n })\n .catch(() => {})\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n if (knownLabelValues.has(value)) return\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n if (coveredOptionValues.has(value)) return\n if (eagerFallbackLoadedValueRef.current === value) return\n eagerFallbackLoadedValueRef.current = value\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n // Note: if the loader is paginated and the value falls outside the first page,\n // the fallback silently fails and the raw value remains visible.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions((prev) => areOptionsEqual(prev, normalized) ? prev : normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- resolveDescription intentionally excluded:\n // including it would re-run the effect on every render when the prop is an inline function\n }, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with value when value changes externally and input is not focused.\n React.useEffect(() => {\n if (document.activeElement !== inputRef.current) {\n const option = optionMap.get(value)\n setInput(option?.label ?? value ?? '')\n }\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n resetBlurCloseState()\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n },\n [disabled, onChange, optionMap, resetBlurCloseState]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n if (clearable && raw.trim() === '') {\n selectValue('')\n return\n }\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, clearable, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleClear = React.useCallback(() => {\n if (disabled) return\n selectValue('')\n inputRef.current?.focus()\n }, [disabled, selectValue])\n\n const closeAfterBlur = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n blurClosePendingRef.current = false\n confirmSelection(input)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }, [confirmSelection, disabled, input])\n\n const attemptBlurClose = React.useCallback(() => {\n blurCloseTimerRef.current = null\n if (disabled) return\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n closeAfterBlur()\n }, [blurCloseMaxDelayMs, closeAfterBlur, disabled])\n\n React.useEffect(() => {\n if (!blurClosePendingRef.current) return\n if (loading) return\n clearBlurCloseTimer()\n closeAfterBlur()\n }, [clearBlurCloseTimer, closeAfterBlur, loading])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n const showClearButton = clearable && !disabled && (value !== '' || input !== '')\n\n return (\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className={[\n 'w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed',\n showClearButton ? 'pr-9' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n value={input}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n resetBlurCloseState()\n if (loadSuggestions && availableOptions.length === 0) {\n setLoading(true)\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay closing so clicks on the popup can resolve first. If async\n // suggestions are still loading, keep the dropdown open instead of\n // closing before the first payload arrives.\n blurClosePendingRef.current = true\n clearBlurCloseTimer()\n if (loadingRef.current) {\n blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)\n return\n }\n blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs)\n }}\n />\n\n {showClearButton ? (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={resolvedClearLabel}\n className=\"absolute right-1 top-1/2 -translate-y-1/2\"\n onMouseDown={(event) => event.preventDefault()}\n onClick={handleClear}\n >\n <X className=\"size-3\" />\n </IconButton>\n ) : null}\n\n {showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && (\n <div className=\"absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto\">\n {loading && touched ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">{loadingLabel}</div>\n ) : (\n <div className=\"flex flex-col gap-1\">\n {filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',\n index === selectedIndex ? 'bg-muted' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => {\n resetBlurCloseState()\n selectValue(option.value)\n }}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium text-foreground\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -66,7 +66,7 @@ function EmailThreadsPanel({
|
|
|
66
66
|
] })
|
|
67
67
|
] }),
|
|
68
68
|
!canCompose && composeDisabledHint ? /* @__PURE__ */ jsx("div", { children: composeDisabledHint }) : null,
|
|
69
|
-
error ? /* @__PURE__ */ jsx(Alert, {
|
|
69
|
+
error ? /* @__PURE__ */ jsx(Alert, { status: "error", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null,
|
|
70
70
|
loading && threads.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground", children: [
|
|
71
71
|
/* @__PURE__ */ jsx(Spinner, { className: "h-4 w-4" }),
|
|
72
72
|
t("ui.email.threads.loading", "Loading conversations\u2026")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/messages/EmailThreadsPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ArrowDownLeft, ArrowUpRight, Check, Mail, RefreshCw, Reply, TriangleAlert } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { Badge } from '../../primitives/badge'\nimport { Spinner } from '../../primitives/spinner'\nimport { Alert, AlertDescription } from '../../primitives/alert'\nimport { EmptyState } from '../EmptyState'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type EmailThreadDirection = 'inbound' | 'outbound'\n\n/**\n * Optimistic delivery status for a just-sent outbound message. Server-confirmed\n * messages leave this `undefined` and render without a status indicator.\n */\nexport type EmailThreadMessageStatus = 'sending' | 'sent' | 'failed'\n\nexport type EmailThreadMessage = {\n id: string\n messageId: string | null\n rfcMessageId: string | null\n references: string[]\n direction: EmailThreadDirection\n fromName: string | null\n fromEmail: string | null\n to: string[]\n cc: string[]\n subject: string | null\n bodyText: string | null\n sentAt: string\n providerKey: string | null\n /** Optimistic send status; absent on server-confirmed messages. */\n status?: EmailThreadMessageStatus\n /** Human-readable failure reason shown when `status === 'failed'`. */\n statusError?: string | null\n}\n\nexport type EmailThread = {\n threadKey: string\n subject: string | null\n preview: string | null\n participants: string[]\n lastMessageAt: string\n messageCount: number\n providerKey: string | null\n lastDirection: EmailThreadDirection\n messages: EmailThreadMessage[]\n}\n\nexport type EmailThreadsPanelProps = {\n threads: EmailThread[]\n loading?: boolean\n error?: string | null\n /** When false, compose/reply controls are hidden and `composeDisabledHint` is shown. */\n canCompose?: boolean\n composeDisabledHint?: React.ReactNode\n onComposeNew?: () => void\n onReply?: (thread: EmailThread) => void\n onRefresh?: () => void\n /** Invoked when the user retries a message whose `status === 'failed'`. */\n onRetry?: (message: EmailThreadMessage) => void\n className?: string\n}\n\nfunction formatWhen(value: string): string {\n return formatDateTime(value) ?? value\n}\n\n/**\n * Entity-agnostic Gmail-style email thread viewer. The host supplies the\n * already-fetched threads plus compose/reply/refresh handlers; this component\n * owns only the master/detail layout, selection, and conversation rendering, so\n * it can be dropped onto the CRM Person page, a Company page, or any module.\n */\nexport function EmailThreadsPanel({\n threads,\n loading = false,\n error = null,\n canCompose = false,\n composeDisabledHint,\n onComposeNew,\n onReply,\n onRefresh,\n onRetry,\n className,\n}: EmailThreadsPanelProps) {\n const t = useT()\n const [selectedKey, setSelectedKey] = React.useState<string | null>(null)\n\n // Keep a valid selection as the thread set changes (refresh, new email).\n React.useEffect(() => {\n if (threads.length === 0) {\n setSelectedKey(null)\n return\n }\n setSelectedKey((current) =>\n current && threads.some((thread) => thread.threadKey === current)\n ? current\n : threads[0].threadKey,\n )\n }, [threads])\n\n const selected = React.useMemo(\n () => threads.find((thread) => thread.threadKey === selectedKey) ?? null,\n [threads, selectedKey],\n )\n\n return (\n <div className={cn('flex flex-col gap-4', className)}>\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm text-muted-foreground\">\n {t('ui.email.threads.count', '{count} conversations', { count: threads.length })}\n </div>\n <div className=\"flex items-center gap-2\">\n {onRefresh ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n onClick={onRefresh}\n disabled={loading}\n >\n <RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />\n {t('ui.email.threads.refresh', 'Refresh')}\n </Button>\n ) : null}\n {canCompose && onComposeNew ? (\n <Button type=\"button\" size=\"sm\" className=\"gap-2\" onClick={onComposeNew}>\n <Mail className=\"h-4 w-4\" />\n {t('ui.email.threads.new', 'New email')}\n </Button>\n ) : null}\n </div>\n </div>\n\n {!canCompose && composeDisabledHint ? <div>{composeDisabledHint}</div> : null}\n\n {error ? (\n <Alert variant=\"destructive\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n {loading && threads.length === 0 ? (\n <div className=\"flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {t('ui.email.threads.loading', 'Loading conversations\u2026')}\n </div>\n ) : threads.length === 0 ? (\n <EmptyState\n icon={<Mail className=\"h-6 w-6\" />}\n title={t('ui.email.threads.empty.title', 'No emails yet')}\n description={t(\n 'ui.email.threads.empty.description',\n 'Emails you send or receive with this contact will appear here, grouped into conversations.',\n )}\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-3\">\n {/* Master: thread list */}\n <div className=\"md:col-span-1\">\n <ul className=\"max-h-96 divide-y divide-border overflow-y-auto rounded-md border border-border\">\n {threads.map((thread) => {\n const isSelected = thread.threadKey === selectedKey\n return (\n <li key={thread.threadKey}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => setSelectedKey(thread.threadKey)}\n className={cn(\n 'h-auto w-full flex-col items-stretch justify-start gap-1 whitespace-normal rounded-none p-3 text-left hover:bg-muted',\n isSelected ? 'bg-muted' : 'bg-transparent',\n )}\n aria-current={isSelected}\n >\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"truncate text-sm font-medium text-foreground\">\n {thread.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {formatWhen(thread.lastMessageAt)}\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs text-muted-foreground\">\n {thread.participants.join(', ') ||\n t('ui.email.threads.unknownParticipant', 'Unknown sender')}\n </span>\n <Badge variant=\"secondary\" className=\"shrink-0\">\n {thread.messageCount}\n </Badge>\n </div>\n {thread.preview ? (\n <span className=\"line-clamp-1 text-xs text-muted-foreground\">\n {thread.preview}\n </span>\n ) : null}\n </Button>\n </li>\n )\n })}\n </ul>\n </div>\n\n {/* Detail: conversation */}\n <div className=\"md:col-span-2\">\n {selected ? (\n <div className=\"flex flex-col gap-3 rounded-md border border-border p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"text-base font-semibold text-foreground\">\n {selected.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </h3>\n {canCompose && onReply ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"shrink-0 gap-2\"\n onClick={() => onReply(selected)}\n >\n <Reply className=\"h-4 w-4\" />\n {t('ui.email.threads.reply', 'Reply')}\n </Button>\n ) : null}\n </div>\n <div className=\"flex flex-col gap-3\">\n {selected.messages.map((message) => (\n <EmailMessageCard key={message.id} message={message} onRetry={onRetry} />\n ))}\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-center rounded-md border border-border p-10 text-sm text-muted-foreground\">\n {t('ui.email.threads.selectPrompt', 'Select a conversation to read it.')}\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n )\n}\n\nfunction EmailMessageCard({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n const isOutbound = message.direction === 'outbound'\n const fromLabel = message.fromName\n ? `${message.fromName}${message.fromEmail ? ` <${message.fromEmail}>` : ''}`\n : message.fromEmail ?? t('ui.email.threads.unknownParticipant', 'Unknown sender')\n return (\n <div\n className={`rounded-md border border-border p-3 ${isOutbound ? 'bg-accent' : 'bg-muted'}`}\n >\n <div className=\"mb-2 flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n {isOutbound ? (\n <ArrowUpRight className=\"h-3.5 w-3.5\" />\n ) : (\n <ArrowDownLeft className=\"h-3.5 w-3.5\" />\n )}\n <span className=\"font-medium text-foreground\">\n {isOutbound ? t('ui.email.threads.you', 'You') : fromLabel}\n </span>\n {message.to.length > 0 ? (\n <span className=\"truncate\">\n {t('ui.email.threads.toLabel', 'to {recipients}', {\n recipients: message.to.join(', '),\n })}\n </span>\n ) : null}\n </div>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{formatWhen(message.sentAt)}</span>\n </div>\n <div className=\"whitespace-pre-wrap break-words text-sm text-foreground\">\n {message.bodyText || t('ui.email.threads.noBody', '(no content)')}\n </div>\n <EmailMessageStatus message={message} onRetry={onRetry} />\n </div>\n )\n}\n\n/**\n * Renders the optimistic send-status footer for an outbound message: a spinner\n * while sending, a success check once confirmed, or an error with a Retry action\n * on delivery failure. Server-confirmed messages have no `status` and render\n * nothing here.\n */\nfunction EmailMessageStatus({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n if (message.status === 'sending') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-muted-foreground\" aria-live=\"polite\">\n <Spinner className=\"h-3 w-3\" />\n {t('ui.email.threads.status.sending', 'Sending\u2026')}\n </div>\n )\n }\n if (message.status === 'sent') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-status-success-text\" aria-live=\"polite\">\n <Check className=\"h-3.5 w-3.5 text-status-success-icon\" />\n {t('ui.email.threads.status.sent', 'Sent')}\n </div>\n )\n }\n if (message.status === 'failed') {\n return (\n <div\n className=\"mt-2 flex flex-wrap items-center gap-2 text-xs text-status-error-text\"\n aria-live=\"polite\"\n >\n <span className=\"flex items-center gap-1.5\">\n <TriangleAlert className=\"h-3.5 w-3.5 text-status-error-icon\" />\n {message.statusError || t('ui.email.threads.status.failed', 'Not delivered')}\n </span>\n {onRetry ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto px-2 py-0.5 text-xs\"\n onClick={() => onRetry(message)}\n >\n {t('ui.email.threads.status.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n )\n }\n return null\n}\n\nexport default EmailThreadsPanel\n"],
|
|
5
|
-
"mappings": ";AAkHQ,cAKI,YALJ;AAhHR,YAAY,WAAW;AACvB,SAAS,eAAe,cAAc,OAAO,MAAM,WAAW,OAAO,qBAAqB;AAC1F,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,eAAe;AACxB,SAAS,OAAO,wBAAwB;AACxC,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAC/B,SAAS,YAAY;AACrB,SAAS,UAAU;AAyDnB,SAAS,WAAW,OAAuB;AACzC,SAAO,eAAe,KAAK,KAAK;AAClC;AAQO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AACzB,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAwB,IAAI;AAGxE,QAAM,UAAU,MAAM;AACpB,QAAI,QAAQ,WAAW,GAAG;AACxB,qBAAe,IAAI;AACnB;AAAA,IACF;AACA;AAAA,MAAe,CAAC,YACd,WAAW,QAAQ,KAAK,CAAC,WAAW,OAAO,cAAc,OAAO,IAC5D,UACA,QAAQ,CAAC,EAAE;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,WAAW,MAAM;AAAA,IACrB,MAAM,QAAQ,KAAK,CAAC,WAAW,OAAO,cAAc,WAAW,KAAK;AAAA,IACpE,CAAC,SAAS,WAAW;AAAA,EACvB;AAEA,SACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,SAAS,GACjD;AAAA,yBAAC,SAAI,WAAU,2CACb;AAAA,0BAAC,SAAI,WAAU,iCACZ,YAAE,0BAA0B,yBAAyB,EAAE,OAAO,QAAQ,OAAO,CAAC,GACjF;AAAA,MACA,qBAAC,SAAI,WAAU,2BACZ;AAAA,oBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,WAAU;AAAA,YACV,SAAS;AAAA,YACT,UAAU;AAAA,YAEV;AAAA,kCAAC,aAAU,WAAW,GAAG,WAAW,WAAW,cAAc,GAAG;AAAA,cAC/D,EAAE,4BAA4B,SAAS;AAAA;AAAA;AAAA,QAC1C,IACE;AAAA,QACH,cAAc,eACb,qBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,WAAU,SAAQ,SAAS,cACzD;AAAA,8BAAC,QAAK,WAAU,WAAU;AAAA,UACzB,EAAE,wBAAwB,WAAW;AAAA,WACxC,IACE;AAAA,SACN;AAAA,OACF;AAAA,IAEC,CAAC,cAAc,sBAAsB,oBAAC,SAAK,+BAAoB,IAAS;AAAA,IAExE,QACC,oBAAC,SAAM,
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ArrowDownLeft, ArrowUpRight, Check, Mail, RefreshCw, Reply, TriangleAlert } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { Badge } from '../../primitives/badge'\nimport { Spinner } from '../../primitives/spinner'\nimport { Alert, AlertDescription } from '../../primitives/alert'\nimport { EmptyState } from '../EmptyState'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type EmailThreadDirection = 'inbound' | 'outbound'\n\n/**\n * Optimistic delivery status for a just-sent outbound message. Server-confirmed\n * messages leave this `undefined` and render without a status indicator.\n */\nexport type EmailThreadMessageStatus = 'sending' | 'sent' | 'failed'\n\nexport type EmailThreadMessage = {\n id: string\n messageId: string | null\n rfcMessageId: string | null\n references: string[]\n direction: EmailThreadDirection\n fromName: string | null\n fromEmail: string | null\n to: string[]\n cc: string[]\n subject: string | null\n bodyText: string | null\n sentAt: string\n providerKey: string | null\n /** Optimistic send status; absent on server-confirmed messages. */\n status?: EmailThreadMessageStatus\n /** Human-readable failure reason shown when `status === 'failed'`. */\n statusError?: string | null\n}\n\nexport type EmailThread = {\n threadKey: string\n subject: string | null\n preview: string | null\n participants: string[]\n lastMessageAt: string\n messageCount: number\n providerKey: string | null\n lastDirection: EmailThreadDirection\n messages: EmailThreadMessage[]\n}\n\nexport type EmailThreadsPanelProps = {\n threads: EmailThread[]\n loading?: boolean\n error?: string | null\n /** When false, compose/reply controls are hidden and `composeDisabledHint` is shown. */\n canCompose?: boolean\n composeDisabledHint?: React.ReactNode\n onComposeNew?: () => void\n onReply?: (thread: EmailThread) => void\n onRefresh?: () => void\n /** Invoked when the user retries a message whose `status === 'failed'`. */\n onRetry?: (message: EmailThreadMessage) => void\n className?: string\n}\n\nfunction formatWhen(value: string): string {\n return formatDateTime(value) ?? value\n}\n\n/**\n * Entity-agnostic Gmail-style email thread viewer. The host supplies the\n * already-fetched threads plus compose/reply/refresh handlers; this component\n * owns only the master/detail layout, selection, and conversation rendering, so\n * it can be dropped onto the CRM Person page, a Company page, or any module.\n */\nexport function EmailThreadsPanel({\n threads,\n loading = false,\n error = null,\n canCompose = false,\n composeDisabledHint,\n onComposeNew,\n onReply,\n onRefresh,\n onRetry,\n className,\n}: EmailThreadsPanelProps) {\n const t = useT()\n const [selectedKey, setSelectedKey] = React.useState<string | null>(null)\n\n // Keep a valid selection as the thread set changes (refresh, new email).\n React.useEffect(() => {\n if (threads.length === 0) {\n setSelectedKey(null)\n return\n }\n setSelectedKey((current) =>\n current && threads.some((thread) => thread.threadKey === current)\n ? current\n : threads[0].threadKey,\n )\n }, [threads])\n\n const selected = React.useMemo(\n () => threads.find((thread) => thread.threadKey === selectedKey) ?? null,\n [threads, selectedKey],\n )\n\n return (\n <div className={cn('flex flex-col gap-4', className)}>\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm text-muted-foreground\">\n {t('ui.email.threads.count', '{count} conversations', { count: threads.length })}\n </div>\n <div className=\"flex items-center gap-2\">\n {onRefresh ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n onClick={onRefresh}\n disabled={loading}\n >\n <RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />\n {t('ui.email.threads.refresh', 'Refresh')}\n </Button>\n ) : null}\n {canCompose && onComposeNew ? (\n <Button type=\"button\" size=\"sm\" className=\"gap-2\" onClick={onComposeNew}>\n <Mail className=\"h-4 w-4\" />\n {t('ui.email.threads.new', 'New email')}\n </Button>\n ) : null}\n </div>\n </div>\n\n {!canCompose && composeDisabledHint ? <div>{composeDisabledHint}</div> : null}\n\n {error ? (\n <Alert status=\"error\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n {loading && threads.length === 0 ? (\n <div className=\"flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {t('ui.email.threads.loading', 'Loading conversations\u2026')}\n </div>\n ) : threads.length === 0 ? (\n <EmptyState\n icon={<Mail className=\"h-6 w-6\" />}\n title={t('ui.email.threads.empty.title', 'No emails yet')}\n description={t(\n 'ui.email.threads.empty.description',\n 'Emails you send or receive with this contact will appear here, grouped into conversations.',\n )}\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-3\">\n {/* Master: thread list */}\n <div className=\"md:col-span-1\">\n <ul className=\"max-h-96 divide-y divide-border overflow-y-auto rounded-md border border-border\">\n {threads.map((thread) => {\n const isSelected = thread.threadKey === selectedKey\n return (\n <li key={thread.threadKey}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => setSelectedKey(thread.threadKey)}\n className={cn(\n 'h-auto w-full flex-col items-stretch justify-start gap-1 whitespace-normal rounded-none p-3 text-left hover:bg-muted',\n isSelected ? 'bg-muted' : 'bg-transparent',\n )}\n aria-current={isSelected}\n >\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"truncate text-sm font-medium text-foreground\">\n {thread.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {formatWhen(thread.lastMessageAt)}\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs text-muted-foreground\">\n {thread.participants.join(', ') ||\n t('ui.email.threads.unknownParticipant', 'Unknown sender')}\n </span>\n <Badge variant=\"secondary\" className=\"shrink-0\">\n {thread.messageCount}\n </Badge>\n </div>\n {thread.preview ? (\n <span className=\"line-clamp-1 text-xs text-muted-foreground\">\n {thread.preview}\n </span>\n ) : null}\n </Button>\n </li>\n )\n })}\n </ul>\n </div>\n\n {/* Detail: conversation */}\n <div className=\"md:col-span-2\">\n {selected ? (\n <div className=\"flex flex-col gap-3 rounded-md border border-border p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"text-base font-semibold text-foreground\">\n {selected.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </h3>\n {canCompose && onReply ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"shrink-0 gap-2\"\n onClick={() => onReply(selected)}\n >\n <Reply className=\"h-4 w-4\" />\n {t('ui.email.threads.reply', 'Reply')}\n </Button>\n ) : null}\n </div>\n <div className=\"flex flex-col gap-3\">\n {selected.messages.map((message) => (\n <EmailMessageCard key={message.id} message={message} onRetry={onRetry} />\n ))}\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-center rounded-md border border-border p-10 text-sm text-muted-foreground\">\n {t('ui.email.threads.selectPrompt', 'Select a conversation to read it.')}\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n )\n}\n\nfunction EmailMessageCard({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n const isOutbound = message.direction === 'outbound'\n const fromLabel = message.fromName\n ? `${message.fromName}${message.fromEmail ? ` <${message.fromEmail}>` : ''}`\n : message.fromEmail ?? t('ui.email.threads.unknownParticipant', 'Unknown sender')\n return (\n <div\n className={`rounded-md border border-border p-3 ${isOutbound ? 'bg-accent' : 'bg-muted'}`}\n >\n <div className=\"mb-2 flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n {isOutbound ? (\n <ArrowUpRight className=\"h-3.5 w-3.5\" />\n ) : (\n <ArrowDownLeft className=\"h-3.5 w-3.5\" />\n )}\n <span className=\"font-medium text-foreground\">\n {isOutbound ? t('ui.email.threads.you', 'You') : fromLabel}\n </span>\n {message.to.length > 0 ? (\n <span className=\"truncate\">\n {t('ui.email.threads.toLabel', 'to {recipients}', {\n recipients: message.to.join(', '),\n })}\n </span>\n ) : null}\n </div>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{formatWhen(message.sentAt)}</span>\n </div>\n <div className=\"whitespace-pre-wrap break-words text-sm text-foreground\">\n {message.bodyText || t('ui.email.threads.noBody', '(no content)')}\n </div>\n <EmailMessageStatus message={message} onRetry={onRetry} />\n </div>\n )\n}\n\n/**\n * Renders the optimistic send-status footer for an outbound message: a spinner\n * while sending, a success check once confirmed, or an error with a Retry action\n * on delivery failure. Server-confirmed messages have no `status` and render\n * nothing here.\n */\nfunction EmailMessageStatus({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n if (message.status === 'sending') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-muted-foreground\" aria-live=\"polite\">\n <Spinner className=\"h-3 w-3\" />\n {t('ui.email.threads.status.sending', 'Sending\u2026')}\n </div>\n )\n }\n if (message.status === 'sent') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-status-success-text\" aria-live=\"polite\">\n <Check className=\"h-3.5 w-3.5 text-status-success-icon\" />\n {t('ui.email.threads.status.sent', 'Sent')}\n </div>\n )\n }\n if (message.status === 'failed') {\n return (\n <div\n className=\"mt-2 flex flex-wrap items-center gap-2 text-xs text-status-error-text\"\n aria-live=\"polite\"\n >\n <span className=\"flex items-center gap-1.5\">\n <TriangleAlert className=\"h-3.5 w-3.5 text-status-error-icon\" />\n {message.statusError || t('ui.email.threads.status.failed', 'Not delivered')}\n </span>\n {onRetry ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto px-2 py-0.5 text-xs\"\n onClick={() => onRetry(message)}\n >\n {t('ui.email.threads.status.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n )\n }\n return null\n}\n\nexport default EmailThreadsPanel\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -156,7 +156,7 @@ function VersionHistoryPanel({
|
|
|
156
156
|
)
|
|
157
157
|
] }),
|
|
158
158
|
/* @__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: [
|
|
159
|
-
shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? /* @__PURE__ */ jsx(Alert, {
|
|
159
|
+
shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? /* @__PURE__ */ jsx(Alert, { status: "information", children: /* @__PURE__ */ jsx(AlertDescription, { children: t("audit_logs.hint.view_self_only", "Showing only your own changes. Contact an administrator for broader access.") }) }) : null,
|
|
160
160
|
isInitialLoading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-muted-foreground", children: [
|
|
161
161
|
/* @__PURE__ */ jsx(Loader2, { className: "mb-2 h-8 w-8 animate-spin" }),
|
|
162
162
|
/* @__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 { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { humanizeResourceKind } from './labels'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\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 logger.error('Undo failed', { 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 logger.error('Redo failed', { 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-overlay bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-modal 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/50 ${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-overline 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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
|
|
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'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\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 logger.error('Undo failed', { 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 logger.error('Redo failed', { 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-overlay bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-modal 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 status=\"information\">\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/50 ${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-overline 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
|
}
|
|
@@ -6,7 +6,7 @@ function ErrorNotice({ title, message, action }) {
|
|
|
6
6
|
const t = useT();
|
|
7
7
|
const resolvedTitle = title ?? t("ui.errors.defaultTitle", "Something went wrong");
|
|
8
8
|
const resolvedMessage = message ?? t("ui.errors.defaultMessage", "Unable to load data. Please try again.");
|
|
9
|
-
return /* @__PURE__ */ jsxs(Alert, {
|
|
9
|
+
return /* @__PURE__ */ jsxs(Alert, { status: "error", children: [
|
|
10
10
|
/* @__PURE__ */ jsx(AlertTitle, { children: resolvedTitle }),
|
|
11
11
|
/* @__PURE__ */ jsx(AlertDescription, { children: resolvedMessage }),
|
|
12
12
|
action ? /* @__PURE__ */ jsx("div", { className: "mt-2", children: action }) : null
|
|
@@ -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 { 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
|
|
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,
|
|
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 status=\"error\">\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,QAAO,SACZ;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
|
}
|
|
@@ -27,7 +27,7 @@ function Notice({
|
|
|
27
27
|
compact = false
|
|
28
28
|
}) {
|
|
29
29
|
if (process.env.NODE_ENV === "development") {
|
|
30
|
-
logger.warn('<Notice> is deprecated. Use <Alert
|
|
30
|
+
logger.warn('<Notice> is deprecated. Use <Alert status="error|warning|information"> instead. See: docs/design-system/migration-tables.md');
|
|
31
31
|
}
|
|
32
32
|
const styles = variantStyles[variant];
|
|
33
33
|
if (compact || !title && !action && (children || message)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/Notice.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\n/**\n * @deprecated Use <Alert
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\n/**\n * @deprecated Use <Alert status=\"error|warning|information\"> instead.\n * Migration guide: docs/design-system/migration-tables.md#j3-component-mapping\n *\n * Notice variant=\"error\" \u2192 Alert status=\"error\"\n * Notice variant=\"warning\" \u2192 Alert status=\"warning\"\n * Notice variant=\"info\" \u2192 Alert status=\"information\"\n */\n\nconst variantStyles = {\n error: {\n container: 'border-status-error-border bg-status-error-bg text-status-error-text',\n icon: 'border-status-error-icon',\n },\n info: {\n container: 'border-status-info-border bg-status-info-bg text-status-info-text',\n icon: 'border-status-info-icon',\n },\n warning: {\n container: 'border-status-warning-border bg-status-warning-bg text-status-warning-text',\n icon: 'border-status-warning-icon',\n },\n} as const\n\nexport type NoticeVariant = keyof typeof variantStyles\n\nexport type NoticeProps = {\n variant?: NoticeVariant\n title?: string\n message?: string\n action?: React.ReactNode\n children?: React.ReactNode\n className?: string\n compact?: boolean\n}\n\nexport function Notice({\n variant = 'info',\n title,\n message,\n action,\n children,\n className,\n compact = false,\n}: NoticeProps) {\n if (process.env.NODE_ENV === 'development') {\n logger.warn('<Notice> is deprecated. Use <Alert status=\"error|warning|information\"> instead. See: docs/design-system/migration-tables.md')\n }\n\n const styles = variantStyles[variant]\n\n if (compact || (!title && !action && (children || message))) {\n return (\n <div className={cn('rounded-md border px-3 py-2 text-sm', styles.container, className)}>\n {children ?? message}\n </div>\n )\n }\n\n return (\n <div className={cn('rounded-md border p-4', styles.container, className)}>\n <div className=\"flex items-start gap-3\">\n <span\n className={cn('inline-block mt-0.5 h-4 w-4 rounded-full border-2 shrink-0', styles.icon)}\n aria-hidden\n />\n <div className=\"space-y-1\">\n {title ? <div className=\"text-sm font-medium\">{title}</div> : null}\n {message ? <div className=\"text-sm opacity-90\">{message}</div> : null}\n {children}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n </div>\n </div>\n )\n}\n"],
|
|
5
5
|
"mappings": ";AA4DM,cAaE,YAbF;AA1DN,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI;AAWhC,MAAM,gBAAgB;AAAA,EACpB,OAAO;AAAA,IACL,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AACF;AAcO,SAAS,OAAO;AAAA,EACrB,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GAAgB;AACd,MAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,WAAO,KAAK,6HAA6H;AAAA,EAC3I;AAEA,QAAM,SAAS,cAAc,OAAO;AAEpC,MAAI,WAAY,CAAC,SAAS,CAAC,WAAW,YAAY,UAAW;AAC3D,WACE,oBAAC,SAAI,WAAW,GAAG,uCAAuC,OAAO,WAAW,SAAS,GAClF,sBAAY,SACf;AAAA,EAEJ;AAEA,SACE,oBAAC,SAAI,WAAW,GAAG,yBAAyB,OAAO,WAAW,SAAS,GACrE,+BAAC,SAAI,WAAU,0BACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,GAAG,8DAA8D,OAAO,IAAI;AAAA,QACvF,eAAW;AAAA;AAAA,IACb;AAAA,IACA,qBAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,oBAAC,SAAI,WAAU,uBAAuB,iBAAM,IAAS;AAAA,MAC7D,UAAU,oBAAC,SAAI,WAAU,sBAAsB,mBAAQ,IAAS;AAAA,MAChE;AAAA,MACA,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,OACnD;AAAA,KACF,GACF;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.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6869.1.d1ce9999ea",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
158
|
+
"@open-mercato/shared": "0.6.7-develop.6869.1.d1ce9999ea",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
165
|
+
"@open-mercato/shared": "0.6.7-develop.6869.1.d1ce9999ea",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.0",
|
|
168
168
|
"@testing-library/react": "^16.3.1",
|
package/src/ai/AiChat.tsx
CHANGED
|
@@ -292,10 +292,10 @@ interface ServerEmittedUiPartRef {
|
|
|
292
292
|
pendingActionId?: string
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
function
|
|
295
|
+
function mapErrorCodeToStatus(
|
|
296
296
|
code: string | undefined,
|
|
297
|
-
): '
|
|
298
|
-
if (!code) return '
|
|
297
|
+
): 'error' | 'warning' {
|
|
298
|
+
if (!code) return 'error'
|
|
299
299
|
// Policy denies that describe a filtered tool or attachment surface a
|
|
300
300
|
// warning alert; caller can still continue. Hard denials (agent_unknown,
|
|
301
301
|
// agent_features_denied, unauthenticated, execution_mode_not_supported,
|
|
@@ -308,7 +308,7 @@ function mapErrorCodeToVariant(
|
|
|
308
308
|
'moderation_blocked',
|
|
309
309
|
'moderation_unavailable',
|
|
310
310
|
])
|
|
311
|
-
return warningCodes.has(code) ? 'warning' : '
|
|
311
|
+
return warningCodes.has(code) ? 'warning' : 'error'
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/**
|
|
@@ -1570,7 +1570,7 @@ export function AiChat({
|
|
|
1570
1570
|
const resolvedPlaceholder =
|
|
1571
1571
|
placeholder ?? t('ai_assistant.chat.composerPlaceholder', 'Message the AI agent...')
|
|
1572
1572
|
|
|
1573
|
-
const
|
|
1573
|
+
const errorStatus = mapErrorCodeToStatus(chat.error?.code)
|
|
1574
1574
|
|
|
1575
1575
|
return (
|
|
1576
1576
|
<section
|
|
@@ -1651,7 +1651,7 @@ export function AiChat({
|
|
|
1651
1651
|
</div>
|
|
1652
1652
|
|
|
1653
1653
|
{chat.error ? (
|
|
1654
|
-
<Alert
|
|
1654
|
+
<Alert status={errorStatus} data-ai-chat-error={chat.error.code ?? 'unknown'}>
|
|
1655
1655
|
<AlertTitle>
|
|
1656
1656
|
{t('ai_assistant.chat.errorTitle', 'Agent dispatch failed')}
|
|
1657
1657
|
</AlertTitle>
|
|
@@ -19,7 +19,7 @@ export function LoopDisabledBanner({ agentId }: { agentId?: string }) {
|
|
|
19
19
|
const t = useT()
|
|
20
20
|
return (
|
|
21
21
|
<Alert
|
|
22
|
-
|
|
22
|
+
status="warning"
|
|
23
23
|
icon={<ShieldOff aria-hidden="true" />}
|
|
24
24
|
data-ai-loop-disabled-banner
|
|
25
25
|
data-ai-loop-disabled-agent-id={agentId}
|
|
@@ -210,7 +210,7 @@ function ConfirmationErrorAlert({
|
|
|
210
210
|
: []
|
|
211
211
|
return (
|
|
212
212
|
<Alert
|
|
213
|
-
|
|
213
|
+
status="warning"
|
|
214
214
|
className="mt-3"
|
|
215
215
|
data-ai-confirmation-error="stale_version"
|
|
216
216
|
>
|
|
@@ -247,7 +247,7 @@ function ConfirmationErrorAlert({
|
|
|
247
247
|
if (code === 'schema_drift') {
|
|
248
248
|
return (
|
|
249
249
|
<Alert
|
|
250
|
-
|
|
250
|
+
status="warning"
|
|
251
251
|
className="mt-3"
|
|
252
252
|
data-ai-confirmation-error="schema_drift"
|
|
253
253
|
>
|
|
@@ -270,7 +270,7 @@ function ConfirmationErrorAlert({
|
|
|
270
270
|
if (code === 'invalid_status') {
|
|
271
271
|
return (
|
|
272
272
|
<Alert
|
|
273
|
-
|
|
273
|
+
status="warning"
|
|
274
274
|
className="mt-3"
|
|
275
275
|
data-ai-confirmation-error="invalid_status"
|
|
276
276
|
>
|
|
@@ -291,7 +291,7 @@ function ConfirmationErrorAlert({
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
return (
|
|
294
|
-
<Alert
|
|
294
|
+
<Alert status="error" className="mt-3" data-ai-confirmation-error={code}>
|
|
295
295
|
<AlertTitle>
|
|
296
296
|
{t('ai_assistant.chat.mutation_cards.confirmation.errorTitle', 'Confirm failed')}
|
|
297
297
|
</AlertTitle>
|
|
@@ -128,7 +128,7 @@ export function FieldDiffCard({ fieldDiff, records }: FieldDiffCardProps) {
|
|
|
128
128
|
afterHeader={afterHeader}
|
|
129
129
|
/>
|
|
130
130
|
) : (
|
|
131
|
-
<Alert
|
|
131
|
+
<Alert status="information">
|
|
132
132
|
<AlertDescription>
|
|
133
133
|
{t(
|
|
134
134
|
'ai_assistant.chat.mutation_cards.diff.empty',
|
|
@@ -145,7 +145,7 @@ export function FieldDiffCard({ fieldDiff, records }: FieldDiffCardProps) {
|
|
|
145
145
|
|
|
146
146
|
if (flat.length === 0) {
|
|
147
147
|
return (
|
|
148
|
-
<Alert
|
|
148
|
+
<Alert status="information" data-ai-field-diff-mode="empty">
|
|
149
149
|
<AlertDescription>
|
|
150
150
|
{t(
|
|
151
151
|
'ai_assistant.chat.mutation_cards.diff.empty',
|
|
@@ -79,7 +79,7 @@ export function MutationResultCard(props: MutationResultCardProps) {
|
|
|
79
79
|
|
|
80
80
|
if (isSuccessStatus(status) && failedRecords && failedRecords.length > 0) {
|
|
81
81
|
return (
|
|
82
|
-
<Alert
|
|
82
|
+
<Alert status="warning" data-ai-mutation-result="partial">
|
|
83
83
|
<AlertTitle>
|
|
84
84
|
{t(
|
|
85
85
|
'ai_assistant.chat.mutation_cards.result.partialTitle',
|
|
@@ -116,7 +116,7 @@ export function MutationResultCard(props: MutationResultCardProps) {
|
|
|
116
116
|
const recordId = result?.recordId ?? action.targetRecordId ?? null
|
|
117
117
|
const href = payload.recordHref ?? null
|
|
118
118
|
return (
|
|
119
|
-
<Alert
|
|
119
|
+
<Alert status="success" data-ai-mutation-result="success">
|
|
120
120
|
<AlertTitle>
|
|
121
121
|
{t('ai_assistant.chat.mutation_cards.result.successTitle', 'Action applied')}
|
|
122
122
|
</AlertTitle>
|
|
@@ -281,7 +281,7 @@ export function MutationResultCard(props: MutationResultCardProps) {
|
|
|
281
281
|
? errorDetails!.issues!.filter((entry) => entry && (entry.message || entry.path))
|
|
282
282
|
: []
|
|
283
283
|
return (
|
|
284
|
-
<Alert
|
|
284
|
+
<Alert status="error" data-ai-mutation-result="failure">
|
|
285
285
|
<AlertTitle>
|
|
286
286
|
{t(
|
|
287
287
|
'ai_assistant.chat.mutation_cards.result.failureTitle',
|
|
@@ -11,7 +11,7 @@ import type { AiUiPartProps } from '../ui-part-registry'
|
|
|
11
11
|
* Consumers that forget to register the real mutation-approval cards still
|
|
12
12
|
* see a humane "Phase 3 pending" state instead of the neutral debug chip the
|
|
13
13
|
* `<AiChat>` fallback uses for genuinely unknown component ids. Uses the
|
|
14
|
-
* shared DS `Alert` primitive with `
|
|
14
|
+
* shared DS `Alert` primitive with `status="information"` — no hardcoded colors.
|
|
15
15
|
*
|
|
16
16
|
* When Step 5.10 lands the real cards, app bootstrappers will call
|
|
17
17
|
* `registerAiUiPart('mutation-preview-card', MutationPreviewCard)` (etc.)
|
|
@@ -23,7 +23,7 @@ export function PendingPhase3Placeholder({ componentId }: AiUiPartProps) {
|
|
|
23
23
|
const t = useT()
|
|
24
24
|
return (
|
|
25
25
|
<Alert
|
|
26
|
-
|
|
26
|
+
status="information"
|
|
27
27
|
data-ai-ui-part-pending-phase3={componentId}
|
|
28
28
|
>
|
|
29
29
|
<AlertTitle>
|