@open-mercato/search 0.4.9-develop-8d8db18714 → 0.4.9-develop-7120508245
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 +7 -1
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +2 -2
- package/dist/modules/search/frontend/utils.js +6 -1
- package/dist/modules/search/frontend/utils.js.map +2 -2
- package/dist/modules/search/i18n/de.json +1 -0
- package/dist/modules/search/i18n/en.json +1 -0
- package/dist/modules/search/i18n/es.json +1 -0
- package/dist/modules/search/i18n/pl.json +1 -0
- package/package.json +4 -4
- package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +9 -1
- package/src/modules/search/frontend/utils.ts +6 -1
- package/src/modules/search/i18n/de.json +1 -0
- package/src/modules/search/i18n/en.json +1 -0
- package/src/modules/search/i18n/es.json +1 -0
- package/src/modules/search/i18n/pl.json +1 -0
|
@@ -215,7 +215,13 @@ function GlobalSearchDialog({
|
|
|
215
215
|
if (controller.signal.aborted) return;
|
|
216
216
|
const abortError = err;
|
|
217
217
|
if (abortError?.name === "AbortError") return;
|
|
218
|
-
|
|
218
|
+
const status = typeof err?.status === "number" ? err.status : null;
|
|
219
|
+
const message = err instanceof Error ? err.message : "";
|
|
220
|
+
if (status === 403 || /forbidden/i.test(message)) {
|
|
221
|
+
setError(t("search.dialog.errors.noPermission"));
|
|
222
|
+
} else {
|
|
223
|
+
setError(err instanceof Error ? err.message : t("search.dialog.errors.searchFailed"));
|
|
224
|
+
}
|
|
219
225
|
setResults([]);
|
|
220
226
|
} finally {
|
|
221
227
|
if (!controller.signal.aborted) setLoading(false);
|
|
@@ -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 { 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 setError(err instanceof Error ? err.message : t('search.dialog.errors.searchFailed'))\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 bg-background px-3 py-2 focus-within:ring-2 focus-within:ring-ring\">\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\"\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": ";
|
|
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 { 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 const status = typeof (err as { status?: unknown })?.status === 'number'\n ? (err as { status: number }).status\n : null\n const message = err instanceof Error ? err.message : ''\n if (status === 403 || /forbidden/i.test(message)) {\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 bg-background px-3 py-2 focus-within:ring-2 focus-within:ring-ring\">\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\"\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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -26,7 +26,12 @@ async function fetchGlobalSearchResults(query, opts = {}) {
|
|
|
26
26
|
params.set("limit", String(limit));
|
|
27
27
|
const body = await readApiResultOrThrow(
|
|
28
28
|
`/api/search/search/global?${params.toString()}`,
|
|
29
|
-
{
|
|
29
|
+
{
|
|
30
|
+
signal: opts.signal,
|
|
31
|
+
headers: {
|
|
32
|
+
"x-om-forbidden-redirect": "0"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
30
35
|
{ errorMessage: "Search failed", allowNullResult: true }
|
|
31
36
|
);
|
|
32
37
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/search/frontend/utils.ts"],
|
|
4
|
-
"sourcesContent": ["import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\n\nexport type HybridSearchResponse = {\n results: SearchResult[]\n strategiesUsed: string[]\n timing: number\n error?: string | null\n}\n\nexport type FetchHybridSearchOptions = {\n limit?: number\n strategies?: SearchStrategyId[]\n signal?: AbortSignal\n}\n\nexport async function fetchHybridSearchResults(\n query: string,\n opts: FetchHybridSearchOptions = {}\n): Promise<HybridSearchResponse> {\n const params = new URLSearchParams()\n params.set('q', query)\n\n const limit = Math.max(1, Math.min(opts.limit ?? 50, 100))\n params.set('limit', String(limit))\n\n if (opts.strategies && opts.strategies.length > 0) {\n params.set('strategies', opts.strategies.join(','))\n }\n\n const body = await readApiResultOrThrow<{\n results?: SearchResult[]\n strategiesUsed?: string[]\n timing?: number\n error?: string\n }>(\n `/api/search/search?${params.toString()}`,\n { signal: opts.signal },\n { errorMessage: 'Hybrid search failed', allowNullResult: true }\n )\n\n return {\n results: Array.isArray(body?.results) ? body.results : [],\n strategiesUsed: Array.isArray(body?.strategiesUsed) ? body.strategiesUsed : [],\n timing: typeof body?.timing === 'number' ? body.timing : 0,\n error: typeof body?.error === 'string' ? body.error : null,\n }\n}\n\n/**\n * Fetch global search results for the Cmd+K dialog.\n * Uses the global search API which respects saved strategy settings.\n */\nexport type FetchGlobalSearchOptions = {\n limit?: number\n signal?: AbortSignal\n}\n\nexport async function fetchGlobalSearchResults(\n query: string,\n opts: FetchGlobalSearchOptions = {}\n): Promise<{ results: SearchResult[]; error?: string | null }> {\n const params = new URLSearchParams()\n params.set('q', query)\n\n const limit = Math.max(1, Math.min(opts.limit ?? 10, 50))\n params.set('limit', String(limit))\n\n const body = await readApiResultOrThrow<{\n results?: SearchResult[]\n error?: string\n }>(\n `/api/search/search/global?${params.toString()}`,\n {
|
|
5
|
-
"mappings": "AAAA,SAAS,4BAA4B;AAgBrC,eAAsB,yBACpB,OACA,OAAiC,CAAC,GACH;AAC/B,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,IAAI,KAAK,KAAK;AAErB,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC;AACzD,SAAO,IAAI,SAAS,OAAO,KAAK,CAAC;AAEjC,MAAI,KAAK,cAAc,KAAK,WAAW,SAAS,GAAG;AACjD,WAAO,IAAI,cAAc,KAAK,WAAW,KAAK,GAAG,CAAC;AAAA,EACpD;AAEA,QAAM,OAAO,MAAM;AAAA,IAMjB,sBAAsB,OAAO,SAAS,CAAC;AAAA,IACvC,EAAE,QAAQ,KAAK,OAAO;AAAA,IACtB,EAAE,cAAc,wBAAwB,iBAAiB,KAAK;AAAA,EAChE;AAEA,SAAO;AAAA,IACL,SAAS,MAAM,QAAQ,MAAM,OAAO,IAAI,KAAK,UAAU,CAAC;AAAA,IACxD,gBAAgB,MAAM,QAAQ,MAAM,cAAc,IAAI,KAAK,iBAAiB,CAAC;AAAA,IAC7E,QAAQ,OAAO,MAAM,WAAW,WAAW,KAAK,SAAS;AAAA,IACzD,OAAO,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AAAA,EACxD;AACF;AAWA,eAAsB,yBACpB,OACA,OAAiC,CAAC,GAC2B;AAC7D,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,IAAI,KAAK,KAAK;AAErB,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,SAAS,IAAI,EAAE,CAAC;AACxD,SAAO,IAAI,SAAS,OAAO,KAAK,CAAC;AAEjC,QAAM,OAAO,MAAM;AAAA,IAIjB,6BAA6B,OAAO,SAAS,CAAC;AAAA,IAC9C,
|
|
4
|
+
"sourcesContent": ["import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\n\nexport type HybridSearchResponse = {\n results: SearchResult[]\n strategiesUsed: string[]\n timing: number\n error?: string | null\n}\n\nexport type FetchHybridSearchOptions = {\n limit?: number\n strategies?: SearchStrategyId[]\n signal?: AbortSignal\n}\n\nexport async function fetchHybridSearchResults(\n query: string,\n opts: FetchHybridSearchOptions = {}\n): Promise<HybridSearchResponse> {\n const params = new URLSearchParams()\n params.set('q', query)\n\n const limit = Math.max(1, Math.min(opts.limit ?? 50, 100))\n params.set('limit', String(limit))\n\n if (opts.strategies && opts.strategies.length > 0) {\n params.set('strategies', opts.strategies.join(','))\n }\n\n const body = await readApiResultOrThrow<{\n results?: SearchResult[]\n strategiesUsed?: string[]\n timing?: number\n error?: string\n }>(\n `/api/search/search?${params.toString()}`,\n { signal: opts.signal },\n { errorMessage: 'Hybrid search failed', allowNullResult: true }\n )\n\n return {\n results: Array.isArray(body?.results) ? body.results : [],\n strategiesUsed: Array.isArray(body?.strategiesUsed) ? body.strategiesUsed : [],\n timing: typeof body?.timing === 'number' ? body.timing : 0,\n error: typeof body?.error === 'string' ? body.error : null,\n }\n}\n\n/**\n * Fetch global search results for the Cmd+K dialog.\n * Uses the global search API which respects saved strategy settings.\n */\nexport type FetchGlobalSearchOptions = {\n limit?: number\n signal?: AbortSignal\n}\n\nexport async function fetchGlobalSearchResults(\n query: string,\n opts: FetchGlobalSearchOptions = {}\n): Promise<{ results: SearchResult[]; error?: string | null }> {\n const params = new URLSearchParams()\n params.set('q', query)\n\n const limit = Math.max(1, Math.min(opts.limit ?? 10, 50))\n params.set('limit', String(limit))\n\n const body = await readApiResultOrThrow<{\n results?: SearchResult[]\n error?: string\n }>(\n `/api/search/search/global?${params.toString()}`,\n {\n signal: opts.signal,\n headers: {\n 'x-om-forbidden-redirect': '0',\n },\n },\n { errorMessage: 'Search failed', allowNullResult: true }\n )\n\n return {\n results: Array.isArray(body?.results) ? body.results : [],\n error: typeof body?.error === 'string' ? body.error : null,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,4BAA4B;AAgBrC,eAAsB,yBACpB,OACA,OAAiC,CAAC,GACH;AAC/B,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,IAAI,KAAK,KAAK;AAErB,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC;AACzD,SAAO,IAAI,SAAS,OAAO,KAAK,CAAC;AAEjC,MAAI,KAAK,cAAc,KAAK,WAAW,SAAS,GAAG;AACjD,WAAO,IAAI,cAAc,KAAK,WAAW,KAAK,GAAG,CAAC;AAAA,EACpD;AAEA,QAAM,OAAO,MAAM;AAAA,IAMjB,sBAAsB,OAAO,SAAS,CAAC;AAAA,IACvC,EAAE,QAAQ,KAAK,OAAO;AAAA,IACtB,EAAE,cAAc,wBAAwB,iBAAiB,KAAK;AAAA,EAChE;AAEA,SAAO;AAAA,IACL,SAAS,MAAM,QAAQ,MAAM,OAAO,IAAI,KAAK,UAAU,CAAC;AAAA,IACxD,gBAAgB,MAAM,QAAQ,MAAM,cAAc,IAAI,KAAK,iBAAiB,CAAC;AAAA,IAC7E,QAAQ,OAAO,MAAM,WAAW,WAAW,KAAK,SAAS;AAAA,IACzD,OAAO,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AAAA,EACxD;AACF;AAWA,eAAsB,yBACpB,OACA,OAAiC,CAAC,GAC2B;AAC7D,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,IAAI,KAAK,KAAK;AAErB,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,SAAS,IAAI,EAAE,CAAC;AACxD,SAAO,IAAI,SAAS,OAAO,KAAK,CAAC;AAEjC,QAAM,OAAO,MAAM;AAAA,IAIjB,6BAA6B,OAAO,SAAS,CAAC;AAAA,IAC9C;AAAA,MACE,QAAQ,KAAK;AAAA,MACb,SAAS;AAAA,QACP,2BAA2B;AAAA,MAC7B;AAAA,IACF;AAAA,IACA,EAAE,cAAc,iBAAiB,iBAAiB,KAAK;AAAA,EACzD;AAEA,SAAO;AAAA,IACL,SAAS,MAAM,QAAQ,MAAM,OAAO,IAAI,KAAK,UAAU,CAAC;AAAA,IACxD,OAAO,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AAAA,EACxD;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"search.dialog.actions.search": "Suchen",
|
|
28
28
|
"search.dialog.empty.hint": "Mindestens zwei Zeichen eingeben, um in indizierten Datensätzen zu suchen.",
|
|
29
29
|
"search.dialog.empty.none": "Keine Ergebnisse gefunden.",
|
|
30
|
+
"search.dialog.errors.noPermission": "Sie haben keine Berechtigung, die globale Suche zu verwenden.",
|
|
30
31
|
"search.dialog.errors.searchFailed": "Suche ist fehlgeschlagen.",
|
|
31
32
|
"search.dialog.input.placeholder": "Suche nach Kunden, Notizen, Deals, Aufgaben...",
|
|
32
33
|
"search.dialog.instructions": "Tippe, um in indizierten Datensätzen zu suchen. Verwende die Pfeiltasten, um Ergebnisse zu navigieren.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"search.dialog.actions.search": "Search",
|
|
28
28
|
"search.dialog.empty.hint": "Type at least two characters to search indexed records.",
|
|
29
29
|
"search.dialog.empty.none": "No results found.",
|
|
30
|
+
"search.dialog.errors.noPermission": "You do not have permission to use global search.",
|
|
30
31
|
"search.dialog.errors.searchFailed": "Search failed.",
|
|
31
32
|
"search.dialog.input.placeholder": "Search customers, notes, deals, todos...",
|
|
32
33
|
"search.dialog.instructions": "Type to search across indexed records. Use arrow keys to navigate results.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"search.dialog.actions.search": "Buscar",
|
|
28
28
|
"search.dialog.empty.hint": "Escribe al menos dos caracteres para buscar en los registros indexados.",
|
|
29
29
|
"search.dialog.empty.none": "No se encontraron resultados.",
|
|
30
|
+
"search.dialog.errors.noPermission": "No tienes permiso para usar la busqueda global.",
|
|
30
31
|
"search.dialog.errors.searchFailed": "La búsqueda falló.",
|
|
31
32
|
"search.dialog.input.placeholder": "Busca clientes, notas, oportunidades, tareas...",
|
|
32
33
|
"search.dialog.instructions": "Escribe para buscar en los registros indexados. Usa las flechas para navegar por los resultados.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"search.dialog.actions.search": "Szukaj",
|
|
28
28
|
"search.dialog.empty.hint": "Wpisz co najmniej dwa znaki, aby przeszukać zindeksowane rekordy.",
|
|
29
29
|
"search.dialog.empty.none": "Brak wyników.",
|
|
30
|
+
"search.dialog.errors.noPermission": "Nie masz uprawnien do korzystania z wyszukiwania globalnego.",
|
|
30
31
|
"search.dialog.errors.searchFailed": "Wyszukiwanie nie powiodło się.",
|
|
31
32
|
"search.dialog.input.placeholder": "Szukaj klientów, notatek, szans, zadań...",
|
|
32
33
|
"search.dialog.instructions": "Wpisz tekst, aby przeszukać zindeksowane rekordy. Użyj strzałek do nawigacji po wynikach.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/search",
|
|
3
|
-
"version": "0.4.9-develop-
|
|
3
|
+
"version": "0.4.9-develop-7120508245",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -126,9 +126,9 @@
|
|
|
126
126
|
"zod": "^4.0.0"
|
|
127
127
|
},
|
|
128
128
|
"peerDependencies": {
|
|
129
|
-
"@open-mercato/core": "0.4.9-develop-
|
|
130
|
-
"@open-mercato/queue": "0.4.9-develop-
|
|
131
|
-
"@open-mercato/shared": "0.4.9-develop-
|
|
129
|
+
"@open-mercato/core": "0.4.9-develop-7120508245",
|
|
130
|
+
"@open-mercato/queue": "0.4.9-develop-7120508245",
|
|
131
|
+
"@open-mercato/shared": "0.4.9-develop-7120508245"
|
|
132
132
|
},
|
|
133
133
|
"devDependencies": {
|
|
134
134
|
"@types/jest": "^30.0.0",
|
|
@@ -253,7 +253,15 @@ export function GlobalSearchDialog({
|
|
|
253
253
|
if (controller.signal.aborted) return
|
|
254
254
|
const abortError = err as { name?: string }
|
|
255
255
|
if (abortError?.name === 'AbortError') return
|
|
256
|
-
|
|
256
|
+
const status = typeof (err as { status?: unknown })?.status === 'number'
|
|
257
|
+
? (err as { status: number }).status
|
|
258
|
+
: null
|
|
259
|
+
const message = err instanceof Error ? err.message : ''
|
|
260
|
+
if (status === 403 || /forbidden/i.test(message)) {
|
|
261
|
+
setError(t('search.dialog.errors.noPermission'))
|
|
262
|
+
} else {
|
|
263
|
+
setError(err instanceof Error ? err.message : t('search.dialog.errors.searchFailed'))
|
|
264
|
+
}
|
|
257
265
|
setResults([])
|
|
258
266
|
} finally {
|
|
259
267
|
if (!controller.signal.aborted) setLoading(false)
|
|
@@ -71,7 +71,12 @@ export async function fetchGlobalSearchResults(
|
|
|
71
71
|
error?: string
|
|
72
72
|
}>(
|
|
73
73
|
`/api/search/search/global?${params.toString()}`,
|
|
74
|
-
{
|
|
74
|
+
{
|
|
75
|
+
signal: opts.signal,
|
|
76
|
+
headers: {
|
|
77
|
+
'x-om-forbidden-redirect': '0',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
75
80
|
{ errorMessage: 'Search failed', allowNullResult: true }
|
|
76
81
|
)
|
|
77
82
|
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"search.dialog.actions.search": "Suchen",
|
|
28
28
|
"search.dialog.empty.hint": "Mindestens zwei Zeichen eingeben, um in indizierten Datensätzen zu suchen.",
|
|
29
29
|
"search.dialog.empty.none": "Keine Ergebnisse gefunden.",
|
|
30
|
+
"search.dialog.errors.noPermission": "Sie haben keine Berechtigung, die globale Suche zu verwenden.",
|
|
30
31
|
"search.dialog.errors.searchFailed": "Suche ist fehlgeschlagen.",
|
|
31
32
|
"search.dialog.input.placeholder": "Suche nach Kunden, Notizen, Deals, Aufgaben...",
|
|
32
33
|
"search.dialog.instructions": "Tippe, um in indizierten Datensätzen zu suchen. Verwende die Pfeiltasten, um Ergebnisse zu navigieren.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"search.dialog.actions.search": "Search",
|
|
28
28
|
"search.dialog.empty.hint": "Type at least two characters to search indexed records.",
|
|
29
29
|
"search.dialog.empty.none": "No results found.",
|
|
30
|
+
"search.dialog.errors.noPermission": "You do not have permission to use global search.",
|
|
30
31
|
"search.dialog.errors.searchFailed": "Search failed.",
|
|
31
32
|
"search.dialog.input.placeholder": "Search customers, notes, deals, todos...",
|
|
32
33
|
"search.dialog.instructions": "Type to search across indexed records. Use arrow keys to navigate results.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"search.dialog.actions.search": "Buscar",
|
|
28
28
|
"search.dialog.empty.hint": "Escribe al menos dos caracteres para buscar en los registros indexados.",
|
|
29
29
|
"search.dialog.empty.none": "No se encontraron resultados.",
|
|
30
|
+
"search.dialog.errors.noPermission": "No tienes permiso para usar la busqueda global.",
|
|
30
31
|
"search.dialog.errors.searchFailed": "La búsqueda falló.",
|
|
31
32
|
"search.dialog.input.placeholder": "Busca clientes, notas, oportunidades, tareas...",
|
|
32
33
|
"search.dialog.instructions": "Escribe para buscar en los registros indexados. Usa las flechas para navegar por los resultados.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"search.dialog.actions.search": "Szukaj",
|
|
28
28
|
"search.dialog.empty.hint": "Wpisz co najmniej dwa znaki, aby przeszukać zindeksowane rekordy.",
|
|
29
29
|
"search.dialog.empty.none": "Brak wyników.",
|
|
30
|
+
"search.dialog.errors.noPermission": "Nie masz uprawnien do korzystania z wyszukiwania globalnego.",
|
|
30
31
|
"search.dialog.errors.searchFailed": "Wyszukiwanie nie powiodło się.",
|
|
31
32
|
"search.dialog.input.placeholder": "Szukaj klientów, notatek, szans, zadań...",
|
|
32
33
|
"search.dialog.instructions": "Wpisz tekst, aby przeszukać zindeksowane rekordy. Użyj strzałek do nawigacji po wynikach.",
|