@open-mercato/ui 0.7.1-develop.7178.1.ab93fbaf4e → 0.7.1-develop.7180.1.9717fbbb43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/backend/FlashMessages.js +34 -1
- package/dist/backend/FlashMessages.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +8 -0
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/inputs/LookupSelect.js +54 -2
- package/dist/backend/inputs/LookupSelect.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/FlashMessages.tsx +50 -1
- package/src/backend/__tests__/CrudForm.dotPath.test.tsx +17 -21
- package/src/backend/__tests__/CrudForm.navigation.test.tsx +3 -5
- package/src/backend/__tests__/CrudForm.transformData.test.tsx +7 -7
- package/src/backend/__tests__/FlashMessages.test.tsx +48 -0
- package/src/backend/icons/lucideRegistry.generated.tsx +8 -0
- package/src/backend/inputs/LookupSelect.tsx +73 -1
- package/src/backend/inputs/__tests__/LookupSelect.test.tsx +92 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Generated lucide registry with
|
|
1
|
+
Generated lucide registry with 165 icons -> /home/runner/work/open-mercato/open-mercato/packages/ui/src/backend/icons/lucideRegistry.generated.tsx
|
|
2
2
|
[build:ui] found 403 entry points
|
|
3
3
|
[build:ui] built successfully
|
|
@@ -29,6 +29,38 @@ function flash(message, type = "info") {
|
|
|
29
29
|
const evt = new CustomEvent("flash", { detail: { message, type } });
|
|
30
30
|
window.dispatchEvent(evt);
|
|
31
31
|
}
|
|
32
|
+
const mountedFlashHostIds = [];
|
|
33
|
+
const flashHostListeners = /* @__PURE__ */ new Set();
|
|
34
|
+
function notifyFlashHostListeners() {
|
|
35
|
+
for (const listener of flashHostListeners) listener();
|
|
36
|
+
}
|
|
37
|
+
function subscribeFlashHosts(listener) {
|
|
38
|
+
flashHostListeners.add(listener);
|
|
39
|
+
return () => {
|
|
40
|
+
flashHostListeners.delete(listener);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function getPrimaryFlashHostId() {
|
|
44
|
+
return mountedFlashHostIds[0] ?? null;
|
|
45
|
+
}
|
|
46
|
+
function getServerFlashHostId() {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function useIsPrimaryFlashHost() {
|
|
50
|
+
const hostIdRef = React.useRef(null);
|
|
51
|
+
if (hostIdRef.current === null) hostIdRef.current = /* @__PURE__ */ Symbol("om-flash-host");
|
|
52
|
+
const hostId = hostIdRef.current;
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
mountedFlashHostIds.push(hostId);
|
|
55
|
+
notifyFlashHostListeners();
|
|
56
|
+
return () => {
|
|
57
|
+
const index = mountedFlashHostIds.indexOf(hostId);
|
|
58
|
+
if (index >= 0) mountedFlashHostIds.splice(index, 1);
|
|
59
|
+
notifyFlashHostListeners();
|
|
60
|
+
};
|
|
61
|
+
}, [hostId]);
|
|
62
|
+
return React.useSyncExternalStore(subscribeFlashHosts, getPrimaryFlashHostId, getServerFlashHostId) === hostId;
|
|
63
|
+
}
|
|
32
64
|
function useLocationKey() {
|
|
33
65
|
const [locationKey, setLocationKey] = React.useState(() => {
|
|
34
66
|
if (typeof window === "undefined") return "";
|
|
@@ -91,6 +123,7 @@ function useLocationKey() {
|
|
|
91
123
|
function FlashMessagesInner() {
|
|
92
124
|
const [msg, setMsg] = React.useState(null);
|
|
93
125
|
const [kind, setKind] = React.useState("info");
|
|
126
|
+
const isPrimaryHost = useIsPrimaryFlashHost();
|
|
94
127
|
const locationKey = useLocationKey();
|
|
95
128
|
const dismissTimerRef = React.useRef(null);
|
|
96
129
|
const clearDismissTimer = React.useCallback(() => {
|
|
@@ -143,7 +176,7 @@ function FlashMessagesInner() {
|
|
|
143
176
|
clearDismissTimer();
|
|
144
177
|
setMsg(null);
|
|
145
178
|
}, [clearDismissTimer]);
|
|
146
|
-
if (!msg) return null;
|
|
179
|
+
if (!msg || !isPrimaryHost) return null;
|
|
147
180
|
return /* @__PURE__ */ jsx("div", { className: "pointer-events-none fixed left-3 right-3 top-3 z-toast sm:left-auto sm:right-4 sm:w-[380px]", children: /* @__PURE__ */ jsx("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx(
|
|
148
181
|
Alert,
|
|
149
182
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/backend/FlashMessages.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Alert, type AlertStatus } from '../primitives/alert'\n\nexport type FlashKind = 'success' | 'error' | 'warning' | 'info'\n\nconst flashKindToAlertStatus: Record<FlashKind, AlertStatus> = {\n success: 'success',\n error: 'error',\n warning: 'warning',\n info: 'information',\n}\n\nfunction normalizeFlashKind(value: string | null | undefined): FlashKind {\n if (value && Object.prototype.hasOwnProperty.call(flashKindToAlertStatus, value)) {\n return value as FlashKind\n }\n return 'success'\n}\n\n// A URL-supplied flash is only trustworthy when the navigation that carried it\n// originated from the same origin (an in-app POST->GET redirect or client-side\n// navigation). Cross-origin links \u2014 the phishing/content-spoofing vector \u2014 must\n// not be allowed to render attacker-controlled copy inside an authoritative banner.\nfunction isSameOriginFlashNavigation(): boolean {\n if (typeof window === 'undefined') return false\n const referrer = document.referrer\n // No referrer (direct load, bookmark, or referrer stripped) is ambiguous, not a\n // cross-origin redirect we can attribute to an attacker page; allow it.\n if (!referrer) return true\n try {\n return new URL(referrer).origin === window.location.origin\n } catch {\n return false\n }\n}\n\n// Programmatic API to show a flash message without navigation.\n// Consumers can import { flash } and call flash('text', 'error').\nexport function flash(message: string, type: FlashKind = 'info') {\n if (typeof window === 'undefined') return\n const evt = new CustomEvent('flash', { detail: { message, type } })\n window.dispatchEvent(evt)\n}\n\ntype HistoryMethod = History['pushState']\n\nfunction useLocationKey() {\n const [locationKey, setLocationKey] = React.useState(() => {\n if (typeof window === 'undefined') return ''\n return window.location.href\n })\n const locationKeyRef = React.useRef(locationKey)\n\n React.useEffect(() => {\n locationKeyRef.current = locationKey\n }, [locationKey])\n\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n\n let active = true\n const scheduleUpdate = (href: string) => {\n const run = () => {\n if (!active) return\n if (locationKeyRef.current === href) return\n locationKeyRef.current = href\n setLocationKey(href)\n }\n if (typeof queueMicrotask === 'function') {\n queueMicrotask(run)\n } else {\n setTimeout(run, 0)\n }\n }\n const updateLocation = () => {\n if (!active) return\n const href = window.location.href\n if (href === locationKeyRef.current) return\n scheduleUpdate(href)\n }\n\n const deferredUpdateLocation = () => {\n setTimeout(updateLocation, 0)\n }\n\n const originalPush: HistoryMethod = window.history.pushState.bind(window.history)\n const originalReplace: HistoryMethod = window.history.replaceState.bind(window.history)\n\n const pushState: HistoryMethod = (...args) => {\n originalPush(...args)\n deferredUpdateLocation()\n }\n\n const replaceState: HistoryMethod = (...args) => {\n originalReplace(...args)\n deferredUpdateLocation()\n }\n\n window.history.pushState = pushState\n window.history.replaceState = replaceState\n window.addEventListener('popstate', updateLocation)\n window.addEventListener('hashchange', updateLocation)\n updateLocation()\n\n return () => {\n active = false\n window.history.pushState = originalPush\n window.history.replaceState = originalReplace\n window.removeEventListener('popstate', updateLocation)\n window.removeEventListener('hashchange', updateLocation)\n }\n }, [])\n\n return locationKey\n}\n\nfunction FlashMessagesInner() {\n const [msg, setMsg] = React.useState<string | null>(null)\n const [kind, setKind] = React.useState<FlashKind>('info')\n const locationKey = useLocationKey()\n const dismissTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const clearDismissTimer = React.useCallback(() => {\n if (dismissTimerRef.current) {\n clearTimeout(dismissTimerRef.current)\n dismissTimerRef.current = null\n }\n }, [])\n\n const showFlash = React.useCallback((message: string, type: FlashKind) => {\n clearDismissTimer()\n setMsg(message)\n setKind(type)\n dismissTimerRef.current = setTimeout(() => {\n dismissTimerRef.current = null\n setMsg(null)\n }, 3000)\n }, [clearDismissTimer])\n\n React.useEffect(() => {\n return () => {\n clearDismissTimer()\n }\n }, [clearDismissTimer])\n\n // Read flash from URL on any navigation change (client-side too)\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const url = new URL(window.location.href)\n const message = url.searchParams.get('flash')\n const type = normalizeFlashKind(url.searchParams.get('type'))\n if (message) {\n // Always strip the params so a spoofed link does not linger in history,\n // but only render the banner for same-origin navigations.\n const trusted = isSameOriginFlashNavigation()\n url.searchParams.delete('flash')\n url.searchParams.delete('type')\n window.history.replaceState({}, '', url.toString())\n if (trusted) {\n showFlash(message, type)\n }\n }\n }, [locationKey, showFlash])\n\n // Listen for programmatic flash events\n React.useEffect(() => {\n const handler = (e: Event) => {\n const ce = e as CustomEvent<{ message?: string; type?: FlashKind }>\n const text = ce.detail?.message\n const t = ce.detail?.type || 'info'\n if (!text) return\n showFlash(text, t)\n }\n window.addEventListener('flash', handler as EventListener)\n return () => window.removeEventListener('flash', handler as EventListener)\n }, [showFlash])\n\n const handleDismiss = React.useCallback(() => {\n clearDismissTimer()\n setMsg(null)\n }, [clearDismissTimer])\n\n if (!msg) return null\n\n return (\n <div className=\"pointer-events-none fixed left-3 right-3 top-3 z-toast sm:left-auto sm:right-4 sm:w-[380px]\">\n <div className=\"pointer-events-auto\">\n <Alert\n status={flashKindToAlertStatus[kind]}\n size=\"sm\"\n dismissible\n onDismiss={handleDismiss}\n className=\"shadow-md\"\n >\n {msg}\n </Alert>\n </div>\n </div>\n )\n}\n\nexport function FlashMessages() {\n return (\n <React.Suspense fallback={null}>\n <FlashMessagesInner />\n </React.Suspense>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Alert, type AlertStatus } from '../primitives/alert'\n\nexport type FlashKind = 'success' | 'error' | 'warning' | 'info'\n\nconst flashKindToAlertStatus: Record<FlashKind, AlertStatus> = {\n success: 'success',\n error: 'error',\n warning: 'warning',\n info: 'information',\n}\n\nfunction normalizeFlashKind(value: string | null | undefined): FlashKind {\n if (value && Object.prototype.hasOwnProperty.call(flashKindToAlertStatus, value)) {\n return value as FlashKind\n }\n return 'success'\n}\n\n// A URL-supplied flash is only trustworthy when the navigation that carried it\n// originated from the same origin (an in-app POST->GET redirect or client-side\n// navigation). Cross-origin links \u2014 the phishing/content-spoofing vector \u2014 must\n// not be allowed to render attacker-controlled copy inside an authoritative banner.\nfunction isSameOriginFlashNavigation(): boolean {\n if (typeof window === 'undefined') return false\n const referrer = document.referrer\n // No referrer (direct load, bookmark, or referrer stripped) is ambiguous, not a\n // cross-origin redirect we can attribute to an attacker page; allow it.\n if (!referrer) return true\n try {\n return new URL(referrer).origin === window.location.origin\n } catch {\n return false\n }\n}\n\n// Programmatic API to show a flash message without navigation.\n// Consumers can import { flash } and call flash('text', 'error').\nexport function flash(message: string, type: FlashKind = 'info') {\n if (typeof window === 'undefined') return\n const evt = new CustomEvent('flash', { detail: { message, type } })\n window.dispatchEvent(evt)\n}\n\n// Two hosts mount `<FlashMessages />` over the same page: `FrontendLayout`\n// wraps the entire app (backend routes included, via `AppProviders`) and\n// `AppShell` mounts its own copy inside `<main>`. Both listen to the same\n// `flash` event, so every backend toast rendered twice \u2014 two stacked fixed\n// overlays, each with its own dismiss button, so dismissing one left the other\n// behind. Both mount points are public surfaces third-party apps rely on, so\n// instead of deleting one, only the first host to mount renders; the others\n// keep their state in sync and take over if it unmounts.\nconst mountedFlashHostIds: symbol[] = []\nconst flashHostListeners = new Set<() => void>()\n\nfunction notifyFlashHostListeners() {\n for (const listener of flashHostListeners) listener()\n}\n\nfunction subscribeFlashHosts(listener: () => void): () => void {\n flashHostListeners.add(listener)\n return () => {\n flashHostListeners.delete(listener)\n }\n}\n\nfunction getPrimaryFlashHostId(): symbol | null {\n return mountedFlashHostIds[0] ?? null\n}\n\nfunction getServerFlashHostId(): symbol | null {\n return null\n}\n\nfunction useIsPrimaryFlashHost(): boolean {\n const hostIdRef = React.useRef<symbol | null>(null)\n if (hostIdRef.current === null) hostIdRef.current = Symbol('om-flash-host')\n const hostId = hostIdRef.current\n\n React.useEffect(() => {\n mountedFlashHostIds.push(hostId)\n notifyFlashHostListeners()\n return () => {\n const index = mountedFlashHostIds.indexOf(hostId)\n if (index >= 0) mountedFlashHostIds.splice(index, 1)\n notifyFlashHostListeners()\n }\n }, [hostId])\n\n return React.useSyncExternalStore(subscribeFlashHosts, getPrimaryFlashHostId, getServerFlashHostId) === hostId\n}\n\ntype HistoryMethod = History['pushState']\n\nfunction useLocationKey() {\n const [locationKey, setLocationKey] = React.useState(() => {\n if (typeof window === 'undefined') return ''\n return window.location.href\n })\n const locationKeyRef = React.useRef(locationKey)\n\n React.useEffect(() => {\n locationKeyRef.current = locationKey\n }, [locationKey])\n\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n\n let active = true\n const scheduleUpdate = (href: string) => {\n const run = () => {\n if (!active) return\n if (locationKeyRef.current === href) return\n locationKeyRef.current = href\n setLocationKey(href)\n }\n if (typeof queueMicrotask === 'function') {\n queueMicrotask(run)\n } else {\n setTimeout(run, 0)\n }\n }\n const updateLocation = () => {\n if (!active) return\n const href = window.location.href\n if (href === locationKeyRef.current) return\n scheduleUpdate(href)\n }\n\n const deferredUpdateLocation = () => {\n setTimeout(updateLocation, 0)\n }\n\n const originalPush: HistoryMethod = window.history.pushState.bind(window.history)\n const originalReplace: HistoryMethod = window.history.replaceState.bind(window.history)\n\n const pushState: HistoryMethod = (...args) => {\n originalPush(...args)\n deferredUpdateLocation()\n }\n\n const replaceState: HistoryMethod = (...args) => {\n originalReplace(...args)\n deferredUpdateLocation()\n }\n\n window.history.pushState = pushState\n window.history.replaceState = replaceState\n window.addEventListener('popstate', updateLocation)\n window.addEventListener('hashchange', updateLocation)\n updateLocation()\n\n return () => {\n active = false\n window.history.pushState = originalPush\n window.history.replaceState = originalReplace\n window.removeEventListener('popstate', updateLocation)\n window.removeEventListener('hashchange', updateLocation)\n }\n }, [])\n\n return locationKey\n}\n\nfunction FlashMessagesInner() {\n const [msg, setMsg] = React.useState<string | null>(null)\n const [kind, setKind] = React.useState<FlashKind>('info')\n const isPrimaryHost = useIsPrimaryFlashHost()\n const locationKey = useLocationKey()\n const dismissTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const clearDismissTimer = React.useCallback(() => {\n if (dismissTimerRef.current) {\n clearTimeout(dismissTimerRef.current)\n dismissTimerRef.current = null\n }\n }, [])\n\n const showFlash = React.useCallback((message: string, type: FlashKind) => {\n clearDismissTimer()\n setMsg(message)\n setKind(type)\n dismissTimerRef.current = setTimeout(() => {\n dismissTimerRef.current = null\n setMsg(null)\n }, 3000)\n }, [clearDismissTimer])\n\n React.useEffect(() => {\n return () => {\n clearDismissTimer()\n }\n }, [clearDismissTimer])\n\n // Read flash from URL on any navigation change (client-side too)\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const url = new URL(window.location.href)\n const message = url.searchParams.get('flash')\n const type = normalizeFlashKind(url.searchParams.get('type'))\n if (message) {\n // Always strip the params so a spoofed link does not linger in history,\n // but only render the banner for same-origin navigations.\n const trusted = isSameOriginFlashNavigation()\n url.searchParams.delete('flash')\n url.searchParams.delete('type')\n window.history.replaceState({}, '', url.toString())\n if (trusted) {\n showFlash(message, type)\n }\n }\n }, [locationKey, showFlash])\n\n // Listen for programmatic flash events\n React.useEffect(() => {\n const handler = (e: Event) => {\n const ce = e as CustomEvent<{ message?: string; type?: FlashKind }>\n const text = ce.detail?.message\n const t = ce.detail?.type || 'info'\n if (!text) return\n showFlash(text, t)\n }\n window.addEventListener('flash', handler as EventListener)\n return () => window.removeEventListener('flash', handler as EventListener)\n }, [showFlash])\n\n const handleDismiss = React.useCallback(() => {\n clearDismissTimer()\n setMsg(null)\n }, [clearDismissTimer])\n\n if (!msg || !isPrimaryHost) return null\n\n return (\n <div className=\"pointer-events-none fixed left-3 right-3 top-3 z-toast sm:left-auto sm:right-4 sm:w-[380px]\">\n <div className=\"pointer-events-auto\">\n <Alert\n status={flashKindToAlertStatus[kind]}\n size=\"sm\"\n dismissible\n onDismiss={handleDismiss}\n className=\"shadow-md\"\n >\n {msg}\n </Alert>\n </div>\n </div>\n )\n}\n\nexport function FlashMessages() {\n return (\n <React.Suspense fallback={null}>\n <FlashMessagesInner />\n </React.Suspense>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
ExternalLink,
|
|
52
52
|
Eye,
|
|
53
53
|
FileCheck,
|
|
54
|
+
FileClock,
|
|
54
55
|
FileMinus,
|
|
55
56
|
FilePenLine,
|
|
56
57
|
Files,
|
|
@@ -82,6 +83,7 @@ import {
|
|
|
82
83
|
LineChart,
|
|
83
84
|
Link,
|
|
84
85
|
List,
|
|
86
|
+
ListChecks,
|
|
85
87
|
ListX,
|
|
86
88
|
Loader,
|
|
87
89
|
Loader2,
|
|
@@ -95,6 +97,7 @@ import {
|
|
|
95
97
|
MessageCircle,
|
|
96
98
|
MessageSquareReply,
|
|
97
99
|
MinusCircle,
|
|
100
|
+
Monitor,
|
|
98
101
|
Notebook,
|
|
99
102
|
Package,
|
|
100
103
|
PackageCheck,
|
|
@@ -102,6 +105,7 @@ import {
|
|
|
102
105
|
PackageSearch,
|
|
103
106
|
PackageX,
|
|
104
107
|
PauseCircle,
|
|
108
|
+
PenLine,
|
|
105
109
|
Percent,
|
|
106
110
|
Phone,
|
|
107
111
|
PhoneCall,
|
|
@@ -214,6 +218,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
214
218
|
"external-link": ExternalLink,
|
|
215
219
|
"eye": Eye,
|
|
216
220
|
"file-check": FileCheck,
|
|
221
|
+
"file-clock": FileClock,
|
|
217
222
|
"file-minus": FileMinus,
|
|
218
223
|
"file-pen-line": FilePenLine,
|
|
219
224
|
"file-spreadsheet": FileSpreadsheet,
|
|
@@ -245,6 +250,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
245
250
|
"line-chart": LineChart,
|
|
246
251
|
"link": Link,
|
|
247
252
|
"list": List,
|
|
253
|
+
"list-checks": ListChecks,
|
|
248
254
|
"list-x": ListX,
|
|
249
255
|
"loader": Loader,
|
|
250
256
|
"loader-2": Loader2,
|
|
@@ -258,6 +264,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
258
264
|
"message-circle": MessageCircle,
|
|
259
265
|
"message-square-reply": MessageSquareReply,
|
|
260
266
|
"minus-circle": MinusCircle,
|
|
267
|
+
"monitor": Monitor,
|
|
261
268
|
"notebook": Notebook,
|
|
262
269
|
"package": Package,
|
|
263
270
|
"package-check": PackageCheck,
|
|
@@ -265,6 +272,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
265
272
|
"package-search": PackageSearch,
|
|
266
273
|
"package-x": PackageX,
|
|
267
274
|
"pause-circle": PauseCircle,
|
|
275
|
+
"pen-line": PenLine,
|
|
268
276
|
"percent": Percent,
|
|
269
277
|
"phone": Phone,
|
|
270
278
|
"phone-call": PhoneCall,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/icons/lucideRegistry.generated.tsx"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type { LucideIcon } from 'lucide-react';\nimport {\n Activity,\n AlarmClock,\n AlertCircle,\n AlertOctagon,\n AlertTriangle,\n Apple,\n Archive,\n AtSign,\n Award,\n BadgeCheck,\n Ban,\n Banknote,\n BarChart2,\n BarChart3,\n Bell,\n Bolt,\n Book,\n Bookmark,\n Bot,\n Box,\n Boxes,\n Briefcase,\n BriefcaseBusiness,\n Building,\n Building2,\n Calendar,\n CalendarCheck,\n CalendarClock,\n CalendarCog,\n CalendarMinus,\n CalendarOff,\n CalendarX,\n Check,\n CheckCircle,\n CheckCircle2,\n CheckSquare,\n Circle,\n ClipboardCheck,\n ClipboardList,\n Clock,\n Clock3,\n Coins,\n Copy,\n CopyX,\n CreditCard,\n Crown,\n Database,\n DollarSign,\n Download,\n ExternalLink,\n Eye,\n FileCheck,\n FileMinus,\n FilePenLine,\n Files,\n FileSpreadsheet,\n FileText,\n FilterX,\n Flag,\n Flame,\n FolderTree,\n Gauge,\n GitBranch,\n GitCompareArrows,\n GitPullRequestArrow,\n Globe,\n GraduationCap,\n Hammer,\n Hand,\n Handshake,\n Headphones,\n Heart,\n HelpCircle,\n Hourglass,\n Inbox,\n Key,\n KeyRound,\n Layers,\n Leaf,\n Lightbulb,\n LineChart,\n Link,\n List,\n ListX,\n Loader,\n Loader2,\n Lock,\n Mail,\n MailOpen,\n Map,\n MapPin,\n MapPinned,\n Megaphone,\n MessageCircle,\n MessageSquareReply,\n MinusCircle,\n Notebook,\n Package,\n PackageCheck,\n PackagePlus,\n PackageSearch,\n PackageX,\n PauseCircle,\n Percent,\n Phone,\n PhoneCall,\n PieChart,\n PlusSquare,\n Receipt,\n ReceiptText,\n RefreshCcw,\n RefreshCw,\n Repeat2,\n Reply,\n RotateCcw,\n Ruler,\n Send,\n Settings,\n Shapes,\n Share2,\n Shield,\n ShieldAlert,\n ShieldCheck,\n ShoppingBag,\n ShoppingCart,\n Shuffle,\n Sliders,\n Smartphone,\n Snowflake,\n Sparkles,\n Star,\n StickyNote,\n Store,\n Tag,\n Target,\n ThumbsUp,\n Ticket,\n Timer,\n Trash2,\n TrendingUp,\n TriangleAlert,\n Trophy,\n Truck,\n Undo,\n Undo2,\n Unlock,\n User,\n UserCheck,\n UserMinus,\n UserPlus,\n UserRound,\n Users,\n UserX,\n Wallet,\n Warehouse,\n Webhook,\n Workflow,\n Wrench,\n X,\n XCircle,\n ZapOff,\n} from 'lucide-react';\n\nexport const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {\n 'activity': Activity,\n 'alarm-clock': AlarmClock,\n 'alert-circle': AlertCircle,\n 'alert-octagon': AlertOctagon,\n 'alert-triangle': AlertTriangle,\n 'apple': Apple,\n 'archive': Archive,\n 'at-sign': AtSign,\n 'award': Award,\n 'badge-check': BadgeCheck,\n 'ban': Ban,\n 'banknote': Banknote,\n 'bar-chart-2': BarChart2,\n 'bar-chart-3': BarChart3,\n 'bell': Bell,\n 'bolt': Bolt,\n 'book': Book,\n 'bookmark': Bookmark,\n 'bot': Bot,\n 'box': Box,\n 'boxes': Boxes,\n 'briefcase': Briefcase,\n 'briefcase-business': BriefcaseBusiness,\n 'building': Building,\n 'building2': Building2,\n 'calendar': Calendar,\n 'calendar-check': CalendarCheck,\n 'calendar-clock': CalendarClock,\n 'calendar-cog': CalendarCog,\n 'calendar-minus': CalendarMinus,\n 'calendar-off': CalendarOff,\n 'calendar-x': CalendarX,\n 'check': Check,\n 'check-circle': CheckCircle,\n 'check-circle-2': CheckCircle2,\n 'check-square': CheckSquare,\n 'circle': Circle,\n 'clipboard-check': ClipboardCheck,\n 'clipboard-list': ClipboardList,\n 'clock': Clock,\n 'clock-3': Clock3,\n 'coins': Coins,\n 'copy': Copy,\n 'copy-x': CopyX,\n 'credit-card': CreditCard,\n 'crown': Crown,\n 'database': Database,\n 'dollar-sign': DollarSign,\n 'download': Download,\n 'external-link': ExternalLink,\n 'eye': Eye,\n 'file-check': FileCheck,\n 'file-minus': FileMinus,\n 'file-pen-line': FilePenLine,\n 'file-spreadsheet': FileSpreadsheet,\n 'file-text': FileText,\n 'files': Files,\n 'filter-x': FilterX,\n 'flag': Flag,\n 'flame': Flame,\n 'folder-tree': FolderTree,\n 'gauge': Gauge,\n 'git-branch': GitBranch,\n 'git-compare-arrows': GitCompareArrows,\n 'git-pull-request-arrow': GitPullRequestArrow,\n 'globe': Globe,\n 'graduation-cap': GraduationCap,\n 'hammer': Hammer,\n 'hand': Hand,\n 'handshake': Handshake,\n 'headphones': Headphones,\n 'heart': Heart,\n 'help-circle': HelpCircle,\n 'hourglass': Hourglass,\n 'inbox': Inbox,\n 'key': Key,\n 'key-round': KeyRound,\n 'layers': Layers,\n 'leaf': Leaf,\n 'lightbulb': Lightbulb,\n 'line-chart': LineChart,\n 'link': Link,\n 'list': List,\n 'list-x': ListX,\n 'loader': Loader,\n 'loader-2': Loader2,\n 'lock': Lock,\n 'mail': Mail,\n 'mail-open': MailOpen,\n 'map': Map,\n 'map-pin': MapPin,\n 'map-pinned': MapPinned,\n 'megaphone': Megaphone,\n 'message-circle': MessageCircle,\n 'message-square-reply': MessageSquareReply,\n 'minus-circle': MinusCircle,\n 'notebook': Notebook,\n 'package': Package,\n 'package-check': PackageCheck,\n 'package-plus': PackagePlus,\n 'package-search': PackageSearch,\n 'package-x': PackageX,\n 'pause-circle': PauseCircle,\n 'percent': Percent,\n 'phone': Phone,\n 'phone-call': PhoneCall,\n 'pie-chart': PieChart,\n 'plus-square': PlusSquare,\n 'receipt': Receipt,\n 'receipt-text': ReceiptText,\n 'refresh-ccw': RefreshCcw,\n 'refresh-cw': RefreshCw,\n 'repeat-2': Repeat2,\n 'reply': Reply,\n 'rotate-ccw': RotateCcw,\n 'ruler': Ruler,\n 'send': Send,\n 'settings': Settings,\n 'shapes': Shapes,\n 'share-2': Share2,\n 'shield': Shield,\n 'shield-alert': ShieldAlert,\n 'shield-check': ShieldCheck,\n 'shopping-bag': ShoppingBag,\n 'shopping-cart': ShoppingCart,\n 'shuffle': Shuffle,\n 'sliders': Sliders,\n 'smartphone': Smartphone,\n 'snowflake': Snowflake,\n 'sparkles': Sparkles,\n 'star': Star,\n 'sticky-note': StickyNote,\n 'store': Store,\n 'tag': Tag,\n 'target': Target,\n 'thumbs-up': ThumbsUp,\n 'ticket': Ticket,\n 'timer': Timer,\n 'trash-2': Trash2,\n 'trending-up': TrendingUp,\n 'triangle-alert': TriangleAlert,\n 'trophy': Trophy,\n 'truck': Truck,\n 'undo': Undo,\n 'undo-2': Undo2,\n 'unlock': Unlock,\n 'user': User,\n 'user-check': UserCheck,\n 'user-minus': UserMinus,\n 'user-plus': UserPlus,\n 'user-round': UserRound,\n 'user-x': UserX,\n 'users': Users,\n 'wallet': Wallet,\n 'warehouse': Warehouse,\n 'webhook': Webhook,\n 'workflow': Workflow,\n 'wrench': Wrench,\n 'x': X,\n 'x-circle': XCircle,\n 'zap-off': ZapOff,\n};\n"],
|
|
5
|
-
"mappings": "AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,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",
|
|
4
|
+
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type { LucideIcon } from 'lucide-react';\nimport {\n Activity,\n AlarmClock,\n AlertCircle,\n AlertOctagon,\n AlertTriangle,\n Apple,\n Archive,\n AtSign,\n Award,\n BadgeCheck,\n Ban,\n Banknote,\n BarChart2,\n BarChart3,\n Bell,\n Bolt,\n Book,\n Bookmark,\n Bot,\n Box,\n Boxes,\n Briefcase,\n BriefcaseBusiness,\n Building,\n Building2,\n Calendar,\n CalendarCheck,\n CalendarClock,\n CalendarCog,\n CalendarMinus,\n CalendarOff,\n CalendarX,\n Check,\n CheckCircle,\n CheckCircle2,\n CheckSquare,\n Circle,\n ClipboardCheck,\n ClipboardList,\n Clock,\n Clock3,\n Coins,\n Copy,\n CopyX,\n CreditCard,\n Crown,\n Database,\n DollarSign,\n Download,\n ExternalLink,\n Eye,\n FileCheck,\n FileClock,\n FileMinus,\n FilePenLine,\n Files,\n FileSpreadsheet,\n FileText,\n FilterX,\n Flag,\n Flame,\n FolderTree,\n Gauge,\n GitBranch,\n GitCompareArrows,\n GitPullRequestArrow,\n Globe,\n GraduationCap,\n Hammer,\n Hand,\n Handshake,\n Headphones,\n Heart,\n HelpCircle,\n Hourglass,\n Inbox,\n Key,\n KeyRound,\n Layers,\n Leaf,\n Lightbulb,\n LineChart,\n Link,\n List,\n ListChecks,\n ListX,\n Loader,\n Loader2,\n Lock,\n Mail,\n MailOpen,\n Map,\n MapPin,\n MapPinned,\n Megaphone,\n MessageCircle,\n MessageSquareReply,\n MinusCircle,\n Monitor,\n Notebook,\n Package,\n PackageCheck,\n PackagePlus,\n PackageSearch,\n PackageX,\n PauseCircle,\n PenLine,\n Percent,\n Phone,\n PhoneCall,\n PieChart,\n PlusSquare,\n Receipt,\n ReceiptText,\n RefreshCcw,\n RefreshCw,\n Repeat2,\n Reply,\n RotateCcw,\n Ruler,\n Send,\n Settings,\n Shapes,\n Share2,\n Shield,\n ShieldAlert,\n ShieldCheck,\n ShoppingBag,\n ShoppingCart,\n Shuffle,\n Sliders,\n Smartphone,\n Snowflake,\n Sparkles,\n Star,\n StickyNote,\n Store,\n Tag,\n Target,\n ThumbsUp,\n Ticket,\n Timer,\n Trash2,\n TrendingUp,\n TriangleAlert,\n Trophy,\n Truck,\n Undo,\n Undo2,\n Unlock,\n User,\n UserCheck,\n UserMinus,\n UserPlus,\n UserRound,\n Users,\n UserX,\n Wallet,\n Warehouse,\n Webhook,\n Workflow,\n Wrench,\n X,\n XCircle,\n ZapOff,\n} from 'lucide-react';\n\nexport const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {\n 'activity': Activity,\n 'alarm-clock': AlarmClock,\n 'alert-circle': AlertCircle,\n 'alert-octagon': AlertOctagon,\n 'alert-triangle': AlertTriangle,\n 'apple': Apple,\n 'archive': Archive,\n 'at-sign': AtSign,\n 'award': Award,\n 'badge-check': BadgeCheck,\n 'ban': Ban,\n 'banknote': Banknote,\n 'bar-chart-2': BarChart2,\n 'bar-chart-3': BarChart3,\n 'bell': Bell,\n 'bolt': Bolt,\n 'book': Book,\n 'bookmark': Bookmark,\n 'bot': Bot,\n 'box': Box,\n 'boxes': Boxes,\n 'briefcase': Briefcase,\n 'briefcase-business': BriefcaseBusiness,\n 'building': Building,\n 'building2': Building2,\n 'calendar': Calendar,\n 'calendar-check': CalendarCheck,\n 'calendar-clock': CalendarClock,\n 'calendar-cog': CalendarCog,\n 'calendar-minus': CalendarMinus,\n 'calendar-off': CalendarOff,\n 'calendar-x': CalendarX,\n 'check': Check,\n 'check-circle': CheckCircle,\n 'check-circle-2': CheckCircle2,\n 'check-square': CheckSquare,\n 'circle': Circle,\n 'clipboard-check': ClipboardCheck,\n 'clipboard-list': ClipboardList,\n 'clock': Clock,\n 'clock-3': Clock3,\n 'coins': Coins,\n 'copy': Copy,\n 'copy-x': CopyX,\n 'credit-card': CreditCard,\n 'crown': Crown,\n 'database': Database,\n 'dollar-sign': DollarSign,\n 'download': Download,\n 'external-link': ExternalLink,\n 'eye': Eye,\n 'file-check': FileCheck,\n 'file-clock': FileClock,\n 'file-minus': FileMinus,\n 'file-pen-line': FilePenLine,\n 'file-spreadsheet': FileSpreadsheet,\n 'file-text': FileText,\n 'files': Files,\n 'filter-x': FilterX,\n 'flag': Flag,\n 'flame': Flame,\n 'folder-tree': FolderTree,\n 'gauge': Gauge,\n 'git-branch': GitBranch,\n 'git-compare-arrows': GitCompareArrows,\n 'git-pull-request-arrow': GitPullRequestArrow,\n 'globe': Globe,\n 'graduation-cap': GraduationCap,\n 'hammer': Hammer,\n 'hand': Hand,\n 'handshake': Handshake,\n 'headphones': Headphones,\n 'heart': Heart,\n 'help-circle': HelpCircle,\n 'hourglass': Hourglass,\n 'inbox': Inbox,\n 'key': Key,\n 'key-round': KeyRound,\n 'layers': Layers,\n 'leaf': Leaf,\n 'lightbulb': Lightbulb,\n 'line-chart': LineChart,\n 'link': Link,\n 'list': List,\n 'list-checks': ListChecks,\n 'list-x': ListX,\n 'loader': Loader,\n 'loader-2': Loader2,\n 'lock': Lock,\n 'mail': Mail,\n 'mail-open': MailOpen,\n 'map': Map,\n 'map-pin': MapPin,\n 'map-pinned': MapPinned,\n 'megaphone': Megaphone,\n 'message-circle': MessageCircle,\n 'message-square-reply': MessageSquareReply,\n 'minus-circle': MinusCircle,\n 'monitor': Monitor,\n 'notebook': Notebook,\n 'package': Package,\n 'package-check': PackageCheck,\n 'package-plus': PackagePlus,\n 'package-search': PackageSearch,\n 'package-x': PackageX,\n 'pause-circle': PauseCircle,\n 'pen-line': PenLine,\n 'percent': Percent,\n 'phone': Phone,\n 'phone-call': PhoneCall,\n 'pie-chart': PieChart,\n 'plus-square': PlusSquare,\n 'receipt': Receipt,\n 'receipt-text': ReceiptText,\n 'refresh-ccw': RefreshCcw,\n 'refresh-cw': RefreshCw,\n 'repeat-2': Repeat2,\n 'reply': Reply,\n 'rotate-ccw': RotateCcw,\n 'ruler': Ruler,\n 'send': Send,\n 'settings': Settings,\n 'shapes': Shapes,\n 'share-2': Share2,\n 'shield': Shield,\n 'shield-alert': ShieldAlert,\n 'shield-check': ShieldCheck,\n 'shopping-bag': ShoppingBag,\n 'shopping-cart': ShoppingCart,\n 'shuffle': Shuffle,\n 'sliders': Sliders,\n 'smartphone': Smartphone,\n 'snowflake': Snowflake,\n 'sparkles': Sparkles,\n 'star': Star,\n 'sticky-note': StickyNote,\n 'store': Store,\n 'tag': Tag,\n 'target': Target,\n 'thumbs-up': ThumbsUp,\n 'ticket': Ticket,\n 'timer': Timer,\n 'trash-2': Trash2,\n 'trending-up': TrendingUp,\n 'triangle-alert': TriangleAlert,\n 'trophy': Trophy,\n 'truck': Truck,\n 'undo': Undo,\n 'undo-2': Undo2,\n 'unlock': Unlock,\n 'user': User,\n 'user-check': UserCheck,\n 'user-minus': UserMinus,\n 'user-plus': UserPlus,\n 'user-round': UserRound,\n 'user-x': UserX,\n 'users': Users,\n 'wallet': Wallet,\n 'warehouse': Warehouse,\n 'webhook': Webhook,\n 'workflow': Workflow,\n 'wrench': Wrench,\n 'x': X,\n 'x-circle': XCircle,\n 'zap-off': ZapOff,\n};\n"],
|
|
5
|
+
"mappings": "AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -46,6 +46,7 @@ function LookupSelect({
|
|
|
46
46
|
const [query, setQuery] = React.useState("");
|
|
47
47
|
const [items, setItems] = React.useState(options ?? []);
|
|
48
48
|
const [loading, setLoading] = React.useState(false);
|
|
49
|
+
const [collapsed, setCollapsed] = React.useState(false);
|
|
49
50
|
const [hasTyped, setHasTyped] = React.useState(defaultOpen);
|
|
50
51
|
const [error, setError] = React.useState(null);
|
|
51
52
|
const [fetchKey, setFetchKey] = React.useState(0);
|
|
@@ -132,6 +133,7 @@ function LookupSelect({
|
|
|
132
133
|
event.stopPropagation();
|
|
133
134
|
setQuery("");
|
|
134
135
|
setActiveIndex(-1);
|
|
136
|
+
setCollapsed(true);
|
|
135
137
|
}
|
|
136
138
|
}, [activeIndex, items, isInteractiveItem, listboxVisible, moveActiveIndex, onChange, query]);
|
|
137
139
|
React.useEffect(() => {
|
|
@@ -173,6 +175,22 @@ function LookupSelect({
|
|
|
173
175
|
if (timer) clearTimeout(timer);
|
|
174
176
|
};
|
|
175
177
|
}, [query, shouldSearch, fetchKey]);
|
|
178
|
+
const [selectedItem, setSelectedItem] = React.useState(null);
|
|
179
|
+
React.useEffect(() => {
|
|
180
|
+
if (!value) {
|
|
181
|
+
setSelectedItem(null);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const match = items.find((item) => item.id === value);
|
|
185
|
+
if (match) setSelectedItem(match);
|
|
186
|
+
}, [items, value]);
|
|
187
|
+
const collapsedSelectionLabel = React.useMemo(() => {
|
|
188
|
+
if (!value || !selectedHintLabel) return null;
|
|
189
|
+
const hinted = selectedHintLabel(value);
|
|
190
|
+
if (hinted && hinted !== value) return hinted;
|
|
191
|
+
const fetchedTitle = selectedItem && selectedItem.id === value ? selectedItem.title : null;
|
|
192
|
+
return fetchedTitle && fetchedTitle !== value ? fetchedTitle : null;
|
|
193
|
+
}, [selectedHintLabel, selectedItem, value]);
|
|
176
194
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
177
195
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3", children: [
|
|
178
196
|
/* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
|
|
@@ -185,6 +203,7 @@ function LookupSelect({
|
|
|
185
203
|
onChange: (event) => {
|
|
186
204
|
setQuery(event.target.value);
|
|
187
205
|
setHasTyped(true);
|
|
206
|
+
setCollapsed(false);
|
|
188
207
|
},
|
|
189
208
|
onKeyDown: handleInputKeyDown,
|
|
190
209
|
placeholder: resolvedSearchPlaceholder,
|
|
@@ -199,7 +218,7 @@ function LookupSelect({
|
|
|
199
218
|
] }),
|
|
200
219
|
actionSlot && !disabled ? /* @__PURE__ */ jsx("div", { className: "sm:self-start", children: actionSlot }) : null
|
|
201
220
|
] }),
|
|
202
|
-
shouldSearch ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
221
|
+
shouldSearch && !collapsed && (items.length > 0 || query.trim().length > 0) ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
203
222
|
loading || loadingProp ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
204
223
|
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
205
224
|
resolvedLoadingLabel
|
|
@@ -276,7 +295,40 @@ function LookupSelect({
|
|
|
276
295
|
]
|
|
277
296
|
}
|
|
278
297
|
) : null
|
|
279
|
-
] }) :
|
|
298
|
+
] }) : collapsedSelectionLabel ? (
|
|
299
|
+
/*
|
|
300
|
+
* The visible input is the *search box*, not a value display: once the
|
|
301
|
+
* list closes it shows its placeholder again, so a selection made and
|
|
302
|
+
* then collapsed left the control looking empty even though the form
|
|
303
|
+
* held the id. `selectedHintLabel` existed for exactly this and was
|
|
304
|
+
* never rendered.
|
|
305
|
+
*/
|
|
306
|
+
/* @__PURE__ */ jsxs(
|
|
307
|
+
"div",
|
|
308
|
+
{
|
|
309
|
+
className: "flex items-center justify-between gap-3 rounded-lg border border-input bg-muted/40 px-3 py-2",
|
|
310
|
+
"data-testid": "lookup-select-selected",
|
|
311
|
+
children: [
|
|
312
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium text-foreground", children: collapsedSelectionLabel }),
|
|
313
|
+
/* @__PURE__ */ jsxs(
|
|
314
|
+
Button,
|
|
315
|
+
{
|
|
316
|
+
type: "button",
|
|
317
|
+
variant: "ghost",
|
|
318
|
+
size: "sm",
|
|
319
|
+
className: "shrink-0 gap-1 text-sm font-normal",
|
|
320
|
+
disabled,
|
|
321
|
+
onClick: () => onChange(null),
|
|
322
|
+
children: [
|
|
323
|
+
/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
|
|
324
|
+
resolvedClearLabel
|
|
325
|
+
]
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
) : hasTyped ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: resolvedMinQueryHintLabel }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: resolvedStartTypingLabel }),
|
|
280
332
|
error ? /* @__PURE__ */ jsx("p", { className: "text-xs text-status-error-text", role: "alert", children: resolvedEmptyLabel }) : null
|
|
281
333
|
] });
|
|
282
334
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/LookupSelect.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Check, Loader2, Search, X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'LookupSelect' })\n\nexport type LookupSelectItem = {\n id: string\n title: string\n subtitle?: string | null\n badge?: string | null\n icon?: React.ReactNode\n disabled?: boolean\n rightLabel?: string | null\n description?: string | null\n}\n\ntype LookupSelectProps = {\n value: string | null\n onChange: (next: string | null) => void\n fetchItems?: (query: string) => Promise<LookupSelectItem[]>\n fetchOptions?: (query?: string) => Promise<LookupSelectItem[]>\n options?: LookupSelectItem[]\n minQuery?: number\n actionSlot?: React.ReactNode\n onReady?: (controls: { setQuery: (value: string) => void }) => void\n searchPlaceholder?: string\n placeholder?: string\n clearLabel?: string\n emptyLabel?: string\n loadingLabel?: string\n selectLabel?: string\n selectedLabel?: string\n minQueryHintLabel?: string\n startTypingLabel?: string\n selectedHintLabel?: (id: string) => string\n disabled?: boolean\n loading?: boolean\n defaultOpen?: boolean\n}\n\nexport function LookupSelect({\n value,\n onChange,\n fetchItems,\n fetchOptions,\n options,\n minQuery = 2,\n actionSlot,\n onReady,\n placeholder,\n searchPlaceholder,\n clearLabel,\n emptyLabel,\n loadingLabel,\n selectLabel,\n selectedLabel,\n minQueryHintLabel,\n startTypingLabel,\n selectedHintLabel,\n disabled = false,\n loading: loadingProp = false,\n defaultOpen = false,\n}: LookupSelectProps) {\n const t = useT()\n const resolvedSearchPlaceholder = searchPlaceholder ?? placeholder ?? t('ui.lookupSelect.searchPlaceholder', 'Search\u2026')\n const resolvedClearLabel = clearLabel ?? t('ui.lookupSelect.clearSelection', 'Clear selection')\n const resolvedEmptyLabel = emptyLabel ?? t('ui.lookupSelect.noResults', 'No results')\n const resolvedLoadingLabel = loadingLabel ?? t('ui.lookupSelect.searching', 'Searching\u2026')\n const resolvedSelectLabel = selectLabel ?? t('ui.lookupSelect.select', 'Select')\n const resolvedSelectedLabel = selectedLabel ?? t('ui.lookupSelect.selected', 'Selected')\n const resolvedStartTypingLabel = startTypingLabel ?? t('ui.lookupSelect.startTyping', 'Start typing to search.')\n const resolvedMinQueryHintLabel = minQueryHintLabel ?? t(\n 'ui.lookupSelect.minQueryHint',\n 'Type at least {minQuery} characters or paste an id to search.',\n { minQuery: String(minQuery) }\n )\n const [query, setQuery] = React.useState('')\n const [items, setItems] = React.useState<LookupSelectItem[]>(options ?? [])\n const [loading, setLoading] = React.useState(false)\n const [hasTyped, setHasTyped] = React.useState(defaultOpen)\n const [error, setError] = React.useState<string | null>(null)\n const [fetchKey, setFetchKey] = React.useState(0)\n const [activeIndex, setActiveIndex] = React.useState(-1)\n const listboxId = React.useId()\n const fetchItemsRef = React.useRef(fetchItems ?? fetchOptions)\n const setQueryRef = React.useRef(setQuery)\n const onReadyRef = React.useRef(onReady)\n const optionsWasArrayRef = React.useRef(Array.isArray(options))\n\n React.useEffect(() => {\n fetchItemsRef.current = fetchItems ?? fetchOptions\n }, [fetchItems, fetchOptions])\n\n React.useEffect(() => {\n onReadyRef.current = onReady\n }, [onReady])\n\n React.useEffect(() => {\n if (Array.isArray(options)) {\n optionsWasArrayRef.current = true\n setItems(options)\n } else if (optionsWasArrayRef.current) {\n optionsWasArrayRef.current = false\n setFetchKey((k) => k + 1)\n }\n }, [options])\n\n React.useEffect(() => {\n setQueryRef.current = setQuery\n if (onReadyRef.current) onReadyRef.current({ setQuery })\n }, [setQuery])\n\n // A set `value` always opens the list: it is the only place the selected row,\n // its checkmark and the clear control render, so collapsing over a selection\n // would hide it with no way to see or undo it. This used to also require an\n // `options` prop, which left every caller that resolves its selection through\n // `fetchItems` blind whenever minQuery kept the list shut.\n const shouldSearch = defaultOpen || query.trim().length >= minQuery || Boolean(value)\n\n React.useEffect(() => {\n setActiveIndex(-1)\n }, [items])\n\n const optionDomId = React.useCallback(\n (index: number) => `${listboxId}-option-${index}`,\n [listboxId],\n )\n\n const isInteractiveItem = React.useCallback(\n (item: LookupSelectItem) => !disabled && (!item.disabled || value === item.id),\n [disabled, value],\n )\n\n const moveActiveIndex = React.useCallback((direction: 1 | -1) => {\n setActiveIndex((current) => {\n if (!items.length) return -1\n let next = current\n for (let step = 0; step < items.length; step += 1) {\n next = (next + direction + items.length) % items.length\n if (isInteractiveItem(items[next])) return next\n }\n return current\n })\n }, [isInteractiveItem, items])\n\n React.useEffect(() => {\n if (activeIndex < 0) return\n const activeElement = typeof document !== 'undefined'\n ? document.getElementById(optionDomId(activeIndex))\n : null\n if (typeof activeElement?.scrollIntoView === 'function') {\n activeElement.scrollIntoView({ block: 'nearest' })\n }\n }, [activeIndex, optionDomId])\n\n const listboxVisible = shouldSearch && !disabled\n const handleInputKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {\n if (!listboxVisible) return\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n moveActiveIndex(1)\n return\n }\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n moveActiveIndex(-1)\n return\n }\n if (event.key === 'Enter') {\n if (activeIndex < 0 || activeIndex >= items.length) return\n const item = items[activeIndex]\n if (!isInteractiveItem(item)) return\n event.preventDefault()\n onChange(item.id)\n setActiveIndex(-1)\n return\n }\n if (event.key === 'Escape') {\n if (query.length === 0 && activeIndex < 0) return\n event.preventDefault()\n event.stopPropagation()\n setQuery('')\n setActiveIndex(-1)\n }\n }, [activeIndex, items, isInteractiveItem, listboxVisible, moveActiveIndex, onChange, query])\n React.useEffect(() => {\n if (disabled) {\n setItems(options ?? [])\n setLoading(false)\n return\n }\n let cancelled = false\n let timer: ReturnType<typeof setTimeout> | null = null\n if (!shouldSearch) {\n setItems(options ?? [])\n setLoading(false)\n setError(null)\n return () => { cancelled = true }\n }\n setLoading(true)\n setError(null)\n timer = setTimeout(() => {\n const requestId = Date.now()\n const fetcher = fetchItemsRef.current\n const loader = fetcher ?? (() => Promise.resolve(options ?? []))\n loader(query.trim())\n .then((result) => {\n if (cancelled) return\n setItems(result)\n })\n .catch((err) => {\n if (cancelled) return\n logger.error('Failed to fetch lookup items', { err })\n setError('error')\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return requestId\n }, 220)\n return () => {\n cancelled = true\n if (timer) clearTimeout(timer)\n }\n }, [query, shouldSearch, fetchKey])\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n <input\n className=\"w-full h-10 rounded-lg border border-input bg-background pl-10 pr-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground hover:border-foreground/20 focus-visible:shadow-focus focus-visible:border-brand-violet disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed\"\n value={query}\n onChange={(event) => {\n setQuery(event.target.value)\n setHasTyped(true)\n }}\n onKeyDown={handleInputKeyDown}\n placeholder={resolvedSearchPlaceholder}\n disabled={disabled}\n role=\"combobox\"\n aria-expanded={listboxVisible}\n aria-controls={listboxId}\n aria-autocomplete=\"list\"\n aria-activedescendant={activeIndex >= 0 ? optionDomId(activeIndex) : undefined}\n />\n </div>\n {actionSlot && !disabled ? <div className=\"sm:self-start\">{actionSlot}</div> : null}\n </div>\n {shouldSearch ? (\n <div className=\"space-y-2\">\n {loading || loadingProp ? (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {resolvedLoadingLabel}\n </div>\n ) : null}\n {!loading && !loadingProp && !items.length ? (\n <p className=\"text-xs text-muted-foreground\">{resolvedEmptyLabel}</p>\n ) : null}\n <div\n id={listboxId}\n role=\"listbox\"\n className=\"flex flex-col gap-1.5 max-h-80 overflow-y-auto -mx-0.5 px-0.5 py-0.5\"\n >\n {items.map((item, index) => {\n const isSelected = value === item.id\n const isInteractive = isInteractiveItem(item)\n const isActive = index === activeIndex\n return (\n <div\n key={item.id}\n id={optionDomId(index)}\n className={cn(\n 'group flex items-center gap-4 rounded-xl border p-4 transition-all duration-150 focus-visible:outline-none focus-visible:shadow-focus',\n isInteractive ? 'cursor-pointer' : 'cursor-not-allowed opacity-60',\n isSelected\n ? 'border-brand-violet bg-brand-violet/5 shadow-sm'\n : 'border-input bg-card hover:border-foreground/20 hover:bg-muted/30 hover:shadow-sm',\n isActive && !isSelected ? 'border-foreground/20 bg-muted/30 shadow-sm' : null\n )}\n role=\"option\"\n tabIndex={isInteractive ? 0 : -1}\n onClick={() => {\n if (!isInteractive) return\n onChange(item.id)\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n if (!isInteractive) return\n onChange(item.id)\n }\n }}\n aria-selected={isSelected}\n aria-disabled={isInteractive ? undefined : true}\n title={isSelected ? resolvedSelectedLabel : isInteractive ? resolvedSelectLabel : undefined}\n >\n {item.icon ? (\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden [&>svg]:size-6 [&_svg]:text-muted-foreground\">\n {item.icon}\n </div>\n ) : (\n <div className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-lg border transition-colors',\n isSelected\n ? 'border-brand-violet/40 bg-brand-violet/10 text-brand-violet'\n : 'border-input bg-muted text-muted-foreground group-hover:border-foreground/20'\n )}>\n <span className=\"text-base font-semibold uppercase\">{item.title.slice(0, 1)}</span>\n </div>\n )}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"truncate text-sm font-semibold text-foreground\">{item.title}</div>\n {item.rightLabel ? (\n <div className=\"shrink-0 text-overline font-medium uppercase tracking-wider text-muted-foreground\">\n {item.rightLabel}\n </div>\n ) : null}\n </div>\n {item.subtitle ? (\n <div className=\"text-xs text-muted-foreground truncate\">{item.subtitle}</div>\n ) : null}\n {item.description ? (\n <div className=\"text-xs text-muted-foreground/70 truncate\">{item.description}</div>\n ) : null}\n </div>\n <div className=\"flex shrink-0 items-center justify-center\">\n {isSelected ? (\n <Check className=\"size-5 text-brand-violet\" aria-hidden=\"true\" />\n ) : (\n <div className=\"size-5\" aria-hidden=\"true\" />\n )}\n </div>\n </div>\n )\n })}\n </div>\n {value && !disabled ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-fit gap-1 text-sm font-normal\"\n onClick={() => onChange(null)}\n >\n <X className=\"h-4 w-4\" />\n {resolvedClearLabel}\n </Button>\n ) : null}\n </div>\n ) : hasTyped ? (\n <p className=\"text-xs text-muted-foreground\">\n {resolvedMinQueryHintLabel}\n </p>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{resolvedStartTypingLabel}</p>\n )}\n {error ? <p className=\"text-xs text-status-error-text\" role=\"alert\">{resolvedEmptyLabel}</p> : null}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Check, Loader2, Search, X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'LookupSelect' })\n\nexport type LookupSelectItem = {\n id: string\n title: string\n subtitle?: string | null\n badge?: string | null\n icon?: React.ReactNode\n disabled?: boolean\n rightLabel?: string | null\n description?: string | null\n}\n\ntype LookupSelectProps = {\n value: string | null\n onChange: (next: string | null) => void\n fetchItems?: (query: string) => Promise<LookupSelectItem[]>\n fetchOptions?: (query?: string) => Promise<LookupSelectItem[]>\n options?: LookupSelectItem[]\n minQuery?: number\n actionSlot?: React.ReactNode\n onReady?: (controls: { setQuery: (value: string) => void }) => void\n searchPlaceholder?: string\n placeholder?: string\n clearLabel?: string\n emptyLabel?: string\n loadingLabel?: string\n selectLabel?: string\n selectedLabel?: string\n minQueryHintLabel?: string\n startTypingLabel?: string\n selectedHintLabel?: (id: string) => string\n disabled?: boolean\n loading?: boolean\n defaultOpen?: boolean\n}\n\nexport function LookupSelect({\n value,\n onChange,\n fetchItems,\n fetchOptions,\n options,\n minQuery = 2,\n actionSlot,\n onReady,\n placeholder,\n searchPlaceholder,\n clearLabel,\n emptyLabel,\n loadingLabel,\n selectLabel,\n selectedLabel,\n minQueryHintLabel,\n startTypingLabel,\n selectedHintLabel,\n disabled = false,\n loading: loadingProp = false,\n defaultOpen = false,\n}: LookupSelectProps) {\n const t = useT()\n const resolvedSearchPlaceholder = searchPlaceholder ?? placeholder ?? t('ui.lookupSelect.searchPlaceholder', 'Search\u2026')\n const resolvedClearLabel = clearLabel ?? t('ui.lookupSelect.clearSelection', 'Clear selection')\n const resolvedEmptyLabel = emptyLabel ?? t('ui.lookupSelect.noResults', 'No results')\n const resolvedLoadingLabel = loadingLabel ?? t('ui.lookupSelect.searching', 'Searching\u2026')\n const resolvedSelectLabel = selectLabel ?? t('ui.lookupSelect.select', 'Select')\n const resolvedSelectedLabel = selectedLabel ?? t('ui.lookupSelect.selected', 'Selected')\n const resolvedStartTypingLabel = startTypingLabel ?? t('ui.lookupSelect.startTyping', 'Start typing to search.')\n const resolvedMinQueryHintLabel = minQueryHintLabel ?? t(\n 'ui.lookupSelect.minQueryHint',\n 'Type at least {minQuery} characters or paste an id to search.',\n { minQuery: String(minQuery) }\n )\n const [query, setQuery] = React.useState('')\n const [items, setItems] = React.useState<LookupSelectItem[]>(options ?? [])\n const [loading, setLoading] = React.useState(false)\n // Escape collapses the list without clearing the selection. Derived state is not\n // enough here: Escape empties the query but the fetched `items` linger, so the\n // list would stay open and the collapsed summary \u2014 the branch that keeps a raw\n // record id out of the DOM (TC-EUDR-013) \u2014 would never render.\n const [collapsed, setCollapsed] = React.useState(false)\n const [hasTyped, setHasTyped] = React.useState(defaultOpen)\n const [error, setError] = React.useState<string | null>(null)\n const [fetchKey, setFetchKey] = React.useState(0)\n const [activeIndex, setActiveIndex] = React.useState(-1)\n const listboxId = React.useId()\n const fetchItemsRef = React.useRef(fetchItems ?? fetchOptions)\n const setQueryRef = React.useRef(setQuery)\n const onReadyRef = React.useRef(onReady)\n const optionsWasArrayRef = React.useRef(Array.isArray(options))\n\n React.useEffect(() => {\n fetchItemsRef.current = fetchItems ?? fetchOptions\n }, [fetchItems, fetchOptions])\n\n React.useEffect(() => {\n onReadyRef.current = onReady\n }, [onReady])\n\n React.useEffect(() => {\n if (Array.isArray(options)) {\n optionsWasArrayRef.current = true\n setItems(options)\n } else if (optionsWasArrayRef.current) {\n optionsWasArrayRef.current = false\n setFetchKey((k) => k + 1)\n }\n }, [options])\n\n React.useEffect(() => {\n setQueryRef.current = setQuery\n if (onReadyRef.current) onReadyRef.current({ setQuery })\n }, [setQuery])\n\n // A set `value` always opens the list: it is the only place the selected row,\n // its checkmark and the clear control render, so collapsing over a selection\n // would hide it with no way to see or undo it. This used to also require an\n // `options` prop, which left every caller that resolves its selection through\n // `fetchItems` blind whenever minQuery kept the list shut.\n const shouldSearch = defaultOpen || query.trim().length >= minQuery || Boolean(value)\n\n React.useEffect(() => {\n setActiveIndex(-1)\n }, [items])\n\n const optionDomId = React.useCallback(\n (index: number) => `${listboxId}-option-${index}`,\n [listboxId],\n )\n\n const isInteractiveItem = React.useCallback(\n (item: LookupSelectItem) => !disabled && (!item.disabled || value === item.id),\n [disabled, value],\n )\n\n const moveActiveIndex = React.useCallback((direction: 1 | -1) => {\n setActiveIndex((current) => {\n if (!items.length) return -1\n let next = current\n for (let step = 0; step < items.length; step += 1) {\n next = (next + direction + items.length) % items.length\n if (isInteractiveItem(items[next])) return next\n }\n return current\n })\n }, [isInteractiveItem, items])\n\n React.useEffect(() => {\n if (activeIndex < 0) return\n const activeElement = typeof document !== 'undefined'\n ? document.getElementById(optionDomId(activeIndex))\n : null\n if (typeof activeElement?.scrollIntoView === 'function') {\n activeElement.scrollIntoView({ block: 'nearest' })\n }\n }, [activeIndex, optionDomId])\n\n const listboxVisible = shouldSearch && !disabled\n const handleInputKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {\n if (!listboxVisible) return\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n moveActiveIndex(1)\n return\n }\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n moveActiveIndex(-1)\n return\n }\n if (event.key === 'Enter') {\n if (activeIndex < 0 || activeIndex >= items.length) return\n const item = items[activeIndex]\n if (!isInteractiveItem(item)) return\n event.preventDefault()\n onChange(item.id)\n setActiveIndex(-1)\n return\n }\n if (event.key === 'Escape') {\n if (query.length === 0 && activeIndex < 0) return\n event.preventDefault()\n event.stopPropagation()\n setQuery('')\n setActiveIndex(-1)\n setCollapsed(true)\n }\n }, [activeIndex, items, isInteractiveItem, listboxVisible, moveActiveIndex, onChange, query])\n React.useEffect(() => {\n if (disabled) {\n setItems(options ?? [])\n setLoading(false)\n return\n }\n let cancelled = false\n let timer: ReturnType<typeof setTimeout> | null = null\n if (!shouldSearch) {\n setItems(options ?? [])\n setLoading(false)\n setError(null)\n return () => { cancelled = true }\n }\n setLoading(true)\n setError(null)\n timer = setTimeout(() => {\n const requestId = Date.now()\n const fetcher = fetchItemsRef.current\n const loader = fetcher ?? (() => Promise.resolve(options ?? []))\n loader(query.trim())\n .then((result) => {\n if (cancelled) return\n setItems(result)\n })\n .catch((err) => {\n if (cancelled) return\n logger.error('Failed to fetch lookup items', { err })\n setError('error')\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return requestId\n }, 220)\n return () => {\n cancelled = true\n if (timer) clearTimeout(timer)\n }\n }, [query, shouldSearch, fetchKey])\n\n const [selectedItem, setSelectedItem] = React.useState<LookupSelectItem | null>(null)\n\n React.useEffect(() => {\n if (!value) {\n setSelectedItem(null)\n return\n }\n const match = items.find((item) => item.id === value)\n if (match) setSelectedItem(match)\n }, [items, value])\n\n /*\n * The collapsed summary is opt-in: only a consumer that supplies\n * `selectedHintLabel` asks this component to display the selection, so\n * consumers rendering their own selected-value label keep their layout and\n * never get a second summary. A resolver that falls back to the id\n * (`known.get(id) ?? id`) is treated as unresolved and falls through to the\n * title of the item that was actually fetched \u2014 a record id must never reach\n * the DOM as a label.\n */\n const collapsedSelectionLabel = React.useMemo(() => {\n if (!value || !selectedHintLabel) return null\n const hinted = selectedHintLabel(value)\n if (hinted && hinted !== value) return hinted\n const fetchedTitle = selectedItem && selectedItem.id === value ? selectedItem.title : null\n return fetchedTitle && fetchedTitle !== value ? fetchedTitle : null\n }, [selectedHintLabel, selectedItem, value])\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n <input\n className=\"w-full h-10 rounded-lg border border-input bg-background pl-10 pr-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground hover:border-foreground/20 focus-visible:shadow-focus focus-visible:border-brand-violet disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed\"\n value={query}\n onChange={(event) => {\n setQuery(event.target.value)\n setHasTyped(true)\n setCollapsed(false)\n }}\n onKeyDown={handleInputKeyDown}\n placeholder={resolvedSearchPlaceholder}\n disabled={disabled}\n role=\"combobox\"\n aria-expanded={listboxVisible}\n aria-controls={listboxId}\n aria-autocomplete=\"list\"\n aria-activedescendant={activeIndex >= 0 ? optionDomId(activeIndex) : undefined}\n />\n </div>\n {actionSlot && !disabled ? <div className=\"sm:self-start\">{actionSlot}</div> : null}\n </div>\n {/*\n * `shouldSearch` decides whether to FETCH; this decides whether to RENDER a\n * list. They differ for a set value whose lookup comes back empty: develop's\n * #5248 work needs the list (and its selected option) whenever items exist,\n * while the collapsed summary \u2014 which keeps a selection visible after the\n * search box reverts to its placeholder, and keeps a raw record id out of the\n * DOM (TC-EUDR-013) \u2014 is only reachable when they do not. Falling through on\n * an empty result satisfies both. A non-empty query still renders the list so\n * its spinner and empty-state are reachable while the user is searching.\n */}\n {shouldSearch && !collapsed && (items.length > 0 || query.trim().length > 0) ? (\n <div className=\"space-y-2\">\n {loading || loadingProp ? (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {resolvedLoadingLabel}\n </div>\n ) : null}\n {!loading && !loadingProp && !items.length ? (\n <p className=\"text-xs text-muted-foreground\">{resolvedEmptyLabel}</p>\n ) : null}\n <div\n id={listboxId}\n role=\"listbox\"\n className=\"flex flex-col gap-1.5 max-h-80 overflow-y-auto -mx-0.5 px-0.5 py-0.5\"\n >\n {items.map((item, index) => {\n const isSelected = value === item.id\n const isInteractive = isInteractiveItem(item)\n const isActive = index === activeIndex\n return (\n <div\n key={item.id}\n id={optionDomId(index)}\n className={cn(\n 'group flex items-center gap-4 rounded-xl border p-4 transition-all duration-150 focus-visible:outline-none focus-visible:shadow-focus',\n isInteractive ? 'cursor-pointer' : 'cursor-not-allowed opacity-60',\n isSelected\n ? 'border-brand-violet bg-brand-violet/5 shadow-sm'\n : 'border-input bg-card hover:border-foreground/20 hover:bg-muted/30 hover:shadow-sm',\n isActive && !isSelected ? 'border-foreground/20 bg-muted/30 shadow-sm' : null\n )}\n role=\"option\"\n tabIndex={isInteractive ? 0 : -1}\n onClick={() => {\n if (!isInteractive) return\n onChange(item.id)\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n if (!isInteractive) return\n onChange(item.id)\n }\n }}\n aria-selected={isSelected}\n aria-disabled={isInteractive ? undefined : true}\n title={isSelected ? resolvedSelectedLabel : isInteractive ? resolvedSelectLabel : undefined}\n >\n {item.icon ? (\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden [&>svg]:size-6 [&_svg]:text-muted-foreground\">\n {item.icon}\n </div>\n ) : (\n <div className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-lg border transition-colors',\n isSelected\n ? 'border-brand-violet/40 bg-brand-violet/10 text-brand-violet'\n : 'border-input bg-muted text-muted-foreground group-hover:border-foreground/20'\n )}>\n <span className=\"text-base font-semibold uppercase\">{item.title.slice(0, 1)}</span>\n </div>\n )}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"truncate text-sm font-semibold text-foreground\">{item.title}</div>\n {item.rightLabel ? (\n <div className=\"shrink-0 text-overline font-medium uppercase tracking-wider text-muted-foreground\">\n {item.rightLabel}\n </div>\n ) : null}\n </div>\n {item.subtitle ? (\n <div className=\"text-xs text-muted-foreground truncate\">{item.subtitle}</div>\n ) : null}\n {item.description ? (\n <div className=\"text-xs text-muted-foreground/70 truncate\">{item.description}</div>\n ) : null}\n </div>\n <div className=\"flex shrink-0 items-center justify-center\">\n {isSelected ? (\n <Check className=\"size-5 text-brand-violet\" aria-hidden=\"true\" />\n ) : (\n <div className=\"size-5\" aria-hidden=\"true\" />\n )}\n </div>\n </div>\n )\n })}\n </div>\n {value && !disabled ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-fit gap-1 text-sm font-normal\"\n onClick={() => onChange(null)}\n >\n <X className=\"h-4 w-4\" />\n {resolvedClearLabel}\n </Button>\n ) : null}\n </div>\n ) : collapsedSelectionLabel ? (\n /*\n * The visible input is the *search box*, not a value display: once the\n * list closes it shows its placeholder again, so a selection made and\n * then collapsed left the control looking empty even though the form\n * held the id. `selectedHintLabel` existed for exactly this and was\n * never rendered.\n */\n <div\n className=\"flex items-center justify-between gap-3 rounded-lg border border-input bg-muted/40 px-3 py-2\"\n data-testid=\"lookup-select-selected\"\n >\n <span className=\"truncate text-sm font-medium text-foreground\">\n {collapsedSelectionLabel}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"shrink-0 gap-1 text-sm font-normal\"\n disabled={disabled}\n onClick={() => onChange(null)}\n >\n <X className=\"h-4 w-4\" />\n {resolvedClearLabel}\n </Button>\n </div>\n ) : hasTyped ? (\n <p className=\"text-xs text-muted-foreground\">\n {resolvedMinQueryHintLabel}\n </p>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{resolvedStartTypingLabel}</p>\n )}\n {error ? <p className=\"text-xs text-status-error-text\" role=\"alert\">{resolvedEmptyLabel}</p> : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7180.1.9717fbbb43",
|
|
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.7.1-develop.
|
|
158
|
+
"@open-mercato/shared": "0.7.1-develop.7180.1.9717fbbb43",
|
|
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.7.1-develop.
|
|
165
|
+
"@open-mercato/shared": "0.7.1-develop.7180.1.9717fbbb43",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.1",
|
|
168
168
|
"@testing-library/react": "^16.3.3",
|
|
@@ -43,6 +43,54 @@ export function flash(message: string, type: FlashKind = 'info') {
|
|
|
43
43
|
window.dispatchEvent(evt)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// Two hosts mount `<FlashMessages />` over the same page: `FrontendLayout`
|
|
47
|
+
// wraps the entire app (backend routes included, via `AppProviders`) and
|
|
48
|
+
// `AppShell` mounts its own copy inside `<main>`. Both listen to the same
|
|
49
|
+
// `flash` event, so every backend toast rendered twice — two stacked fixed
|
|
50
|
+
// overlays, each with its own dismiss button, so dismissing one left the other
|
|
51
|
+
// behind. Both mount points are public surfaces third-party apps rely on, so
|
|
52
|
+
// instead of deleting one, only the first host to mount renders; the others
|
|
53
|
+
// keep their state in sync and take over if it unmounts.
|
|
54
|
+
const mountedFlashHostIds: symbol[] = []
|
|
55
|
+
const flashHostListeners = new Set<() => void>()
|
|
56
|
+
|
|
57
|
+
function notifyFlashHostListeners() {
|
|
58
|
+
for (const listener of flashHostListeners) listener()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function subscribeFlashHosts(listener: () => void): () => void {
|
|
62
|
+
flashHostListeners.add(listener)
|
|
63
|
+
return () => {
|
|
64
|
+
flashHostListeners.delete(listener)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getPrimaryFlashHostId(): symbol | null {
|
|
69
|
+
return mountedFlashHostIds[0] ?? null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getServerFlashHostId(): symbol | null {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function useIsPrimaryFlashHost(): boolean {
|
|
77
|
+
const hostIdRef = React.useRef<symbol | null>(null)
|
|
78
|
+
if (hostIdRef.current === null) hostIdRef.current = Symbol('om-flash-host')
|
|
79
|
+
const hostId = hostIdRef.current
|
|
80
|
+
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
mountedFlashHostIds.push(hostId)
|
|
83
|
+
notifyFlashHostListeners()
|
|
84
|
+
return () => {
|
|
85
|
+
const index = mountedFlashHostIds.indexOf(hostId)
|
|
86
|
+
if (index >= 0) mountedFlashHostIds.splice(index, 1)
|
|
87
|
+
notifyFlashHostListeners()
|
|
88
|
+
}
|
|
89
|
+
}, [hostId])
|
|
90
|
+
|
|
91
|
+
return React.useSyncExternalStore(subscribeFlashHosts, getPrimaryFlashHostId, getServerFlashHostId) === hostId
|
|
92
|
+
}
|
|
93
|
+
|
|
46
94
|
type HistoryMethod = History['pushState']
|
|
47
95
|
|
|
48
96
|
function useLocationKey() {
|
|
@@ -118,6 +166,7 @@ function useLocationKey() {
|
|
|
118
166
|
function FlashMessagesInner() {
|
|
119
167
|
const [msg, setMsg] = React.useState<string | null>(null)
|
|
120
168
|
const [kind, setKind] = React.useState<FlashKind>('info')
|
|
169
|
+
const isPrimaryHost = useIsPrimaryFlashHost()
|
|
121
170
|
const locationKey = useLocationKey()
|
|
122
171
|
const dismissTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
123
172
|
|
|
@@ -181,7 +230,7 @@ function FlashMessagesInner() {
|
|
|
181
230
|
setMsg(null)
|
|
182
231
|
}, [clearDismissTimer])
|
|
183
232
|
|
|
184
|
-
if (!msg) return null
|
|
233
|
+
if (!msg || !isPrimaryHost) return null
|
|
185
234
|
|
|
186
235
|
return (
|
|
187
236
|
<div className="pointer-events-none fixed left-3 right-3 top-3 z-toast sm:left-auto sm:right-4 sm:w-[380px]">
|
|
@@ -34,7 +34,7 @@ jest.mock('../injection/useInjectionDataWidgets', () => ({
|
|
|
34
34
|
}))
|
|
35
35
|
|
|
36
36
|
import * as React from 'react'
|
|
37
|
-
import { act, fireEvent, screen } from '@testing-library/react'
|
|
37
|
+
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
|
38
38
|
import { z } from 'zod'
|
|
39
39
|
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
40
40
|
import { CrudForm, type CrudField } from '../CrudForm'
|
|
@@ -54,6 +54,18 @@ function flushMicrotasks() {
|
|
|
54
54
|
})
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// CrudForm's submit path awaits the injection hooks and the schema parse before
|
|
58
|
+
// the harness records values, and the number of ticks that takes varies with
|
|
59
|
+
// machine load. Wait for the recorded result instead of a fixed microtask count.
|
|
60
|
+
async function submitAndWaitForValues(form: HTMLFormElement) {
|
|
61
|
+
await act(async () => {
|
|
62
|
+
fireEvent.submit(form)
|
|
63
|
+
})
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
expect(screen.getByTestId('submitted').textContent).not.toBe('null')
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
describe('CrudForm dot-path base fields (issue #2503)', () => {
|
|
58
70
|
beforeEach(() => {
|
|
59
71
|
triggerEventMock.mockReset()
|
|
@@ -121,11 +133,7 @@ describe('CrudForm dot-path base fields (issue #2503)', () => {
|
|
|
121
133
|
fireEvent.blur(categoryInput)
|
|
122
134
|
})
|
|
123
135
|
|
|
124
|
-
await
|
|
125
|
-
fireEvent.submit(form)
|
|
126
|
-
await Promise.resolve()
|
|
127
|
-
await Promise.resolve()
|
|
128
|
-
})
|
|
136
|
+
await submitAndWaitForValues(form)
|
|
129
137
|
|
|
130
138
|
const submitted = JSON.parse(screen.getByTestId('submitted').textContent || 'null')
|
|
131
139
|
expect(submitted).not.toBeNull()
|
|
@@ -170,11 +178,7 @@ describe('CrudForm dot-path base fields (issue #2503)', () => {
|
|
|
170
178
|
fireEvent.blur(categoryInput)
|
|
171
179
|
})
|
|
172
180
|
|
|
173
|
-
await
|
|
174
|
-
fireEvent.submit(form)
|
|
175
|
-
await Promise.resolve()
|
|
176
|
-
await Promise.resolve()
|
|
177
|
-
})
|
|
181
|
+
await submitAndWaitForValues(form)
|
|
178
182
|
|
|
179
183
|
const submitted = JSON.parse(screen.getByTestId('submitted').textContent || 'null')
|
|
180
184
|
expect(submitted).not.toBeNull()
|
|
@@ -256,11 +260,7 @@ describe('CrudForm dot-path base fields (issue #2503)', () => {
|
|
|
256
260
|
await flushMicrotasks()
|
|
257
261
|
|
|
258
262
|
const form = container.querySelector('form') as HTMLFormElement
|
|
259
|
-
await
|
|
260
|
-
fireEvent.submit(form)
|
|
261
|
-
await Promise.resolve()
|
|
262
|
-
await Promise.resolve()
|
|
263
|
-
})
|
|
263
|
+
await submitAndWaitForValues(form)
|
|
264
264
|
|
|
265
265
|
const submitted = JSON.parse(screen.getByTestId('submitted').textContent || 'null')
|
|
266
266
|
expect(submitted.metadata).toEqual({ category: 'Sales', icon: 'shopping-cart' })
|
|
@@ -293,11 +293,7 @@ describe('CrudForm dot-path base fields (issue #2503)', () => {
|
|
|
293
293
|
await flushMicrotasks()
|
|
294
294
|
|
|
295
295
|
const form = container.querySelector('form') as HTMLFormElement
|
|
296
|
-
await
|
|
297
|
-
fireEvent.submit(form)
|
|
298
|
-
await Promise.resolve()
|
|
299
|
-
await Promise.resolve()
|
|
300
|
-
})
|
|
296
|
+
await submitAndWaitForValues(form)
|
|
301
297
|
|
|
302
298
|
// The collapse step must skip prototype-polluting segments entirely, so
|
|
303
299
|
// neither a fresh object nor Object.prototype gains the injected key.
|
|
@@ -17,7 +17,7 @@ jest.mock('../confirm-dialog', () => ({
|
|
|
17
17
|
}))
|
|
18
18
|
|
|
19
19
|
import * as React from 'react'
|
|
20
|
-
import { act, fireEvent } from '@testing-library/react'
|
|
20
|
+
import { act, fireEvent, waitFor } from '@testing-library/react'
|
|
21
21
|
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
22
22
|
import { CrudForm, type CrudField } from '../CrudForm'
|
|
23
23
|
|
|
@@ -250,10 +250,9 @@ describe('CrudForm unsaved navigation guard', () => {
|
|
|
250
250
|
|
|
251
251
|
await act(async () => {
|
|
252
252
|
fireEvent.submit(form)
|
|
253
|
-
await Promise.resolve()
|
|
254
253
|
})
|
|
255
254
|
|
|
256
|
-
expect(onSubmit).toHaveBeenCalled()
|
|
255
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalled())
|
|
257
256
|
expect(confirmDialogMock).not.toHaveBeenCalled()
|
|
258
257
|
|
|
259
258
|
await act(async () => {
|
|
@@ -306,10 +305,9 @@ describe('CrudForm unsaved navigation guard', () => {
|
|
|
306
305
|
|
|
307
306
|
await act(async () => {
|
|
308
307
|
fireEvent.submit(form)
|
|
309
|
-
await Promise.resolve()
|
|
310
308
|
})
|
|
311
309
|
|
|
312
|
-
expect(onSubmit).toHaveBeenCalled()
|
|
310
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalled())
|
|
313
311
|
expect(beforeUnloadDuringSubmit).not.toBeNull()
|
|
314
312
|
expect(beforeUnloadDuringSubmit!.defaultPrevented).toBe(false)
|
|
315
313
|
expect(beforeUnloadDuringSubmit!.returnValue).toBeUndefined()
|
|
@@ -29,7 +29,7 @@ jest.mock('../injection/useInjectionDataWidgets', () => ({
|
|
|
29
29
|
}))
|
|
30
30
|
|
|
31
31
|
import * as React from 'react'
|
|
32
|
-
import { act, fireEvent, screen } from '@testing-library/react'
|
|
32
|
+
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
|
33
33
|
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
34
34
|
import { CrudForm, type CrudField } from '../CrudForm'
|
|
35
35
|
|
|
@@ -103,11 +103,11 @@ describe('CrudForm transform pipeline behavior', () => {
|
|
|
103
103
|
|
|
104
104
|
await act(async () => {
|
|
105
105
|
fireEvent.submit(form)
|
|
106
|
-
await Promise.resolve()
|
|
107
|
-
await Promise.resolve()
|
|
108
106
|
})
|
|
109
107
|
|
|
110
|
-
|
|
108
|
+
await waitFor(() =>
|
|
109
|
+
expect(screen.getByTestId('submitted')).toHaveTextContent('"title":"spaces around"'),
|
|
110
|
+
)
|
|
111
111
|
expect(input).toHaveValue(' spaces around ')
|
|
112
112
|
})
|
|
113
113
|
|
|
@@ -162,11 +162,11 @@ describe('CrudForm transform pipeline behavior', () => {
|
|
|
162
162
|
|
|
163
163
|
await act(async () => {
|
|
164
164
|
fireEvent.submit(form)
|
|
165
|
-
await Promise.resolve()
|
|
166
|
-
await Promise.resolve()
|
|
167
165
|
})
|
|
168
166
|
|
|
169
|
-
|
|
167
|
+
await waitFor(() =>
|
|
168
|
+
expect(screen.getByTestId('submitted')).toHaveTextContent('"note":"MAKE ME UPPERCASE"'),
|
|
169
|
+
)
|
|
170
170
|
expect(input).toHaveValue('MAKE ME UPPERCASE')
|
|
171
171
|
})
|
|
172
172
|
|
|
@@ -77,6 +77,54 @@ describe('FlashMessages', () => {
|
|
|
77
77
|
expect(badge?.getAttribute('data-status')).toBe('success')
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
it('renders a single toast when several hosts mount it on the same page', () => {
|
|
81
|
+
renderWithProviders(
|
|
82
|
+
<>
|
|
83
|
+
<FlashMessages />
|
|
84
|
+
<FlashMessages />
|
|
85
|
+
</>,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
act(() => {
|
|
89
|
+
flash('Project team updated.', 'success')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(screen.getAllByText('Project team updated.')).toHaveLength(1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The handover is the half of the primary-host election that nothing else
|
|
97
|
+
* exercises. Only the first host to mount renders, so if it unmounts while a
|
|
98
|
+
* toast is up — a backend page tearing its `AppShell` down on navigation, with
|
|
99
|
+
* `FrontendLayout`'s host still mounted above it — the surviving host has to
|
|
100
|
+
* take over. If it did not, the message would vanish, which is worse than the
|
|
101
|
+
* duplicate this election replaced.
|
|
102
|
+
*/
|
|
103
|
+
it('hands a displayed toast over when the primary host unmounts', () => {
|
|
104
|
+
function Hosts({ withPrimary }: { withPrimary: boolean }) {
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
{withPrimary ? <FlashMessages /> : null}
|
|
108
|
+
<FlashMessages />
|
|
109
|
+
</>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { rerender } = renderWithProviders(<Hosts withPrimary />)
|
|
114
|
+
|
|
115
|
+
act(() => {
|
|
116
|
+
flash('Project team updated.', 'success')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
expect(screen.getAllByText('Project team updated.')).toHaveLength(1)
|
|
120
|
+
|
|
121
|
+
act(() => {
|
|
122
|
+
rerender(<Hosts withPrimary={false} />)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(screen.getAllByText('Project team updated.')).toHaveLength(1)
|
|
126
|
+
})
|
|
127
|
+
|
|
80
128
|
it('auto-dismisses programmatic flashes', () => {
|
|
81
129
|
renderWithProviders(<FlashMessages />)
|
|
82
130
|
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
ExternalLink,
|
|
57
57
|
Eye,
|
|
58
58
|
FileCheck,
|
|
59
|
+
FileClock,
|
|
59
60
|
FileMinus,
|
|
60
61
|
FilePenLine,
|
|
61
62
|
Files,
|
|
@@ -87,6 +88,7 @@ import {
|
|
|
87
88
|
LineChart,
|
|
88
89
|
Link,
|
|
89
90
|
List,
|
|
91
|
+
ListChecks,
|
|
90
92
|
ListX,
|
|
91
93
|
Loader,
|
|
92
94
|
Loader2,
|
|
@@ -100,6 +102,7 @@ import {
|
|
|
100
102
|
MessageCircle,
|
|
101
103
|
MessageSquareReply,
|
|
102
104
|
MinusCircle,
|
|
105
|
+
Monitor,
|
|
103
106
|
Notebook,
|
|
104
107
|
Package,
|
|
105
108
|
PackageCheck,
|
|
@@ -107,6 +110,7 @@ import {
|
|
|
107
110
|
PackageSearch,
|
|
108
111
|
PackageX,
|
|
109
112
|
PauseCircle,
|
|
113
|
+
PenLine,
|
|
110
114
|
Percent,
|
|
111
115
|
Phone,
|
|
112
116
|
PhoneCall,
|
|
@@ -220,6 +224,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
|
220
224
|
'external-link': ExternalLink,
|
|
221
225
|
'eye': Eye,
|
|
222
226
|
'file-check': FileCheck,
|
|
227
|
+
'file-clock': FileClock,
|
|
223
228
|
'file-minus': FileMinus,
|
|
224
229
|
'file-pen-line': FilePenLine,
|
|
225
230
|
'file-spreadsheet': FileSpreadsheet,
|
|
@@ -251,6 +256,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
|
251
256
|
'line-chart': LineChart,
|
|
252
257
|
'link': Link,
|
|
253
258
|
'list': List,
|
|
259
|
+
'list-checks': ListChecks,
|
|
254
260
|
'list-x': ListX,
|
|
255
261
|
'loader': Loader,
|
|
256
262
|
'loader-2': Loader2,
|
|
@@ -264,6 +270,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
|
264
270
|
'message-circle': MessageCircle,
|
|
265
271
|
'message-square-reply': MessageSquareReply,
|
|
266
272
|
'minus-circle': MinusCircle,
|
|
273
|
+
'monitor': Monitor,
|
|
267
274
|
'notebook': Notebook,
|
|
268
275
|
'package': Package,
|
|
269
276
|
'package-check': PackageCheck,
|
|
@@ -271,6 +278,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
|
271
278
|
'package-search': PackageSearch,
|
|
272
279
|
'package-x': PackageX,
|
|
273
280
|
'pause-circle': PauseCircle,
|
|
281
|
+
'pen-line': PenLine,
|
|
274
282
|
'percent': Percent,
|
|
275
283
|
'phone': Phone,
|
|
276
284
|
'phone-call': PhoneCall,
|
|
@@ -83,6 +83,11 @@ export function LookupSelect({
|
|
|
83
83
|
const [query, setQuery] = React.useState('')
|
|
84
84
|
const [items, setItems] = React.useState<LookupSelectItem[]>(options ?? [])
|
|
85
85
|
const [loading, setLoading] = React.useState(false)
|
|
86
|
+
// Escape collapses the list without clearing the selection. Derived state is not
|
|
87
|
+
// enough here: Escape empties the query but the fetched `items` linger, so the
|
|
88
|
+
// list would stay open and the collapsed summary — the branch that keeps a raw
|
|
89
|
+
// record id out of the DOM (TC-EUDR-013) — would never render.
|
|
90
|
+
const [collapsed, setCollapsed] = React.useState(false)
|
|
86
91
|
const [hasTyped, setHasTyped] = React.useState(defaultOpen)
|
|
87
92
|
const [error, setError] = React.useState<string | null>(null)
|
|
88
93
|
const [fetchKey, setFetchKey] = React.useState(0)
|
|
@@ -187,6 +192,7 @@ export function LookupSelect({
|
|
|
187
192
|
event.stopPropagation()
|
|
188
193
|
setQuery('')
|
|
189
194
|
setActiveIndex(-1)
|
|
195
|
+
setCollapsed(true)
|
|
190
196
|
}
|
|
191
197
|
}, [activeIndex, items, isInteractiveItem, listboxVisible, moveActiveIndex, onChange, query])
|
|
192
198
|
React.useEffect(() => {
|
|
@@ -230,6 +236,34 @@ export function LookupSelect({
|
|
|
230
236
|
}
|
|
231
237
|
}, [query, shouldSearch, fetchKey])
|
|
232
238
|
|
|
239
|
+
const [selectedItem, setSelectedItem] = React.useState<LookupSelectItem | null>(null)
|
|
240
|
+
|
|
241
|
+
React.useEffect(() => {
|
|
242
|
+
if (!value) {
|
|
243
|
+
setSelectedItem(null)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
const match = items.find((item) => item.id === value)
|
|
247
|
+
if (match) setSelectedItem(match)
|
|
248
|
+
}, [items, value])
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* The collapsed summary is opt-in: only a consumer that supplies
|
|
252
|
+
* `selectedHintLabel` asks this component to display the selection, so
|
|
253
|
+
* consumers rendering their own selected-value label keep their layout and
|
|
254
|
+
* never get a second summary. A resolver that falls back to the id
|
|
255
|
+
* (`known.get(id) ?? id`) is treated as unresolved and falls through to the
|
|
256
|
+
* title of the item that was actually fetched — a record id must never reach
|
|
257
|
+
* the DOM as a label.
|
|
258
|
+
*/
|
|
259
|
+
const collapsedSelectionLabel = React.useMemo(() => {
|
|
260
|
+
if (!value || !selectedHintLabel) return null
|
|
261
|
+
const hinted = selectedHintLabel(value)
|
|
262
|
+
if (hinted && hinted !== value) return hinted
|
|
263
|
+
const fetchedTitle = selectedItem && selectedItem.id === value ? selectedItem.title : null
|
|
264
|
+
return fetchedTitle && fetchedTitle !== value ? fetchedTitle : null
|
|
265
|
+
}, [selectedHintLabel, selectedItem, value])
|
|
266
|
+
|
|
233
267
|
return (
|
|
234
268
|
<div className="space-y-3">
|
|
235
269
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
|
@@ -241,6 +275,7 @@ export function LookupSelect({
|
|
|
241
275
|
onChange={(event) => {
|
|
242
276
|
setQuery(event.target.value)
|
|
243
277
|
setHasTyped(true)
|
|
278
|
+
setCollapsed(false)
|
|
244
279
|
}}
|
|
245
280
|
onKeyDown={handleInputKeyDown}
|
|
246
281
|
placeholder={resolvedSearchPlaceholder}
|
|
@@ -254,7 +289,17 @@ export function LookupSelect({
|
|
|
254
289
|
</div>
|
|
255
290
|
{actionSlot && !disabled ? <div className="sm:self-start">{actionSlot}</div> : null}
|
|
256
291
|
</div>
|
|
257
|
-
{
|
|
292
|
+
{/*
|
|
293
|
+
* `shouldSearch` decides whether to FETCH; this decides whether to RENDER a
|
|
294
|
+
* list. They differ for a set value whose lookup comes back empty: develop's
|
|
295
|
+
* #5248 work needs the list (and its selected option) whenever items exist,
|
|
296
|
+
* while the collapsed summary — which keeps a selection visible after the
|
|
297
|
+
* search box reverts to its placeholder, and keeps a raw record id out of the
|
|
298
|
+
* DOM (TC-EUDR-013) — is only reachable when they do not. Falling through on
|
|
299
|
+
* an empty result satisfies both. A non-empty query still renders the list so
|
|
300
|
+
* its spinner and empty-state are reachable while the user is searching.
|
|
301
|
+
*/}
|
|
302
|
+
{shouldSearch && !collapsed && (items.length > 0 || query.trim().length > 0) ? (
|
|
258
303
|
<div className="space-y-2">
|
|
259
304
|
{loading || loadingProp ? (
|
|
260
305
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
@@ -357,6 +402,33 @@ export function LookupSelect({
|
|
|
357
402
|
</Button>
|
|
358
403
|
) : null}
|
|
359
404
|
</div>
|
|
405
|
+
) : collapsedSelectionLabel ? (
|
|
406
|
+
/*
|
|
407
|
+
* The visible input is the *search box*, not a value display: once the
|
|
408
|
+
* list closes it shows its placeholder again, so a selection made and
|
|
409
|
+
* then collapsed left the control looking empty even though the form
|
|
410
|
+
* held the id. `selectedHintLabel` existed for exactly this and was
|
|
411
|
+
* never rendered.
|
|
412
|
+
*/
|
|
413
|
+
<div
|
|
414
|
+
className="flex items-center justify-between gap-3 rounded-lg border border-input bg-muted/40 px-3 py-2"
|
|
415
|
+
data-testid="lookup-select-selected"
|
|
416
|
+
>
|
|
417
|
+
<span className="truncate text-sm font-medium text-foreground">
|
|
418
|
+
{collapsedSelectionLabel}
|
|
419
|
+
</span>
|
|
420
|
+
<Button
|
|
421
|
+
type="button"
|
|
422
|
+
variant="ghost"
|
|
423
|
+
size="sm"
|
|
424
|
+
className="shrink-0 gap-1 text-sm font-normal"
|
|
425
|
+
disabled={disabled}
|
|
426
|
+
onClick={() => onChange(null)}
|
|
427
|
+
>
|
|
428
|
+
<X className="h-4 w-4" />
|
|
429
|
+
{resolvedClearLabel}
|
|
430
|
+
</Button>
|
|
431
|
+
</div>
|
|
360
432
|
) : hasTyped ? (
|
|
361
433
|
<p className="text-xs text-muted-foreground">
|
|
362
434
|
{resolvedMinQueryHintLabel}
|
|
@@ -216,6 +216,98 @@ describe('LookupSelect keyboard accessibility', () => {
|
|
|
216
216
|
})
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
+
describe('LookupSelect selected value display', () => {
|
|
220
|
+
const RECORD_ID = 'd7f88312-f4b3-44b7-b03a-dc10e561cf8e'
|
|
221
|
+
|
|
222
|
+
it('shows the selected item once the list is collapsed', () => {
|
|
223
|
+
// The visible input is the search box and reverts to its placeholder, so
|
|
224
|
+
// without this the control looked empty after a selection even though the
|
|
225
|
+
// form held the id — the user could not tell what they had picked.
|
|
226
|
+
render(
|
|
227
|
+
<LookupSelect
|
|
228
|
+
value="cust-1"
|
|
229
|
+
onChange={() => {}}
|
|
230
|
+
fetchItems={async () => []}
|
|
231
|
+
selectedHintLabel={(id) => (id === 'cust-1' ? 'ExcelMed' : id)}
|
|
232
|
+
/>,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
expect(screen.getByTestId('lookup-select-selected')).toHaveTextContent('ExcelMed')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('never renders the raw id when no label resolver is given', () => {
|
|
239
|
+
const { container } = render(
|
|
240
|
+
<LookupSelect value={RECORD_ID} onChange={() => {}} fetchItems={async () => []} />,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
expect(screen.queryByTestId('lookup-select-selected')).not.toBeInTheDocument()
|
|
244
|
+
expect(container.textContent ?? '').not.toContain(RECORD_ID)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('adds no second summary when the consumer renders its own selected label', () => {
|
|
248
|
+
// Mirrors eudr's LookupSelectField: the host already prints the resolved
|
|
249
|
+
// order label above the picker, so the collapsed block would both duplicate
|
|
250
|
+
// it and expose the uuid (TC-EUDR-013).
|
|
251
|
+
const { container } = render(
|
|
252
|
+
<div>
|
|
253
|
+
<p>Order ORDER-20260820-0000</p>
|
|
254
|
+
<LookupSelect value={RECORD_ID} onChange={() => {}} fetchItems={async () => []} />
|
|
255
|
+
</div>,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
expect(screen.queryByTestId('lookup-select-selected')).not.toBeInTheDocument()
|
|
259
|
+
expect(container.textContent ?? '').not.toContain(RECORD_ID)
|
|
260
|
+
expect(screen.getAllByText(/ORDER-20260820-0000/)).toHaveLength(1)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('shows the fetched title when the resolver has not resolved the id yet', async () => {
|
|
264
|
+
// The staff CustomerPicker resolves names from a map it fills
|
|
265
|
+
// asynchronously and falls back to `id` until then — that fallback must not
|
|
266
|
+
// put a uuid on screen once the list collapses.
|
|
267
|
+
function CustomerPickerHarness() {
|
|
268
|
+
const [value, setValue] = React.useState<string | null>(null)
|
|
269
|
+
return (
|
|
270
|
+
<LookupSelect
|
|
271
|
+
value={value}
|
|
272
|
+
onChange={setValue}
|
|
273
|
+
fetchItems={async () => [{ id: RECORD_ID, title: 'ExcelMed' }]}
|
|
274
|
+
selectedHintLabel={(id) => id}
|
|
275
|
+
/>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const { container } = render(<CustomerPickerHarness />)
|
|
280
|
+
const input = getInput(container)
|
|
281
|
+
|
|
282
|
+
fireEvent.change(input, { target: { value: 'Exc' } })
|
|
283
|
+
fireEvent.click(await screen.findByRole('option'))
|
|
284
|
+
fireEvent.keyDown(input, { key: 'Escape' })
|
|
285
|
+
|
|
286
|
+
expect(screen.getByTestId('lookup-select-selected')).toHaveTextContent('ExcelMed')
|
|
287
|
+
expect(container.textContent ?? '').not.toContain(RECORD_ID)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('clears the selection from the collapsed summary', () => {
|
|
291
|
+
const onChange = jest.fn()
|
|
292
|
+
render(
|
|
293
|
+
<LookupSelect
|
|
294
|
+
value="cust-1"
|
|
295
|
+
onChange={onChange}
|
|
296
|
+
fetchItems={async () => []}
|
|
297
|
+
selectedHintLabel={() => 'ExcelMed'}
|
|
298
|
+
/>,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
fireEvent.click(screen.getByRole('button', { name: /clear/i }))
|
|
302
|
+
expect(onChange).toHaveBeenCalledWith(null)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('renders nothing selected when there is no value', () => {
|
|
306
|
+
render(<LookupSelect value={null} onChange={() => {}} fetchItems={async () => []} />)
|
|
307
|
+
expect(screen.queryByTestId('lookup-select-selected')).not.toBeInTheDocument()
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
219
311
|
// `disabled` used to gate only the search box, so a caller that locked the
|
|
220
312
|
// control still shipped a live option list: the selected card kept its click and
|
|
221
313
|
// Enter/Space handlers, "Clear selection" stayed reachable, and the action slot
|