import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { ActionIcon, TextInput, ScrollArea, Loader } from "@mantine/core"; import { useDebouncedValue } from "@mantine/hooks"; import { IconSearch, IconFileDescription } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { useGetSpacesQuery } from "@/features/space/queries/space-query"; import { useSearchSuggestionsQuery } from "@/features/search/queries/search-query"; import { ISpace } from "@/features/space/types/space.types"; import { IPage } from "@/features/page/types/page.types"; import { DestinationSelection } from "./destination-picker.types"; import { SpaceRow } from "./space-row"; import classes from "./destination-picker.module.css"; type DestinationPickerProps = { onSelectionChange: (selection: DestinationSelection | null) => void; excludePageId?: string; pageLimit?: number; initialSpaceId?: string; searchSpacesOnly?: boolean; }; export function DestinationPicker({ onSelectionChange, excludePageId, pageLimit = 15, initialSpaceId, searchSpacesOnly, }: DestinationPickerProps) { const { t } = useTranslation(); const [searchQuery, setSearchQuery] = useState(""); const [selection, setSelection] = useState(null); const [debouncedQuery] = useDebouncedValue(searchQuery, 300); const viewportRef = useRef(null); const { data: spacesData, isLoading: spacesLoading } = useGetSpacesQuery({ limit: 100, }); const searchEnabled = !searchSpacesOnly && debouncedQuery && debouncedQuery.length >= 2; const { data: searchData, isLoading: searchLoading } = useSearchSuggestionsQuery({ query: searchEnabled ? debouncedQuery : "", includePages: true, limit: 20, }); const isSearching = !!searchEnabled; const filteredSpaces = useMemo(() => { const items = spacesData?.items ?? []; if (!searchSpacesOnly || !debouncedQuery) return items; const fold = (s: string) => s .normalize("NFD") .replace(/[̀-ͯ]/g, "") .toLocaleLowerCase(); const term = fold(debouncedQuery); return items.filter((s) => fold(s.name).includes(term)); }, [spacesData, searchSpacesOnly, debouncedQuery]); const selectedId = selection?.type === "space" ? selection.spaceId : selection?.pageId ?? null; const updateSelection = useCallback( (next: DestinationSelection | null) => { setSelection(next); onSelectionChange(next); }, [onSelectionChange], ); const handleSearchResultClick = (page: Partial) => { if (!page.space || !page.id) return; updateSelection({ type: "page", spaceId: page.space.id, pageId: page.id, page, space: page.space, }); setSearchQuery(""); }; const handleSelectSpace = useCallback( (space: ISpace) => { updateSelection({ type: "space", spaceId: space.id, space }); }, [updateSelection], ); const handleSelectPage = useCallback( (page: Partial, space: ISpace) => { if (!page.id) return; updateSelection({ type: "page", spaceId: page.spaceId ?? space.id, pageId: page.id, page, space, }); }, [updateSelection], ); // Pre-select space when initialSpaceId is set and spaces have loaded. // Only runs once: skip if user has already made a selection. useEffect(() => { if (!initialSpaceId || selection) return; const match = spacesData?.items?.find((s) => s.id === initialSpaceId); if (match) { updateSelection({ type: "space", spaceId: match.id, space: match }); requestAnimationFrame(() => { const el = viewportRef.current?.querySelector( `[data-space-id="${match.id}"]`, ); el?.scrollIntoView({ block: "nearest" }); }); } }, [initialSpaceId, selection, spacesData, updateSelection]); return ( <> } placeholder={ searchSpacesOnly ? t("Search spaces...") : t("Search pages and spaces...") } aria-label={ searchSpacesOnly ? t("Search spaces...") : t("Search pages and spaces...") } variant="filled" value={searchQuery} onChange={(e) => setSearchQuery(e.currentTarget.value)} className={classes.searchInput} /> {isSearching ? ( searchLoading ? (
) : searchData?.pages && searchData.pages.length > 0 ? ( searchData.pages.map( (page) => page && (
handleSearchResultClick(page)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleSearchResultClick(page); } }} >
{page.icon ? ( page.icon ) : ( )}
{page.title || t("Untitled")}
{page.space && (
{page.space.name}
)}
), ) ) : (
{t("No results found")}
) ) : spacesLoading ? (
) : filteredSpaces.length === 0 ? (
{searchSpacesOnly && debouncedQuery ? t("No spaces found") : t("No results found")}
) : ( filteredSpaces.map((space) => ( )) )}
); }