@open-mercato/search 0.5.1-develop.2860.07af3a6a9d → 0.5.1-develop.2874.77704bccbd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js +3 -3
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +2 -2
- package/dist/modules/search/frontend/components/HybridSearchTable.js +3 -3
- package/dist/modules/search/frontend/components/HybridSearchTable.js.map +1 -1
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +25 -25
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +2 -2
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js +3 -3
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js.map +1 -1
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +32 -32
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +2 -2
- package/package.json +4 -4
- package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +3 -3
- package/src/modules/search/frontend/components/HybridSearchTable.tsx +3 -3
- package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +29 -29
- package/src/modules/search/frontend/components/sections/GlobalSearchSection.tsx +3 -3
- package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +33 -33
|
@@ -323,7 +323,7 @@ function GlobalSearchDialog({
|
|
|
323
323
|
loading && /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" })
|
|
324
324
|
] }),
|
|
325
325
|
error ? /* @__PURE__ */ jsx("p", { className: "rounded bg-destructive/10 px-3 py-2 text-sm text-destructive", children: error }) : null,
|
|
326
|
-
showVectorWarning ? /* @__PURE__ */ jsx("p", { className: "rounded bg-
|
|
326
|
+
showVectorWarning ? /* @__PURE__ */ jsx("p", { className: "rounded bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text", children: missingConfigMessage }) : null,
|
|
327
327
|
showScopeHint ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("search.scopeHint.currentOrg", "Scoped to current organization") }) : null
|
|
328
328
|
] }),
|
|
329
329
|
/* @__PURE__ */ jsxs("div", { ref: listRef, className: "max-h-96 overflow-y-auto px-2 pb-3", children: [
|
|
@@ -341,7 +341,7 @@ function GlobalSearchDialog({
|
|
|
341
341
|
onMouseEnter: () => setSelectedIndex(index),
|
|
342
342
|
className: cn(
|
|
343
343
|
"w-full rounded-lg px-4 py-3 text-left transition border",
|
|
344
|
-
isActive ? "border-primary bg-primary/10 text-foreground shadow-sm" : "border-transparent hover:border-muted-foreground/30 hover:bg-muted/
|
|
344
|
+
isActive ? "border-primary bg-primary/10 text-foreground shadow-sm" : "border-transparent hover:border-muted-foreground/30 hover:bg-muted/50",
|
|
345
345
|
!hasLink && "opacity-60"
|
|
346
346
|
),
|
|
347
347
|
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
|
|
@@ -349,7 +349,7 @@ function GlobalSearchDialog({
|
|
|
349
349
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
350
350
|
/* @__PURE__ */ jsx("span", { className: cn("font-medium text-base whitespace-normal break-all", !hasLink && "text-muted-foreground"), children: presenter?.title ?? result.recordId }),
|
|
351
351
|
/* @__PURE__ */ jsx("span", { className: "rounded-full border border-muted-foreground/30 px-2 py-0.5 text-xs text-muted-foreground", children: formatEntityId(result.entityId) }),
|
|
352
|
-
!hasLink && /* @__PURE__ */ jsx("span", { className: "rounded-full border border-
|
|
352
|
+
!hasLink && /* @__PURE__ */ jsx("span", { className: "rounded-full border border-status-warning-border bg-status-warning-bg px-2 py-0.5 text-xs text-status-warning-text", children: t("search.dialog.noLink") })
|
|
353
353
|
] }),
|
|
354
354
|
presenter?.subtitle ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground whitespace-normal break-words", children: presenter.subtitle }) : null,
|
|
355
355
|
normalizeLinks(result.links).length ? /* @__PURE__ */ jsx("div", { className: "mt-1 flex flex-wrap items-center gap-2", children: normalizeLinks(result.links).map((link) => /* @__PURE__ */ jsx(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/search/frontend/components/GlobalSearchDialog.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport {\n Search,\n Loader2,\n Zap,\n User,\n Users,\n Building,\n StickyNote,\n Briefcase,\n CheckSquare,\n FileText,\n Mail,\n Phone,\n Calendar,\n Clock,\n Star,\n Tag,\n Flag,\n Heart,\n Bookmark,\n Package,\n Truck,\n ShoppingCart,\n CreditCard,\n DollarSign,\n Target,\n Award,\n Trophy,\n Rocket,\n Lightbulb,\n MessageSquare,\n Bell,\n Settings,\n Globe,\n MapPin,\n Link,\n Folder,\n Database,\n Activity,\n} from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport type { SearchResult, SearchResultLink, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n getCurrentOrganizationScope,\n subscribeOrganizationScopeChanged,\n} from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport { parseSelectedOrganizationCookie } from '@open-mercato/core/modules/directory/utils/scopeCookies'\nimport { ForbiddenError } from '@open-mercato/ui/backend/utils/api'\nimport { fetchGlobalSearchResults } from '../utils'\n\nconst MIN_QUERY_LENGTH = 2\n\n/** Default strategies used when none are configured */\nconst DEFAULT_STRATEGIES: SearchStrategyId[] = ['fulltext', 'vector', 'tokens']\n\nfunction normalizeLinks(links?: SearchResultLink[] | null): SearchResultLink[] {\n if (!Array.isArray(links)) return []\n return links.filter((link) => typeof link?.href === 'string')\n}\n\nfunction pickPrimaryLink(result: SearchResult): string | null {\n if (result.url) return result.url\n const links = normalizeLinks(result.links)\n if (!links.length) return null\n const primary = links.find((link) => link.kind === 'primary')\n return (primary ?? links[0]).href\n}\n\nfunction hasActiveOrganizationSelection(): boolean {\n const fromEvent = getCurrentOrganizationScope().organizationId\n if (typeof fromEvent === 'string' && fromEvent.trim().length > 0) return true\n\n const cookieHeader = typeof document === 'undefined' ? null : document.cookie\n const cookieValue = parseSelectedOrganizationCookie(cookieHeader)\n if (!cookieValue) return false\n return !isAllOrganizationsSelection(cookieValue);\n}\n\nfunction humanizeSegment(segment: string): string {\n return segment\n .split(/[_-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nconst ICON_MAP: Record<string, LucideIcon> = {\n bolt: Zap,\n zap: Zap,\n user: User,\n users: Users,\n building: Building,\n 'sticky-note': StickyNote,\n briefcase: Briefcase,\n 'check-square': CheckSquare,\n 'file-text': FileText,\n mail: Mail,\n phone: Phone,\n calendar: Calendar,\n clock: Clock,\n star: Star,\n tag: Tag,\n flag: Flag,\n heart: Heart,\n bookmark: Bookmark,\n package: Package,\n truck: Truck,\n 'shopping-cart': ShoppingCart,\n 'credit-card': CreditCard,\n 'dollar-sign': DollarSign,\n target: Target,\n award: Award,\n trophy: Trophy,\n rocket: Rocket,\n lightbulb: Lightbulb,\n 'message-square': MessageSquare,\n bell: Bell,\n settings: Settings,\n globe: Globe,\n 'map-pin': MapPin,\n link: Link,\n folder: Folder,\n database: Database,\n activity: Activity,\n}\n\nfunction resolveIcon(name?: string): LucideIcon | null {\n if (!name) return null\n return ICON_MAP[name.toLowerCase()] ?? null\n}\n\nfunction formatEntityId(entityId: string): string {\n if (!entityId.includes(':')) return humanizeSegment(entityId)\n const [module, entity] = entityId.split(':')\n return `${humanizeSegment(module)} \u00B7 ${humanizeSegment(entity)}`\n}\n\nexport type GlobalSearchDialogProps = {\n /** Whether embedding provider is configured for vector search */\n embeddingConfigured: boolean\n /** Message to show when embedding is not configured */\n missingConfigMessage: string\n /** Enabled strategies from tenant configuration (optional - uses defaults if not provided) */\n enabledStrategies?: SearchStrategyId[]\n}\n\nexport function GlobalSearchDialog({\n embeddingConfigured,\n missingConfigMessage,\n enabledStrategies: propStrategies,\n}: GlobalSearchDialogProps) {\n const router = useRouter()\n const [open, setOpen] = React.useState(false)\n const [query, setQuery] = React.useState('')\n const [results, setResults] = React.useState<SearchResult[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [selectedIndex, setSelectedIndex] = React.useState(0)\n const inputRef = React.useRef<HTMLInputElement | null>(null)\n const listRef = React.useRef<HTMLDivElement | null>(null)\n const abortRef = React.useRef<AbortController | null>(null)\n const t = useT()\n const [showScopeHint, setShowScopeHint] = React.useState<boolean>(() => hasActiveOrganizationSelection())\n\n React.useEffect(() => {\n setShowScopeHint(hasActiveOrganizationSelection())\n return subscribeOrganizationScopeChanged((detail) => {\n setShowScopeHint(Boolean(detail.organizationId && detail.organizationId.trim().length > 0))\n })\n }, [])\n\n // Use configured strategies or fall back to defaults\n const enabledStrategies = React.useMemo(() => {\n if (propStrategies && propStrategies.length > 0) {\n return propStrategies\n }\n return DEFAULT_STRATEGIES\n }, [propStrategies])\n\n const resetState = React.useCallback(() => {\n setQuery('')\n setResults([])\n setError(null)\n setSelectedIndex(0)\n setLoading(false)\n }, [])\n\n React.useEffect(() => {\n if (!open) {\n resetState()\n return\n }\n const handler = (event: KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {\n event.preventDefault()\n }\n }\n window.addEventListener('keydown', handler)\n return () => window.removeEventListener('keydown', handler)\n }, [open, resetState])\n\n React.useEffect(() => {\n const shortcut = (event: KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {\n event.preventDefault()\n setOpen((prev) => !prev)\n }\n }\n window.addEventListener('keydown', shortcut)\n return () => window.removeEventListener('keydown', shortcut)\n }, [])\n\n React.useEffect(() => {\n if (!open) return\n const focusTimer = setTimeout(() => inputRef.current?.focus(), 50)\n return () => clearTimeout(focusTimer)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n\n abortRef.current?.abort()\n if (query.trim().length < MIN_QUERY_LENGTH) {\n setResults([])\n setError(null)\n setLoading(false)\n return\n }\n\n const controller = new AbortController()\n abortRef.current = controller\n setLoading(true)\n\n const handle = setTimeout(async () => {\n try {\n const data = await fetchGlobalSearchResults(query, {\n limit: 10,\n signal: controller.signal,\n })\n setResults(data.results)\n setError(data.error ?? null)\n setSelectedIndex(0)\n } catch (err: unknown) {\n if (controller.signal.aborted) return\n const abortError = err as { name?: string }\n if (abortError?.name === 'AbortError') return\n if (err instanceof ForbiddenError) {\n setError(t('search.dialog.errors.noPermission'))\n } else {\n setError(err instanceof Error ? err.message : t('search.dialog.errors.searchFailed'))\n }\n setResults([])\n } finally {\n if (!controller.signal.aborted) setLoading(false)\n }\n }, 220)\n\n return () => {\n clearTimeout(handle)\n controller.abort()\n }\n }, [open, query, enabledStrategies, t])\n\n const openResult = React.useCallback((result: SearchResult | undefined) => {\n if (!result) return\n const href = pickPrimaryLink(result)\n if (!href) return\n router.push(href)\n setOpen(false)\n }, [router])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {\n if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {\n event.preventDefault()\n openResult(results[selectedIndex])\n return\n }\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n setSelectedIndex((prev) => (prev + 1) % Math.max(results.length || 1, 1))\n return\n }\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => {\n if (!results.length) return 0\n return prev <= 0 ? results.length - 1 : prev - 1\n })\n return\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setOpen(false)\n return\n }\n if (event.key === 'Enter') {\n event.preventDefault()\n const target = results[selectedIndex]\n openResult(target)\n return\n }\n }, [results, selectedIndex, openResult])\n\n React.useEffect(() => {\n const container = listRef.current\n const active = container?.querySelector<HTMLElement>('[data-active=\"true\"]')\n if (!container || !active) return\n const { top: containerTop, bottom: containerBottom } = container.getBoundingClientRect()\n const { top: activeTop, bottom: activeBottom } = active.getBoundingClientRect()\n if (activeTop < containerTop) {\n container.scrollTop -= containerTop - activeTop\n } else if (activeBottom > containerBottom) {\n container.scrollTop += activeBottom - containerBottom\n }\n }, [selectedIndex])\n\n // Check if vector search is enabled but not configured\n const showVectorWarning = !embeddingConfigured && enabledStrategies.includes('vector') && !error\n\n // Check if selected result has a navigable link\n const selectedResult = results[selectedIndex]\n const selectedHasLink = selectedResult ? pickPrimaryLink(selectedResult) !== null : false\n\n return (\n <>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setOpen(true)} className=\"hidden sm:inline-flex items-center gap-2\">\n <Search className=\"h-4 w-4\" />\n <span>{t('search.dialog.actions.search')}</span>\n <span className=\"ml-2 rounded border px-1 text-xs text-muted-foreground\">\u2318K</span>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"sm:hidden\"\n onClick={() => setOpen(true)}\n aria-label={t('search.dialog.actions.openGlobalSearch')}\n >\n <Search className=\"h-4 w-4\" />\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"max-w-xl p-0\" aria-describedby=\"global-search-description\">\n <DialogTitle className=\"sr-only\">\n {t('search.dialog.title', 'Global Search')}\n </DialogTitle>\n <span id=\"global-search-description\" className=\"sr-only\">\n {t('search.dialog.instructions')}\n </span>\n <div className=\"flex flex-col gap-3 border-b px-4 pb-3 pt-12\">\n <div className=\"flex items-center gap-2 rounded border border-border bg-background px-3 py-2 transition-colors focus-within:border-primary\">\n <Search className=\"h-4 w-4 text-muted-foreground\" />\n <TypedInput\n ref={inputRef}\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={t('search.dialog.input.placeholder')}\n className=\"border-none px-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0\"\n autoFocus\n />\n {loading && <Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />}\n </div>\n\n {error ? (\n <p className=\"rounded bg-destructive/10 px-3 py-2 text-sm text-destructive\">{error}</p>\n ) : null}\n {showVectorWarning ? (\n <p className=\"rounded bg-amber-100 dark:bg-amber-900/20 px-3 py-2 text-sm text-amber-800 dark:text-amber-200\">{missingConfigMessage}</p>\n ) : null}\n {showScopeHint ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('search.scopeHint.currentOrg', 'Scoped to current organization')}\n </p>\n ) : null}\n </div>\n <div ref={listRef} className=\"max-h-96 overflow-y-auto px-2 pb-3\">\n {results.length === 0 && !loading && !error ? (\n <div className=\"px-4 py-6 text-sm text-muted-foreground\">\n {query.trim().length < MIN_QUERY_LENGTH\n ? t('search.dialog.empty.hint')\n : t('search.dialog.empty.none')}\n </div>\n ) : null}\n <ul className=\"flex flex-col\">\n {results.map((result, index) => {\n const presenter = result.presenter\n const isActive = index === selectedIndex\n const hasLink = pickPrimaryLink(result) !== null\n const Icon = presenter?.icon ? resolveIcon(presenter.icon) : null\n return (\n <li key={`${result.entityId}:${result.recordId}`} data-active={isActive}>\n <button\n type=\"button\"\n onClick={() => openResult(result)}\n onMouseEnter={() => setSelectedIndex(index)}\n className={cn(\n 'w-full rounded-lg px-4 py-3 text-left transition border',\n isActive\n ? 'border-primary bg-primary/10 text-foreground shadow-sm'\n : 'border-transparent hover:border-muted-foreground/30 hover:bg-muted/60',\n !hasLink && 'opacity-60'\n )}\n >\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className={cn('font-medium text-base whitespace-normal break-all', !hasLink && 'text-muted-foreground')}>{presenter?.title ?? result.recordId}</span>\n <span className=\"rounded-full border border-muted-foreground/30 px-2 py-0.5 text-xs text-muted-foreground\">\n {formatEntityId(result.entityId)}\n </span>\n {!hasLink && (\n <span className=\"rounded-full border border-amber-500/50 bg-amber-50 dark:bg-amber-900/20 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-400\">\n {t('search.dialog.noLink')}\n </span>\n )}\n </div>\n {presenter?.subtitle ? (\n <div className=\"text-sm text-muted-foreground whitespace-normal break-words\">{presenter.subtitle}</div>\n ) : null}\n {normalizeLinks(result.links).length ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n {normalizeLinks(result.links).map((link) => (\n <span\n key={`${link.href}`}\n className={cn(\n 'rounded-full border px-2 py-0.5 text-xs',\n link.kind === 'primary'\n ? 'border-primary text-primary'\n : 'border-muted-foreground/40 text-muted-foreground'\n )}\n >\n {link.label ?? link.href}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n {Icon ? (\n <div className=\"flex flex-col items-end gap-2\">\n <Icon className=\"h-5 w-5 text-muted-foreground\" />\n </div>\n ) : null}\n </div>\n </button>\n </li>\n )\n })}\n </ul>\n </div>\n <div className=\"flex items-center justify-between border-t px-4 py-3\">\n <span className=\"text-xs text-muted-foreground\">\n {selectedResult && !selectedHasLink\n ? t('search.dialog.noLinkHint')\n : t('search.dialog.shortcuts.hint')}\n </span>\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setOpen(false)}>\n {t('search.dialog.actions.cancel')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={() => openResult(results[selectedIndex])}\n disabled={!results.length || !selectedHasLink}\n >\n {t('search.dialog.actions.openSelected')}\n </Button>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\nexport default GlobalSearchDialog\nconst TypedInput = Input as React.ForwardRefExoticComponent<React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement>>\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport {\n Search,\n Loader2,\n Zap,\n User,\n Users,\n Building,\n StickyNote,\n Briefcase,\n CheckSquare,\n FileText,\n Mail,\n Phone,\n Calendar,\n Clock,\n Star,\n Tag,\n Flag,\n Heart,\n Bookmark,\n Package,\n Truck,\n ShoppingCart,\n CreditCard,\n DollarSign,\n Target,\n Award,\n Trophy,\n Rocket,\n Lightbulb,\n MessageSquare,\n Bell,\n Settings,\n Globe,\n MapPin,\n Link,\n Folder,\n Database,\n Activity,\n} from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport type { SearchResult, SearchResultLink, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n getCurrentOrganizationScope,\n subscribeOrganizationScopeChanged,\n} from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport { parseSelectedOrganizationCookie } from '@open-mercato/core/modules/directory/utils/scopeCookies'\nimport { ForbiddenError } from '@open-mercato/ui/backend/utils/api'\nimport { fetchGlobalSearchResults } from '../utils'\n\nconst MIN_QUERY_LENGTH = 2\n\n/** Default strategies used when none are configured */\nconst DEFAULT_STRATEGIES: SearchStrategyId[] = ['fulltext', 'vector', 'tokens']\n\nfunction normalizeLinks(links?: SearchResultLink[] | null): SearchResultLink[] {\n if (!Array.isArray(links)) return []\n return links.filter((link) => typeof link?.href === 'string')\n}\n\nfunction pickPrimaryLink(result: SearchResult): string | null {\n if (result.url) return result.url\n const links = normalizeLinks(result.links)\n if (!links.length) return null\n const primary = links.find((link) => link.kind === 'primary')\n return (primary ?? links[0]).href\n}\n\nfunction hasActiveOrganizationSelection(): boolean {\n const fromEvent = getCurrentOrganizationScope().organizationId\n if (typeof fromEvent === 'string' && fromEvent.trim().length > 0) return true\n\n const cookieHeader = typeof document === 'undefined' ? null : document.cookie\n const cookieValue = parseSelectedOrganizationCookie(cookieHeader)\n if (!cookieValue) return false\n return !isAllOrganizationsSelection(cookieValue);\n}\n\nfunction humanizeSegment(segment: string): string {\n return segment\n .split(/[_-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nconst ICON_MAP: Record<string, LucideIcon> = {\n bolt: Zap,\n zap: Zap,\n user: User,\n users: Users,\n building: Building,\n 'sticky-note': StickyNote,\n briefcase: Briefcase,\n 'check-square': CheckSquare,\n 'file-text': FileText,\n mail: Mail,\n phone: Phone,\n calendar: Calendar,\n clock: Clock,\n star: Star,\n tag: Tag,\n flag: Flag,\n heart: Heart,\n bookmark: Bookmark,\n package: Package,\n truck: Truck,\n 'shopping-cart': ShoppingCart,\n 'credit-card': CreditCard,\n 'dollar-sign': DollarSign,\n target: Target,\n award: Award,\n trophy: Trophy,\n rocket: Rocket,\n lightbulb: Lightbulb,\n 'message-square': MessageSquare,\n bell: Bell,\n settings: Settings,\n globe: Globe,\n 'map-pin': MapPin,\n link: Link,\n folder: Folder,\n database: Database,\n activity: Activity,\n}\n\nfunction resolveIcon(name?: string): LucideIcon | null {\n if (!name) return null\n return ICON_MAP[name.toLowerCase()] ?? null\n}\n\nfunction formatEntityId(entityId: string): string {\n if (!entityId.includes(':')) return humanizeSegment(entityId)\n const [module, entity] = entityId.split(':')\n return `${humanizeSegment(module)} \u00B7 ${humanizeSegment(entity)}`\n}\n\nexport type GlobalSearchDialogProps = {\n /** Whether embedding provider is configured for vector search */\n embeddingConfigured: boolean\n /** Message to show when embedding is not configured */\n missingConfigMessage: string\n /** Enabled strategies from tenant configuration (optional - uses defaults if not provided) */\n enabledStrategies?: SearchStrategyId[]\n}\n\nexport function GlobalSearchDialog({\n embeddingConfigured,\n missingConfigMessage,\n enabledStrategies: propStrategies,\n}: GlobalSearchDialogProps) {\n const router = useRouter()\n const [open, setOpen] = React.useState(false)\n const [query, setQuery] = React.useState('')\n const [results, setResults] = React.useState<SearchResult[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [selectedIndex, setSelectedIndex] = React.useState(0)\n const inputRef = React.useRef<HTMLInputElement | null>(null)\n const listRef = React.useRef<HTMLDivElement | null>(null)\n const abortRef = React.useRef<AbortController | null>(null)\n const t = useT()\n const [showScopeHint, setShowScopeHint] = React.useState<boolean>(() => hasActiveOrganizationSelection())\n\n React.useEffect(() => {\n setShowScopeHint(hasActiveOrganizationSelection())\n return subscribeOrganizationScopeChanged((detail) => {\n setShowScopeHint(Boolean(detail.organizationId && detail.organizationId.trim().length > 0))\n })\n }, [])\n\n // Use configured strategies or fall back to defaults\n const enabledStrategies = React.useMemo(() => {\n if (propStrategies && propStrategies.length > 0) {\n return propStrategies\n }\n return DEFAULT_STRATEGIES\n }, [propStrategies])\n\n const resetState = React.useCallback(() => {\n setQuery('')\n setResults([])\n setError(null)\n setSelectedIndex(0)\n setLoading(false)\n }, [])\n\n React.useEffect(() => {\n if (!open) {\n resetState()\n return\n }\n const handler = (event: KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {\n event.preventDefault()\n }\n }\n window.addEventListener('keydown', handler)\n return () => window.removeEventListener('keydown', handler)\n }, [open, resetState])\n\n React.useEffect(() => {\n const shortcut = (event: KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {\n event.preventDefault()\n setOpen((prev) => !prev)\n }\n }\n window.addEventListener('keydown', shortcut)\n return () => window.removeEventListener('keydown', shortcut)\n }, [])\n\n React.useEffect(() => {\n if (!open) return\n const focusTimer = setTimeout(() => inputRef.current?.focus(), 50)\n return () => clearTimeout(focusTimer)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n\n abortRef.current?.abort()\n if (query.trim().length < MIN_QUERY_LENGTH) {\n setResults([])\n setError(null)\n setLoading(false)\n return\n }\n\n const controller = new AbortController()\n abortRef.current = controller\n setLoading(true)\n\n const handle = setTimeout(async () => {\n try {\n const data = await fetchGlobalSearchResults(query, {\n limit: 10,\n signal: controller.signal,\n })\n setResults(data.results)\n setError(data.error ?? null)\n setSelectedIndex(0)\n } catch (err: unknown) {\n if (controller.signal.aborted) return\n const abortError = err as { name?: string }\n if (abortError?.name === 'AbortError') return\n if (err instanceof ForbiddenError) {\n setError(t('search.dialog.errors.noPermission'))\n } else {\n setError(err instanceof Error ? err.message : t('search.dialog.errors.searchFailed'))\n }\n setResults([])\n } finally {\n if (!controller.signal.aborted) setLoading(false)\n }\n }, 220)\n\n return () => {\n clearTimeout(handle)\n controller.abort()\n }\n }, [open, query, enabledStrategies, t])\n\n const openResult = React.useCallback((result: SearchResult | undefined) => {\n if (!result) return\n const href = pickPrimaryLink(result)\n if (!href) return\n router.push(href)\n setOpen(false)\n }, [router])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {\n if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {\n event.preventDefault()\n openResult(results[selectedIndex])\n return\n }\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n setSelectedIndex((prev) => (prev + 1) % Math.max(results.length || 1, 1))\n return\n }\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => {\n if (!results.length) return 0\n return prev <= 0 ? results.length - 1 : prev - 1\n })\n return\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setOpen(false)\n return\n }\n if (event.key === 'Enter') {\n event.preventDefault()\n const target = results[selectedIndex]\n openResult(target)\n return\n }\n }, [results, selectedIndex, openResult])\n\n React.useEffect(() => {\n const container = listRef.current\n const active = container?.querySelector<HTMLElement>('[data-active=\"true\"]')\n if (!container || !active) return\n const { top: containerTop, bottom: containerBottom } = container.getBoundingClientRect()\n const { top: activeTop, bottom: activeBottom } = active.getBoundingClientRect()\n if (activeTop < containerTop) {\n container.scrollTop -= containerTop - activeTop\n } else if (activeBottom > containerBottom) {\n container.scrollTop += activeBottom - containerBottom\n }\n }, [selectedIndex])\n\n // Check if vector search is enabled but not configured\n const showVectorWarning = !embeddingConfigured && enabledStrategies.includes('vector') && !error\n\n // Check if selected result has a navigable link\n const selectedResult = results[selectedIndex]\n const selectedHasLink = selectedResult ? pickPrimaryLink(selectedResult) !== null : false\n\n return (\n <>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setOpen(true)} className=\"hidden sm:inline-flex items-center gap-2\">\n <Search className=\"h-4 w-4\" />\n <span>{t('search.dialog.actions.search')}</span>\n <span className=\"ml-2 rounded border px-1 text-xs text-muted-foreground\">\u2318K</span>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"sm:hidden\"\n onClick={() => setOpen(true)}\n aria-label={t('search.dialog.actions.openGlobalSearch')}\n >\n <Search className=\"h-4 w-4\" />\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"max-w-xl p-0\" aria-describedby=\"global-search-description\">\n <DialogTitle className=\"sr-only\">\n {t('search.dialog.title', 'Global Search')}\n </DialogTitle>\n <span id=\"global-search-description\" className=\"sr-only\">\n {t('search.dialog.instructions')}\n </span>\n <div className=\"flex flex-col gap-3 border-b px-4 pb-3 pt-12\">\n <div className=\"flex items-center gap-2 rounded border border-border bg-background px-3 py-2 transition-colors focus-within:border-primary\">\n <Search className=\"h-4 w-4 text-muted-foreground\" />\n <TypedInput\n ref={inputRef}\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={t('search.dialog.input.placeholder')}\n className=\"border-none px-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0\"\n autoFocus\n />\n {loading && <Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />}\n </div>\n\n {error ? (\n <p className=\"rounded bg-destructive/10 px-3 py-2 text-sm text-destructive\">{error}</p>\n ) : null}\n {showVectorWarning ? (\n <p className=\"rounded bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text\">{missingConfigMessage}</p>\n ) : null}\n {showScopeHint ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('search.scopeHint.currentOrg', 'Scoped to current organization')}\n </p>\n ) : null}\n </div>\n <div ref={listRef} className=\"max-h-96 overflow-y-auto px-2 pb-3\">\n {results.length === 0 && !loading && !error ? (\n <div className=\"px-4 py-6 text-sm text-muted-foreground\">\n {query.trim().length < MIN_QUERY_LENGTH\n ? t('search.dialog.empty.hint')\n : t('search.dialog.empty.none')}\n </div>\n ) : null}\n <ul className=\"flex flex-col\">\n {results.map((result, index) => {\n const presenter = result.presenter\n const isActive = index === selectedIndex\n const hasLink = pickPrimaryLink(result) !== null\n const Icon = presenter?.icon ? resolveIcon(presenter.icon) : null\n return (\n <li key={`${result.entityId}:${result.recordId}`} data-active={isActive}>\n <button\n type=\"button\"\n onClick={() => openResult(result)}\n onMouseEnter={() => setSelectedIndex(index)}\n className={cn(\n 'w-full rounded-lg px-4 py-3 text-left transition border',\n isActive\n ? 'border-primary bg-primary/10 text-foreground shadow-sm'\n : 'border-transparent hover:border-muted-foreground/30 hover:bg-muted/50',\n !hasLink && 'opacity-60'\n )}\n >\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className={cn('font-medium text-base whitespace-normal break-all', !hasLink && 'text-muted-foreground')}>{presenter?.title ?? result.recordId}</span>\n <span className=\"rounded-full border border-muted-foreground/30 px-2 py-0.5 text-xs text-muted-foreground\">\n {formatEntityId(result.entityId)}\n </span>\n {!hasLink && (\n <span className=\"rounded-full border border-status-warning-border bg-status-warning-bg px-2 py-0.5 text-xs text-status-warning-text\">\n {t('search.dialog.noLink')}\n </span>\n )}\n </div>\n {presenter?.subtitle ? (\n <div className=\"text-sm text-muted-foreground whitespace-normal break-words\">{presenter.subtitle}</div>\n ) : null}\n {normalizeLinks(result.links).length ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n {normalizeLinks(result.links).map((link) => (\n <span\n key={`${link.href}`}\n className={cn(\n 'rounded-full border px-2 py-0.5 text-xs',\n link.kind === 'primary'\n ? 'border-primary text-primary'\n : 'border-muted-foreground/40 text-muted-foreground'\n )}\n >\n {link.label ?? link.href}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n {Icon ? (\n <div className=\"flex flex-col items-end gap-2\">\n <Icon className=\"h-5 w-5 text-muted-foreground\" />\n </div>\n ) : null}\n </div>\n </button>\n </li>\n )\n })}\n </ul>\n </div>\n <div className=\"flex items-center justify-between border-t px-4 py-3\">\n <span className=\"text-xs text-muted-foreground\">\n {selectedResult && !selectedHasLink\n ? t('search.dialog.noLinkHint')\n : t('search.dialog.shortcuts.hint')}\n </span>\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setOpen(false)}>\n {t('search.dialog.actions.cancel')}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={() => openResult(results[selectedIndex])}\n disabled={!results.length || !selectedHasLink}\n >\n {t('search.dialog.actions.openSelected')}\n </Button>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\nexport default GlobalSearchDialog\nconst TypedInput = Input as React.ForwardRefExoticComponent<React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement>>\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -83,13 +83,13 @@ function createColumns(t) {
|
|
|
83
83
|
function getStrategyColorClass(strategy) {
|
|
84
84
|
switch (strategy) {
|
|
85
85
|
case "fulltext":
|
|
86
|
-
return "bg-
|
|
86
|
+
return "bg-status-info-bg text-status-info-text";
|
|
87
87
|
case "vector":
|
|
88
88
|
return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200";
|
|
89
89
|
case "tokens":
|
|
90
|
-
return "bg-
|
|
90
|
+
return "bg-status-success-bg text-status-success-text";
|
|
91
91
|
default:
|
|
92
|
-
return "bg-
|
|
92
|
+
return "bg-status-neutral-bg text-status-neutral-text";
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
function normalizeLinks(links) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/search/frontend/components/HybridSearchTable.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport * as LucideIcons from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n getCurrentOrganizationScope,\n subscribeOrganizationScopeChanged,\n} from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport { parseSelectedOrganizationCookie } from '@open-mercato/core/modules/directory/utils/scopeCookies'\nimport { fetchHybridSearchResults } from '../utils'\n\ntype Row = {\n entityId: string\n recordId: string\n source: string\n score: number | null\n url: string | null\n presenter: SearchResult['presenter'] | null\n links: SearchResult['links'] | null\n metadata: Record<string, unknown> | null\n}\n\nconst MIN_QUERY_LENGTH = 2\nconst ALL_STRATEGIES: SearchStrategyId[] = ['fulltext', 'vector', 'tokens']\n\ntype Translator = (\n key: string,\n fallbackOrParams?: string | Record<string, string | number>,\n params?: Record<string, string | number>\n) => string\n\nfunction hasActiveOrganizationSelection(): boolean {\n const fromEvent = getCurrentOrganizationScope().organizationId\n if (typeof fromEvent === 'string' && fromEvent.trim().length > 0) return true\n\n const cookieHeader = typeof document === 'undefined' ? null : document.cookie\n const cookieValue = parseSelectedOrganizationCookie(cookieHeader)\n if (!cookieValue) return false\n return !isAllOrganizationsSelection(cookieValue);\n}\n\nfunction createColumns(t: Translator): ColumnDef<Row>[] {\n return [\n {\n id: 'title',\n header: () => t('search.table.columns.result', 'Result'),\n cell: ({ row }) => {\n const item = row.original\n const title = resolveRowTitle(item)\n const iconName = item.presenter?.icon\n const Icon = iconName ? resolveIcon(iconName) : null\n const typeLabel = formatEntityId(item.entityId)\n const snapshot = item.presenter?.subtitle ?? extractSnapshot(item.metadata)\n const links = normalizeLinks(item.links)\n return (\n <div className=\"flex flex-col\">\n <div className=\"flex items-start gap-3\">\n {Icon ? <Icon className=\"mt-0.5 h-5 w-5 text-muted-foreground\" /> : null}\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium whitespace-normal break-all\">{title}</span>\n <span className=\"rounded border border-muted-foreground/40 px-2 py-0.5 text-xs text-muted-foreground\">\n {typeLabel}\n </span>\n </div>\n {snapshot ? (\n <span className=\"text-sm text-muted-foreground whitespace-normal break-words\">{snapshot}</span>\n ) : null}\n {links.length ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n {links.map((link) => (\n <span\n key={`${item.entityId}:${item.recordId}:${link.href}`}\n className={cn(\n 'rounded-full border px-2 py-0.5 text-xs',\n link.kind === 'primary'\n ? 'border-primary text-primary'\n : 'border-muted-foreground/40 text-muted-foreground'\n )}\n >\n {link.label ?? link.href}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n )\n },\n meta: { priority: 1 },\n },\n {\n id: 'source',\n header: () => t('search.table.columns.source', 'Source'),\n cell: ({ row }) => {\n const source = row.original.source\n const colorClass = getStrategyColorClass(source)\n return (\n <span className={cn('rounded px-2 py-0.5 text-xs font-medium', colorClass)}>\n {source}\n </span>\n )\n },\n meta: { priority: 2 },\n },\n {\n id: 'score',\n header: () => t('search.table.columns.score', 'Score'),\n cell: ({ row }) => <span>{row.original.score != null ? row.original.score.toFixed(2) : '\u2014'}</span>,\n meta: { priority: 2 },\n },\n ]\n}\n\nfunction getStrategyColorClass(strategy: string): string {\n switch (strategy) {\n case 'fulltext':\n return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'\n case 'vector':\n return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'\n case 'tokens':\n return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'\n default:\n return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'\n }\n}\n\nfunction normalizeLinks(links?: Row['links']): { href: string; label?: string; kind?: string }[] {\n if (!Array.isArray(links)) return []\n return links.filter((link) => typeof link?.href === 'string') as Array<{ href: string; label?: string; kind?: string }>\n}\n\nfunction toPascalCase(input: string): string {\n return input\n .split(/[-_ ]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('')\n}\n\nfunction resolveIcon(name?: string): LucideIcon | null {\n if (!name) return null\n const key = toPascalCase(name)\n const candidate = (LucideIcons as Record<string, unknown>)[key]\n if (typeof candidate === 'function') {\n return candidate as LucideIcon\n }\n return null\n}\n\nfunction humanizeSegment(segment: string): string {\n return segment\n .split(/[_-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nfunction formatEntityId(entityId: string): string {\n if (!entityId.includes(':')) return humanizeSegment(entityId)\n const [module, entity] = entityId.split(':')\n const moduleLabel = humanizeSegment(module)\n const entityLabel = humanizeSegment(entity)\n return `${moduleLabel} \u00B7 ${entityLabel}`\n}\n\nfunction resolveRowTitle(row: Row): string {\n const presenterTitle = row.presenter?.title\n if (typeof presenterTitle === 'string') {\n const trimmed = presenterTitle.trim()\n if (trimmed.length) return trimmed\n }\n return row.recordId\n}\n\nfunction extractSnapshot(metadata: Record<string, unknown> | null): string | null {\n if (!metadata) return null\n const candidateKeys = ['snapshot', 'summary', 'description', 'body', 'content', 'note']\n for (const key of candidateKeys) {\n const value = metadata[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length) return trimmed\n }\n }\n return null\n}\n\nfunction pickPrimaryLink(row: Row): string | null {\n if (row.url) return row.url\n const links = normalizeLinks(row.links)\n if (!links.length) return null\n const primary = links.find((link) => link.kind === 'primary')\n return (primary ?? links[0]).href\n}\n\nfunction normalizeErrorMessage(input: unknown, fallback?: string): string | null {\n const fallbackMessage = typeof fallback === 'string' && fallback.trim().length ? fallback.trim() : null\n let message: string | null = null\n if (typeof input === 'string') {\n message = input\n } else if (input instanceof Error && typeof input.message === 'string') {\n message = input.message\n }\n if (message) {\n const trimmed = message.trim()\n if (trimmed.length) {\n const sanitized = trimmed.replace(/^\\[[^\\]]+\\]\\s*/, '').trim()\n if (sanitized.length) return sanitized\n }\n }\n return fallbackMessage\n}\n\ntype HybridSearchTableProps = {\n /** Show strategy selector checkboxes (default: false - hidden from regular users) */\n showStrategySelector?: boolean\n /** Show source column in results (default: false - hidden from regular users) */\n showSourceColumn?: boolean\n}\n\nexport function HybridSearchTable({\n showStrategySelector = false,\n showSourceColumn = false,\n}: HybridSearchTableProps = {}) {\n const router = useRouter()\n const t = useT()\n const [showScopeHint, setShowScopeHint] = React.useState<boolean>(() => hasActiveOrganizationSelection())\n const [searchValue, setSearchValue] = React.useState('')\n const [rows, setRows] = React.useState<Row[]>([])\n const [page, setPage] = React.useState(1)\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [timing, setTiming] = React.useState<number | null>(null)\n const [strategiesUsed, setStrategiesUsed] = React.useState<string[]>([])\n const [enabledStrategies, setEnabledStrategies] = React.useState<Set<SearchStrategyId>>(\n new Set(ALL_STRATEGIES)\n )\n const debounceRef = React.useRef<number | null>(null)\n const abortRef = React.useRef<AbortController | null>(null)\n const columns = React.useMemo(() => {\n const allColumns = createColumns(t)\n if (!showSourceColumn) {\n return allColumns.filter((col) => col.id !== 'source')\n }\n return allColumns\n }, [t, showSourceColumn])\n\n React.useEffect(() => {\n setShowScopeHint(hasActiveOrganizationSelection())\n return subscribeOrganizationScopeChanged((detail) => {\n setShowScopeHint(Boolean(detail.organizationId && detail.organizationId.trim().length > 0))\n })\n }, [])\n\n const toggleStrategy = React.useCallback((strategy: SearchStrategyId) => {\n setEnabledStrategies((prev) => {\n const next = new Set(prev)\n if (next.has(strategy)) {\n next.delete(strategy)\n } else {\n next.add(strategy)\n }\n return next\n })\n }, [])\n\n const openRow = React.useCallback(\n (row: Row) => {\n const href = pickPrimaryLink(row)\n if (!href) return\n router.push(href)\n },\n [router]\n )\n\n React.useEffect(() => {\n const trimmed = searchValue.trim()\n abortRef.current?.abort()\n if (debounceRef.current) {\n window.clearTimeout(debounceRef.current)\n debounceRef.current = null\n }\n\n if (trimmed.length < MIN_QUERY_LENGTH) {\n setRows([])\n setTiming(null)\n setStrategiesUsed([])\n setError(null)\n setLoading(false)\n return\n }\n\n if (enabledStrategies.size === 0) {\n setRows([])\n setTiming(null)\n setStrategiesUsed([])\n setError(t('search.table.errors.noSources', 'Select at least one search source'))\n setLoading(false)\n return\n }\n\n const controller = new AbortController()\n abortRef.current = controller\n setLoading(true)\n\n debounceRef.current = window.setTimeout(async () => {\n try {\n const data = await fetchHybridSearchResults(trimmed, {\n limit: 50,\n strategies: Array.from(enabledStrategies),\n signal: controller.signal,\n })\n const mapped = data.results.map<Row>((item) => ({\n entityId: item.entityId,\n recordId: item.recordId,\n source: item.source,\n score: typeof item.score === 'number' ? item.score : null,\n url: item.url ?? null,\n presenter: item.presenter ?? null,\n links: item.links ?? null,\n metadata: (item.metadata as Record<string, unknown> | null) ?? null,\n }))\n setRows(mapped)\n setTiming(data.timing)\n setStrategiesUsed(data.strategiesUsed)\n const message = data.error ? normalizeErrorMessage(data.error, t('search.table.errors.searchFailed', 'Search failed')) : null\n setError(message ?? null)\n setPage(1)\n } catch (err: unknown) {\n if (controller.signal.aborted) return\n if ((err as { name?: string })?.name === 'AbortError') return\n setError(normalizeErrorMessage(err, t('search.table.errors.searchFailed', 'Search failed')))\n setRows([])\n setTiming(null)\n setStrategiesUsed([])\n } finally {\n if (!controller.signal.aborted) setLoading(false)\n }\n }, 250)\n\n return () => {\n controller.abort()\n if (debounceRef.current) window.clearTimeout(debounceRef.current)\n }\n }, [searchValue, enabledStrategies, t])\n\n React.useEffect(() => {\n if (!error) return\n flash(error, 'error')\n }, [error])\n\n return (\n <div className=\"flex w-full flex-col gap-4\">\n {/* Source Selector - only shown when showStrategySelector is true */}\n {showStrategySelector && (\n <div className=\"flex flex-wrap items-center gap-4 rounded-lg border bg-muted/50 p-3\">\n <span className=\"text-sm font-medium text-muted-foreground\">\n {t('search.table.sources', 'Sources:')}\n </span>\n {ALL_STRATEGIES.map((strategy) => (\n <label key={strategy} className=\"flex cursor-pointer items-center gap-2\">\n <input\n type=\"checkbox\"\n className=\"size-4 rounded border-gray-300\"\n checked={enabledStrategies.has(strategy)}\n onChange={() => toggleStrategy(strategy)}\n />\n <span\n className={cn(\n 'rounded px-2 py-0.5 text-xs font-medium',\n getStrategyColorClass(strategy)\n )}\n >\n {strategy}\n </span>\n </label>\n ))}\n </div>\n )}\n\n {/* Stats Bar */}\n {timing !== null && rows.length > 0 && (\n <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n <span>{rows.length} {t('search.table.stats.results', 'results')}</span>\n <span>{timing}ms</span>\n {/* Only show sources when strategy selector is visible */}\n {showStrategySelector && strategiesUsed.length > 0 && (\n <span>\n {t('search.table.stats.sources', 'Sources:')} {strategiesUsed.join(', ')}\n </span>\n )}\n </div>\n )}\n\n {/* Error Alert */}\n {error ? (\n <div\n role=\"alert\"\n className=\"w-full rounded-md border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\"\n >\n {error}\n </div>\n ) : null}\n\n {showScopeHint ? (\n <div className=\"text-xs text-muted-foreground\">\n {t('search.scopeHint.currentOrg', 'Scoped to current organization')}\n </div>\n ) : null}\n\n {/* Data Table */}\n <DataTable<Row>\n title={t('search.table.title', 'Search')}\n columns={columns}\n data={rows}\n searchValue={searchValue}\n onSearchChange={(value) => {\n setSearchValue(value)\n setPage(1)\n }}\n searchPlaceholder={t('search.table.searchPlaceholder', 'Search across all strategies...')}\n isLoading={loading}\n pagination={{ page, pageSize: rows.length || 1, total: rows.length, totalPages: 1, onPageChange: setPage }}\n onRowClick={(row) => openRow(row)}\n rowActions={(row) => {\n const primaryHref = pickPrimaryLink(row)\n if (!primaryHref) return null\n return <RowActions items={[{ id: 'open', label: t('search.table.actions.open', 'Open'), href: primaryHref }]} />\n }}\n embedded\n />\n </div>\n )\n}\n\nexport default HybridSearchTable\n"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport * as LucideIcons from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n getCurrentOrganizationScope,\n subscribeOrganizationScopeChanged,\n} from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport { parseSelectedOrganizationCookie } from '@open-mercato/core/modules/directory/utils/scopeCookies'\nimport { fetchHybridSearchResults } from '../utils'\n\ntype Row = {\n entityId: string\n recordId: string\n source: string\n score: number | null\n url: string | null\n presenter: SearchResult['presenter'] | null\n links: SearchResult['links'] | null\n metadata: Record<string, unknown> | null\n}\n\nconst MIN_QUERY_LENGTH = 2\nconst ALL_STRATEGIES: SearchStrategyId[] = ['fulltext', 'vector', 'tokens']\n\ntype Translator = (\n key: string,\n fallbackOrParams?: string | Record<string, string | number>,\n params?: Record<string, string | number>\n) => string\n\nfunction hasActiveOrganizationSelection(): boolean {\n const fromEvent = getCurrentOrganizationScope().organizationId\n if (typeof fromEvent === 'string' && fromEvent.trim().length > 0) return true\n\n const cookieHeader = typeof document === 'undefined' ? null : document.cookie\n const cookieValue = parseSelectedOrganizationCookie(cookieHeader)\n if (!cookieValue) return false\n return !isAllOrganizationsSelection(cookieValue);\n}\n\nfunction createColumns(t: Translator): ColumnDef<Row>[] {\n return [\n {\n id: 'title',\n header: () => t('search.table.columns.result', 'Result'),\n cell: ({ row }) => {\n const item = row.original\n const title = resolveRowTitle(item)\n const iconName = item.presenter?.icon\n const Icon = iconName ? resolveIcon(iconName) : null\n const typeLabel = formatEntityId(item.entityId)\n const snapshot = item.presenter?.subtitle ?? extractSnapshot(item.metadata)\n const links = normalizeLinks(item.links)\n return (\n <div className=\"flex flex-col\">\n <div className=\"flex items-start gap-3\">\n {Icon ? <Icon className=\"mt-0.5 h-5 w-5 text-muted-foreground\" /> : null}\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium whitespace-normal break-all\">{title}</span>\n <span className=\"rounded border border-muted-foreground/40 px-2 py-0.5 text-xs text-muted-foreground\">\n {typeLabel}\n </span>\n </div>\n {snapshot ? (\n <span className=\"text-sm text-muted-foreground whitespace-normal break-words\">{snapshot}</span>\n ) : null}\n {links.length ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n {links.map((link) => (\n <span\n key={`${item.entityId}:${item.recordId}:${link.href}`}\n className={cn(\n 'rounded-full border px-2 py-0.5 text-xs',\n link.kind === 'primary'\n ? 'border-primary text-primary'\n : 'border-muted-foreground/40 text-muted-foreground'\n )}\n >\n {link.label ?? link.href}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n )\n },\n meta: { priority: 1 },\n },\n {\n id: 'source',\n header: () => t('search.table.columns.source', 'Source'),\n cell: ({ row }) => {\n const source = row.original.source\n const colorClass = getStrategyColorClass(source)\n return (\n <span className={cn('rounded px-2 py-0.5 text-xs font-medium', colorClass)}>\n {source}\n </span>\n )\n },\n meta: { priority: 2 },\n },\n {\n id: 'score',\n header: () => t('search.table.columns.score', 'Score'),\n cell: ({ row }) => <span>{row.original.score != null ? row.original.score.toFixed(2) : '\u2014'}</span>,\n meta: { priority: 2 },\n },\n ]\n}\n\nfunction getStrategyColorClass(strategy: string): string {\n switch (strategy) {\n case 'fulltext':\n return 'bg-status-info-bg text-status-info-text'\n case 'vector':\n return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'\n case 'tokens':\n return 'bg-status-success-bg text-status-success-text'\n default:\n return 'bg-status-neutral-bg text-status-neutral-text'\n }\n}\n\nfunction normalizeLinks(links?: Row['links']): { href: string; label?: string; kind?: string }[] {\n if (!Array.isArray(links)) return []\n return links.filter((link) => typeof link?.href === 'string') as Array<{ href: string; label?: string; kind?: string }>\n}\n\nfunction toPascalCase(input: string): string {\n return input\n .split(/[-_ ]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('')\n}\n\nfunction resolveIcon(name?: string): LucideIcon | null {\n if (!name) return null\n const key = toPascalCase(name)\n const candidate = (LucideIcons as Record<string, unknown>)[key]\n if (typeof candidate === 'function') {\n return candidate as LucideIcon\n }\n return null\n}\n\nfunction humanizeSegment(segment: string): string {\n return segment\n .split(/[_-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nfunction formatEntityId(entityId: string): string {\n if (!entityId.includes(':')) return humanizeSegment(entityId)\n const [module, entity] = entityId.split(':')\n const moduleLabel = humanizeSegment(module)\n const entityLabel = humanizeSegment(entity)\n return `${moduleLabel} \u00B7 ${entityLabel}`\n}\n\nfunction resolveRowTitle(row: Row): string {\n const presenterTitle = row.presenter?.title\n if (typeof presenterTitle === 'string') {\n const trimmed = presenterTitle.trim()\n if (trimmed.length) return trimmed\n }\n return row.recordId\n}\n\nfunction extractSnapshot(metadata: Record<string, unknown> | null): string | null {\n if (!metadata) return null\n const candidateKeys = ['snapshot', 'summary', 'description', 'body', 'content', 'note']\n for (const key of candidateKeys) {\n const value = metadata[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length) return trimmed\n }\n }\n return null\n}\n\nfunction pickPrimaryLink(row: Row): string | null {\n if (row.url) return row.url\n const links = normalizeLinks(row.links)\n if (!links.length) return null\n const primary = links.find((link) => link.kind === 'primary')\n return (primary ?? links[0]).href\n}\n\nfunction normalizeErrorMessage(input: unknown, fallback?: string): string | null {\n const fallbackMessage = typeof fallback === 'string' && fallback.trim().length ? fallback.trim() : null\n let message: string | null = null\n if (typeof input === 'string') {\n message = input\n } else if (input instanceof Error && typeof input.message === 'string') {\n message = input.message\n }\n if (message) {\n const trimmed = message.trim()\n if (trimmed.length) {\n const sanitized = trimmed.replace(/^\\[[^\\]]+\\]\\s*/, '').trim()\n if (sanitized.length) return sanitized\n }\n }\n return fallbackMessage\n}\n\ntype HybridSearchTableProps = {\n /** Show strategy selector checkboxes (default: false - hidden from regular users) */\n showStrategySelector?: boolean\n /** Show source column in results (default: false - hidden from regular users) */\n showSourceColumn?: boolean\n}\n\nexport function HybridSearchTable({\n showStrategySelector = false,\n showSourceColumn = false,\n}: HybridSearchTableProps = {}) {\n const router = useRouter()\n const t = useT()\n const [showScopeHint, setShowScopeHint] = React.useState<boolean>(() => hasActiveOrganizationSelection())\n const [searchValue, setSearchValue] = React.useState('')\n const [rows, setRows] = React.useState<Row[]>([])\n const [page, setPage] = React.useState(1)\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [timing, setTiming] = React.useState<number | null>(null)\n const [strategiesUsed, setStrategiesUsed] = React.useState<string[]>([])\n const [enabledStrategies, setEnabledStrategies] = React.useState<Set<SearchStrategyId>>(\n new Set(ALL_STRATEGIES)\n )\n const debounceRef = React.useRef<number | null>(null)\n const abortRef = React.useRef<AbortController | null>(null)\n const columns = React.useMemo(() => {\n const allColumns = createColumns(t)\n if (!showSourceColumn) {\n return allColumns.filter((col) => col.id !== 'source')\n }\n return allColumns\n }, [t, showSourceColumn])\n\n React.useEffect(() => {\n setShowScopeHint(hasActiveOrganizationSelection())\n return subscribeOrganizationScopeChanged((detail) => {\n setShowScopeHint(Boolean(detail.organizationId && detail.organizationId.trim().length > 0))\n })\n }, [])\n\n const toggleStrategy = React.useCallback((strategy: SearchStrategyId) => {\n setEnabledStrategies((prev) => {\n const next = new Set(prev)\n if (next.has(strategy)) {\n next.delete(strategy)\n } else {\n next.add(strategy)\n }\n return next\n })\n }, [])\n\n const openRow = React.useCallback(\n (row: Row) => {\n const href = pickPrimaryLink(row)\n if (!href) return\n router.push(href)\n },\n [router]\n )\n\n React.useEffect(() => {\n const trimmed = searchValue.trim()\n abortRef.current?.abort()\n if (debounceRef.current) {\n window.clearTimeout(debounceRef.current)\n debounceRef.current = null\n }\n\n if (trimmed.length < MIN_QUERY_LENGTH) {\n setRows([])\n setTiming(null)\n setStrategiesUsed([])\n setError(null)\n setLoading(false)\n return\n }\n\n if (enabledStrategies.size === 0) {\n setRows([])\n setTiming(null)\n setStrategiesUsed([])\n setError(t('search.table.errors.noSources', 'Select at least one search source'))\n setLoading(false)\n return\n }\n\n const controller = new AbortController()\n abortRef.current = controller\n setLoading(true)\n\n debounceRef.current = window.setTimeout(async () => {\n try {\n const data = await fetchHybridSearchResults(trimmed, {\n limit: 50,\n strategies: Array.from(enabledStrategies),\n signal: controller.signal,\n })\n const mapped = data.results.map<Row>((item) => ({\n entityId: item.entityId,\n recordId: item.recordId,\n source: item.source,\n score: typeof item.score === 'number' ? item.score : null,\n url: item.url ?? null,\n presenter: item.presenter ?? null,\n links: item.links ?? null,\n metadata: (item.metadata as Record<string, unknown> | null) ?? null,\n }))\n setRows(mapped)\n setTiming(data.timing)\n setStrategiesUsed(data.strategiesUsed)\n const message = data.error ? normalizeErrorMessage(data.error, t('search.table.errors.searchFailed', 'Search failed')) : null\n setError(message ?? null)\n setPage(1)\n } catch (err: unknown) {\n if (controller.signal.aborted) return\n if ((err as { name?: string })?.name === 'AbortError') return\n setError(normalizeErrorMessage(err, t('search.table.errors.searchFailed', 'Search failed')))\n setRows([])\n setTiming(null)\n setStrategiesUsed([])\n } finally {\n if (!controller.signal.aborted) setLoading(false)\n }\n }, 250)\n\n return () => {\n controller.abort()\n if (debounceRef.current) window.clearTimeout(debounceRef.current)\n }\n }, [searchValue, enabledStrategies, t])\n\n React.useEffect(() => {\n if (!error) return\n flash(error, 'error')\n }, [error])\n\n return (\n <div className=\"flex w-full flex-col gap-4\">\n {/* Source Selector - only shown when showStrategySelector is true */}\n {showStrategySelector && (\n <div className=\"flex flex-wrap items-center gap-4 rounded-lg border bg-muted/50 p-3\">\n <span className=\"text-sm font-medium text-muted-foreground\">\n {t('search.table.sources', 'Sources:')}\n </span>\n {ALL_STRATEGIES.map((strategy) => (\n <label key={strategy} className=\"flex cursor-pointer items-center gap-2\">\n <input\n type=\"checkbox\"\n className=\"size-4 rounded border-gray-300\"\n checked={enabledStrategies.has(strategy)}\n onChange={() => toggleStrategy(strategy)}\n />\n <span\n className={cn(\n 'rounded px-2 py-0.5 text-xs font-medium',\n getStrategyColorClass(strategy)\n )}\n >\n {strategy}\n </span>\n </label>\n ))}\n </div>\n )}\n\n {/* Stats Bar */}\n {timing !== null && rows.length > 0 && (\n <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n <span>{rows.length} {t('search.table.stats.results', 'results')}</span>\n <span>{timing}ms</span>\n {/* Only show sources when strategy selector is visible */}\n {showStrategySelector && strategiesUsed.length > 0 && (\n <span>\n {t('search.table.stats.sources', 'Sources:')} {strategiesUsed.join(', ')}\n </span>\n )}\n </div>\n )}\n\n {/* Error Alert */}\n {error ? (\n <div\n role=\"alert\"\n className=\"w-full rounded-md border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive\"\n >\n {error}\n </div>\n ) : null}\n\n {showScopeHint ? (\n <div className=\"text-xs text-muted-foreground\">\n {t('search.scopeHint.currentOrg', 'Scoped to current organization')}\n </div>\n ) : null}\n\n {/* Data Table */}\n <DataTable<Row>\n title={t('search.table.title', 'Search')}\n columns={columns}\n data={rows}\n searchValue={searchValue}\n onSearchChange={(value) => {\n setSearchValue(value)\n setPage(1)\n }}\n searchPlaceholder={t('search.table.searchPlaceholder', 'Search across all strategies...')}\n isLoading={loading}\n pagination={{ page, pageSize: rows.length || 1, total: rows.length, totalPages: 1, onPageChange: setPage }}\n onRowClick={(row) => openRow(row)}\n rowActions={(row) => {\n const primaryHref = pickPrimaryLink(row)\n if (!primaryHref) return null\n return <RowActions items={[{ id: 'open', label: t('search.table.actions.open', 'Open'), href: primaryHref }]} />\n }}\n embedded\n />\n </div>\n )\n}\n\nexport default HybridSearchTable\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -142,24 +142,24 @@ function FulltextSearchSection({
|
|
|
142
142
|
/* @__PURE__ */ jsx("span", { children: t("search.settings.loadingLabel", "Loading settings...") })
|
|
143
143
|
] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
144
144
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-3 rounded-md border border-border bg-muted/30", children: [
|
|
145
|
-
/* @__PURE__ */ jsx("div", { className: `flex h-10 w-10 items-center justify-center rounded-full ${fulltextConfig?.configured ? "bg-
|
|
145
|
+
/* @__PURE__ */ jsx("div", { className: `flex h-10 w-10 items-center justify-center rounded-full ${fulltextConfig?.configured ? "bg-status-success-bg text-status-success-icon" : "bg-status-warning-bg text-status-warning-icon"}`, children: getStrategyIcon() }),
|
|
146
146
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
147
147
|
/* @__PURE__ */ jsxs("p", { className: "font-medium", children: [
|
|
148
148
|
t("search.settings.fulltext.driver", "Current Driver"),
|
|
149
149
|
": ",
|
|
150
150
|
fulltextConfig?.driver ? "Meilisearch" : t("search.settings.fulltext.noDriver", "None")
|
|
151
151
|
] }),
|
|
152
|
-
/* @__PURE__ */ jsx("p", { className: `text-sm ${fulltextConfig?.configured ? "text-
|
|
152
|
+
/* @__PURE__ */ jsx("p", { className: `text-sm ${fulltextConfig?.configured ? "text-status-success-text" : "text-status-warning-text"}`, children: fulltextConfig?.configured ? t("search.settings.fulltext.ready", "Ready to use") : t("search.settings.fulltext.notReady", "Not configured - set environment variables below") })
|
|
153
153
|
] })
|
|
154
154
|
] }),
|
|
155
155
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
156
156
|
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold mb-2", children: t("search.settings.fulltext.envVars", "Required Environment Variables") }),
|
|
157
157
|
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: fulltextConfig?.envVars && Object.entries(fulltextConfig.envVars).map(([key, status]) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 p-3 rounded-md border border-border", children: [
|
|
158
|
-
/* @__PURE__ */ jsx("div", { className: `flex h-5 w-5 items-center justify-center rounded-full flex-shrink-0 mt-0.5 ${status.set ? "bg-
|
|
158
|
+
/* @__PURE__ */ jsx("div", { className: `flex h-5 w-5 items-center justify-center rounded-full flex-shrink-0 mt-0.5 ${status.set ? "bg-status-success-bg text-status-success-icon" : "bg-status-error-bg text-status-error-icon"}`, children: status.set ? /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }) : /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }),
|
|
159
159
|
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
160
160
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
161
161
|
/* @__PURE__ */ jsx("code", { className: "text-sm font-mono bg-muted px-1.5 py-0.5 rounded", children: key }),
|
|
162
|
-
/* @__PURE__ */ jsx("span", { className: `text-xs ${status.set ? "text-
|
|
162
|
+
/* @__PURE__ */ jsx("span", { className: `text-xs ${status.set ? "text-status-success-text" : "text-status-error-text"}`, children: status.set ? t("search.settings.fulltext.envSet", "Set") : t("search.settings.fulltext.envMissing", "Missing") })
|
|
163
163
|
] }),
|
|
164
164
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1", children: status.hint })
|
|
165
165
|
] })
|
|
@@ -171,7 +171,7 @@ function FulltextSearchSection({
|
|
|
171
171
|
/* @__PURE__ */ jsx("code", { className: "text-xs font-mono bg-muted px-1.5 py-0.5 rounded", children: key }),
|
|
172
172
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 text-xs text-muted-foreground", children: [
|
|
173
173
|
/* @__PURE__ */ jsx("span", { children: status.hint }),
|
|
174
|
-
status.set ? /* @__PURE__ */ jsxs("span", { className: "ml-2 text-
|
|
174
|
+
status.set ? /* @__PURE__ */ jsxs("span", { className: "ml-2 text-status-success-text", children: [
|
|
175
175
|
"(",
|
|
176
176
|
t("search.settings.fulltext.currentValue", "Current"),
|
|
177
177
|
": ",
|
|
@@ -187,9 +187,9 @@ function FulltextSearchSection({
|
|
|
187
187
|
] })
|
|
188
188
|
] }, key)) })
|
|
189
189
|
] }),
|
|
190
|
-
/* @__PURE__ */ jsx("div", { className: "p-3 rounded-md bg-
|
|
191
|
-
/* @__PURE__ */ jsx("svg", { className: "h-5 w-5 text-
|
|
192
|
-
/* @__PURE__ */ jsxs("div", { className: "text-sm text-
|
|
190
|
+
/* @__PURE__ */ jsx("div", { className: "p-3 rounded-md bg-status-info-bg border border-status-info-border", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
191
|
+
/* @__PURE__ */ jsx("svg", { className: "h-5 w-5 text-status-info-icon flex-shrink-0 mt-0.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }),
|
|
192
|
+
/* @__PURE__ */ jsxs("div", { className: "text-sm text-status-info-text", children: [
|
|
193
193
|
/* @__PURE__ */ jsx("p", { className: "font-medium mb-1", children: t("search.settings.fulltext.howTo", "How to set up") }),
|
|
194
194
|
/* @__PURE__ */ jsx("p", { className: "text-xs", children: t("search.settings.fulltext.howToDescription", "Add these variables to your .env file or deployment environment. You can use a hosted Meilisearch instance or run it locally with Docker.") }),
|
|
195
195
|
/* @__PURE__ */ jsxs(
|
|
@@ -198,7 +198,7 @@ function FulltextSearchSection({
|
|
|
198
198
|
href: "https://www.meilisearch.com/docs/learn/getting_started/quick_start",
|
|
199
199
|
target: "_blank",
|
|
200
200
|
rel: "noopener noreferrer",
|
|
201
|
-
className: "text-xs text-
|
|
201
|
+
className: "text-xs text-status-info-text hover:underline mt-1 inline-block",
|
|
202
202
|
children: [
|
|
203
203
|
t("search.settings.fulltext.learnMore", "Learn more: Meilisearch Quick Start"),
|
|
204
204
|
" \u2192"
|
|
@@ -211,22 +211,22 @@ function FulltextSearchSection({
|
|
|
211
211
|
/* @__PURE__ */ jsx(TabsContent, { value: "index", children: loading || fulltextConfigLoading ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
|
|
212
212
|
/* @__PURE__ */ jsx(Spinner, { size: "sm" }),
|
|
213
213
|
/* @__PURE__ */ jsx("span", { children: t("search.settings.loadingLabel", "Loading settings...") })
|
|
214
|
-
] }) : !fulltextConfig?.configured ? /* @__PURE__ */ jsx("div", { className: "p-4 rounded-md bg-
|
|
215
|
-
/* @__PURE__ */ jsx("svg", { className: "h-5 w-5 text-
|
|
214
|
+
] }) : !fulltextConfig?.configured ? /* @__PURE__ */ jsx("div", { className: "p-4 rounded-md bg-status-warning-bg border border-status-warning-border", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
215
|
+
/* @__PURE__ */ jsx("svg", { className: "h-5 w-5 text-status-warning-icon flex-shrink-0 mt-0.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" }) }),
|
|
216
216
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
217
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-
|
|
218
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-
|
|
217
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-status-warning-text", children: t("search.settings.fulltextNotConfigured", "Full-text search driver not configured") }),
|
|
218
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-status-warning-text mt-1", children: t("search.settings.fulltextNotConfiguredHint", "Configure the required environment variables in the Configuration tab to enable indexing.") })
|
|
219
219
|
] })
|
|
220
220
|
] }) }) : /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
221
221
|
fulltextStats ? /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-border p-4 max-w-xs", children: [
|
|
222
222
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("search.settings.documentsLabel", "Documents") }),
|
|
223
223
|
/* @__PURE__ */ jsx("p", { className: "text-2xl font-bold", children: fulltextStats.numberOfDocuments.toLocaleString() })
|
|
224
224
|
] }) : /* @__PURE__ */ jsx("div", { className: "p-3 rounded-md bg-muted/50", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("search.settings.noIndexMessage", "No index found for this tenant. Click 'Full Reindex' to create one.") }) }),
|
|
225
|
-
fulltextReindexLock && /* @__PURE__ */ jsx("div", { className: "p-4 rounded-md bg-
|
|
226
|
-
/* @__PURE__ */ jsx(Spinner, { size: "sm", className: "flex-shrink-0 mt-0.5 text-
|
|
225
|
+
fulltextReindexLock && /* @__PURE__ */ jsx("div", { className: "p-4 rounded-md bg-status-info-bg border border-status-info-border", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
226
|
+
/* @__PURE__ */ jsx(Spinner, { size: "sm", className: "flex-shrink-0 mt-0.5 text-status-info-icon" }),
|
|
227
227
|
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
228
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-
|
|
229
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-
|
|
228
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-status-info-text", children: t("search.settings.reindexInProgress", "Reindex operation in progress") }),
|
|
229
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-status-info-text mt-1", children: t("search.settings.reindexInProgressDetails", "Action: {{action}} | Started {{minutes}} minutes ago", {
|
|
230
230
|
action: fulltextReindexLock.action,
|
|
231
231
|
minutes: fulltextReindexLock.elapsedMinutes
|
|
232
232
|
}) })
|
|
@@ -295,11 +295,11 @@ function FulltextSearchSection({
|
|
|
295
295
|
] }) : activityLogs.length === 0 ? /* @__PURE__ */ jsx("div", { className: "p-4 rounded-md bg-muted/50 text-center", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("search.settings.activity.noLogs", "No recent indexing activity") }) }) : /* @__PURE__ */ jsx("div", { className: "space-y-2 max-h-80 overflow-y-auto", children: activityLogs.map((log) => /* @__PURE__ */ jsx(
|
|
296
296
|
"div",
|
|
297
297
|
{
|
|
298
|
-
className: `p-2 rounded-md text-sm ${log.level === "error" ? "bg-
|
|
298
|
+
className: `p-2 rounded-md text-sm ${log.level === "error" ? "bg-status-error-bg border border-status-error-border" : "bg-muted/50"}`,
|
|
299
299
|
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
300
|
-
log.level === "error" && /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-
|
|
300
|
+
log.level === "error" && /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-status-error-icon flex-shrink-0 mt-0.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }),
|
|
301
301
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
302
|
-
/* @__PURE__ */ jsx("p", { className: `text-xs ${log.level === "error" ? "text-
|
|
302
|
+
/* @__PURE__ */ jsx("p", { className: `text-xs ${log.level === "error" ? "text-status-error-text" : "text-foreground"}`, children: log.message }),
|
|
303
303
|
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-0.5", children: [
|
|
304
304
|
(() => {
|
|
305
305
|
const d = new Date(log.occurredAt);
|
|
@@ -329,17 +329,17 @@ function FulltextSearchSection({
|
|
|
329
329
|
) })
|
|
330
330
|
] })
|
|
331
331
|
] }),
|
|
332
|
-
showReindexDialog && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-
|
|
332
|
+
showReindexDialog && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-modal flex items-center justify-center bg-black/50", children: /* @__PURE__ */ jsxs("div", { className: "mx-4 max-w-md rounded-lg border border-border bg-card p-6 shadow-lg", children: [
|
|
333
333
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 mb-4", children: [
|
|
334
|
-
/* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-full bg-
|
|
334
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-full bg-status-warning-bg", children: /* @__PURE__ */ jsx("svg", { className: "h-5 w-5 text-status-warning-icon", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }) }),
|
|
335
335
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
336
336
|
/* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: getDialogContent(showReindexDialog).title }),
|
|
337
337
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1", children: getDialogContent(showReindexDialog).description })
|
|
338
338
|
] })
|
|
339
339
|
] }),
|
|
340
|
-
/* @__PURE__ */ jsx("div", { className: "mb-4 p-3 rounded-md bg-
|
|
341
|
-
/* @__PURE__ */ jsx("svg", { className: "h-5 w-5 text-
|
|
342
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-
|
|
340
|
+
/* @__PURE__ */ jsx("div", { className: "mb-4 p-3 rounded-md bg-status-warning-bg", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
341
|
+
/* @__PURE__ */ jsx("svg", { className: "h-5 w-5 text-status-warning-icon flex-shrink-0 mt-0.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" }) }),
|
|
342
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-status-warning-text", children: getDialogContent(showReindexDialog).warning })
|
|
343
343
|
] }) }),
|
|
344
344
|
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-3", children: [
|
|
345
345
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: handleReindexCancel, children: t("search.settings.cancelLabel", "Cancel") }),
|