- Generate UUID v4 values in the client with validated custom UID support.\n- Show immutable UIDs where Space titles need disambiguation.\n- Standardize Space surfaces on the Lucide Astroid icon.
156 lines
6.8 KiB
TypeScript
156 lines
6.8 KiB
TypeScript
import { CornerDownLeftIcon, SearchIcon } from "lucide-react";
|
|
import { FormEvent, useEffect, useState } from "react";
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { useAppSidebar } from "@/contexts/AppSidebarContext";
|
|
import { type MemoFilter, replaceFiltersByFactor, stringifyFilters, useMemoFilterContext } from "@/contexts/MemoFilterContext";
|
|
import { useSpaceContext } from "@/contexts/SpaceContext";
|
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
|
import { useMemoViews } from "@/hooks/useUserQueries";
|
|
import { BUILTIN_TASKS_VIEW_ID, getMemoViewId, isMemoScopeRoute } from "@/lib/memo-views";
|
|
import { extractSpaceUidFromName, formatSpaceUidForDisplay } from "@/lib/space-display";
|
|
import { useTranslate } from "@/utils/i18n";
|
|
import { getRouteActionPolicy, getSidebarRouteKind } from "./routes";
|
|
|
|
export const buildQuickFindFilters = (query: string, currentFilters: MemoFilter[], preserveCurrentScope: boolean): MemoFilter[] => {
|
|
const words = Array.from(new Set(query.trim().split(/\s+/).filter(Boolean)));
|
|
const contentFilters: MemoFilter[] = words.map((value) => ({ factor: "contentSearch", value }));
|
|
return preserveCurrentScope ? replaceFiltersByFactor(currentFilters, "contentSearch", contentFilters) : contentFilters;
|
|
};
|
|
|
|
export interface QuickFindSubmission {
|
|
filters: MemoFilter[];
|
|
destination?: string;
|
|
switchToAll: boolean;
|
|
}
|
|
|
|
export const resolveQuickFindSubmission = (pathname: string, query: string, currentFilters: MemoFilter[]): QuickFindSubmission => {
|
|
const routePolicy = getRouteActionPolicy(pathname);
|
|
const filters = buildQuickFindFilters(query, currentFilters, routePolicy.searchScope !== "all");
|
|
const filterQuery = stringifyFilters(filters);
|
|
return {
|
|
filters,
|
|
destination: routePolicy.searchDestination
|
|
? filterQuery
|
|
? `${routePolicy.searchDestination}?filter=${filterQuery}`
|
|
: routePolicy.searchDestination
|
|
: undefined,
|
|
switchToAll: routePolicy.searchScope === "all",
|
|
};
|
|
};
|
|
|
|
const getScopeLabel = (pathname: string, t: ReturnType<typeof useTranslate>) => {
|
|
const routeKind = getSidebarRouteKind(pathname);
|
|
if (routeKind === "archived") return t("common.archived");
|
|
if (routeKind === "explore") return t("common.explore");
|
|
if (routeKind === "profile") return t("common.profile");
|
|
return t("common.memos");
|
|
};
|
|
|
|
const QuickFindDialog = () => {
|
|
const t = useTranslate();
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const currentUser = useCurrentUser();
|
|
const { data: memoViews = [] } = useMemoViews(currentUser?.name);
|
|
const { filters, setFilters, setMemoView, memoView } = useMemoFilterContext();
|
|
const { clearSelectedSpace, duplicateSpaceTitles, selectedSpace, selectedSpaceName } = useSpaceContext();
|
|
const { quickFindOpen, setQuickFindOpen } = useAppSidebar();
|
|
const [query, setQuery] = useState("");
|
|
const viewApplies = isMemoScopeRoute(location.pathname);
|
|
const selectedMemoView = viewApplies ? memoViews.find((item) => getMemoViewId(item.name) === memoView) : undefined;
|
|
const lensLabel =
|
|
viewApplies && memoView === BUILTIN_TASKS_VIEW_ID ? t("common.tasks") : selectedMemoView?.title || getScopeLabel(location.pathname, t);
|
|
const routePolicy = getRouteActionPolicy(location.pathname);
|
|
const selectedSpaceUid = selectedSpaceName ? extractSpaceUidFromName(selectedSpaceName) : "";
|
|
const selectedSpaceUidDisplay = selectedSpaceName ? formatSpaceUidForDisplay(selectedSpaceName) : "";
|
|
const showSelectedSpaceUid = selectedSpace ? duplicateSpaceTitles.has(selectedSpace.title) : Boolean(selectedSpaceName);
|
|
const selectedSpaceLabel = `${selectedSpace?.title || t("space.current")}${
|
|
showSelectedSpaceUid && selectedSpaceUid ? ` (${selectedSpaceUid})` : ""
|
|
}`;
|
|
const compactSelectedSpaceLabel = `${selectedSpace?.title || t("space.current")}${
|
|
showSelectedSpaceUid && selectedSpaceUidDisplay ? ` (${selectedSpaceUidDisplay})` : ""
|
|
}`;
|
|
const scopeLabel =
|
|
routePolicy.searchScope === "remembered-collection" && selectedSpaceName ? `${selectedSpaceLabel} · ${lensLabel}` : lensLabel;
|
|
const compactScopeLabel =
|
|
routePolicy.searchScope === "remembered-collection" && selectedSpaceName ? `${compactSelectedSpaceLabel} · ${lensLabel}` : lensLabel;
|
|
|
|
useEffect(() => {
|
|
if (!quickFindOpen) return;
|
|
setQuery(
|
|
filters
|
|
.filter((filter) => filter.factor === "contentSearch")
|
|
.map((filter) => filter.value)
|
|
.join(" "),
|
|
);
|
|
}, [filters, quickFindOpen]);
|
|
|
|
const submitQuery = () => {
|
|
const submission = resolveQuickFindSubmission(location.pathname, query, filters);
|
|
|
|
if (submission.switchToAll) {
|
|
// This is an explicit cross-Space action, so switch the collection state
|
|
// to All without inserting an intermediate Home history entry.
|
|
clearSelectedSpace();
|
|
}
|
|
|
|
setFilters(submission.filters);
|
|
|
|
if (submission.destination) {
|
|
setMemoView(undefined);
|
|
navigate(submission.destination);
|
|
}
|
|
|
|
setQuickFindOpen(false);
|
|
};
|
|
|
|
const handleSubmit = (event: FormEvent) => {
|
|
event.preventDefault();
|
|
submitQuery();
|
|
};
|
|
|
|
return (
|
|
<Dialog open={quickFindOpen} onOpenChange={setQuickFindOpen}>
|
|
<DialogContent
|
|
size="lg"
|
|
className="top-[12vh]! translate-y-0! overflow-hidden border-border/70 p-0! shadow-xl sm:top-[16vh]! [&>div:first-child]:gap-0!"
|
|
showCloseButton={false}
|
|
>
|
|
<DialogTitle className="sr-only">{t("common.search")}</DialogTitle>
|
|
<DialogDescription className="sr-only">
|
|
{t("common.search")} {scopeLabel}
|
|
</DialogDescription>
|
|
<form onSubmit={handleSubmit} className="flex h-[52px] shrink-0 items-center gap-2.5 px-4">
|
|
<SearchIcon className="size-[17px] shrink-0 text-muted-foreground" strokeWidth={1.8} />
|
|
<Input
|
|
autoFocus
|
|
value={query}
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
onKeyDown={(event) => {
|
|
if (event.key !== "Enter") return;
|
|
event.preventDefault();
|
|
submitQuery();
|
|
}}
|
|
className="h-10 border-0 bg-transparent px-0 !text-[14px] shadow-none focus-visible:ring-0"
|
|
placeholder={`${t("common.search")} ${compactScopeLabel}`}
|
|
aria-label={`${t("common.search")} ${scopeLabel}`}
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
className="size-7 shrink-0 rounded-md text-muted-foreground"
|
|
aria-label={t("common.search")}
|
|
>
|
|
<CornerDownLeftIcon className="size-3.5" />
|
|
</Button>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default QuickFindDialog;
|