mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
feat(beta): public spaces (#2473)
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { Group, Center, Text, Button } from "@mantine/core";
|
||||
import { Spotlight } from "@mantine/spotlight";
|
||||
import { IconLetterCase, IconSearch } from "@tabler/icons-react";
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { usePublicSpaceSearchQuery } from "@/features/search/queries/search-query";
|
||||
import { buildPublicSpaceUrl } from "@/features/page/page.utils.ts";
|
||||
import { getPageIcon } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { publicSpaceSearchSpotlightStore } from "@/features/search/constants.ts";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
interface PublicSpaceSearchSpotlightProps {
|
||||
spaceSlug: string;
|
||||
}
|
||||
export function PublicSpaceSearchSpotlight({
|
||||
spaceSlug,
|
||||
}: PublicSpaceSearchSpotlightProps) {
|
||||
const { t } = useTranslation();
|
||||
const [query, setQuery] = useState("");
|
||||
const [titleOnly, setTitleOnly] = useState(false);
|
||||
const [debouncedSearchQuery] = useDebouncedValue(query, 300);
|
||||
|
||||
const { data: searchResults } = usePublicSpaceSearchQuery({
|
||||
query: debouncedSearchQuery,
|
||||
spaceSlug,
|
||||
...(titleOnly && { titleOnly: true }),
|
||||
});
|
||||
|
||||
const pages = (
|
||||
searchResults && searchResults.length > 0 ? searchResults : []
|
||||
).map((page) => (
|
||||
<Spotlight.Action
|
||||
key={page.id}
|
||||
component={Link}
|
||||
//@ts-ignore
|
||||
to={buildPublicSpaceUrl({
|
||||
spaceSlug: spaceSlug,
|
||||
pageTitle: page.title,
|
||||
pageSlugId: page.slugId,
|
||||
})}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
<Group wrap="nowrap" w="100%">
|
||||
<Center>{getPageIcon(page?.icon)}</Center>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text>{page.title}</Text>
|
||||
|
||||
{page?.highlight && (
|
||||
<Text
|
||||
opacity={0.6}
|
||||
size="xs"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(page.highlight, {
|
||||
ALLOWED_TAGS: ["mark", "em", "strong", "b"],
|
||||
ALLOWED_ATTR: [],
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Group>
|
||||
</Spotlight.Action>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spotlight.Root
|
||||
store={publicSpaceSearchSpotlightStore}
|
||||
query={query}
|
||||
onQueryChange={setQuery}
|
||||
scrollable
|
||||
overlayProps={{
|
||||
backgroundOpacity: 0.55,
|
||||
}}
|
||||
>
|
||||
<Spotlight.Search
|
||||
placeholder={t("Search...")}
|
||||
aria-label={t("Search")}
|
||||
leftSection={<IconSearch size={20} stroke={1.5} />}
|
||||
/>
|
||||
<Group px="sm" py={6}>
|
||||
<Button
|
||||
variant={titleOnly ? "light" : "subtle"}
|
||||
color={titleOnly ? undefined : "gray"}
|
||||
size="compact-sm"
|
||||
radius="xl"
|
||||
fw={500}
|
||||
leftSection={<IconLetterCase size={15} />}
|
||||
aria-pressed={titleOnly}
|
||||
onClick={() => setTitleOnly((value) => !value)}
|
||||
>
|
||||
{t("Title only")}
|
||||
</Button>
|
||||
</Group>
|
||||
<Spotlight.ActionsList>
|
||||
{query.length === 0 && pages.length === 0 && (
|
||||
<Spotlight.Empty>{t("Start typing to search...")}</Spotlight.Empty>
|
||||
)}
|
||||
|
||||
{query.length > 0 && pages.length === 0 && (
|
||||
<Spotlight.Empty>{t("No results found...")}</Spotlight.Empty>
|
||||
)}
|
||||
|
||||
{pages.length > 0 && pages}
|
||||
</Spotlight.ActionsList>
|
||||
</Spotlight.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,3 +5,6 @@ export const [searchSpotlightStore, searchSpotlight] = createSpotlight();
|
||||
export const [shareSearchSpotlightStore, shareSearchSpotlight] =
|
||||
createSpotlight();
|
||||
|
||||
export const [publicSpaceSearchSpotlightStore, publicSpaceSearchSpotlight] =
|
||||
createSpotlight();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { keepPreviousData, useQuery, UseQueryResult } from "@tanstack/react-quer
|
||||
import {
|
||||
searchAttachments,
|
||||
searchPage,
|
||||
searchPublicSpace,
|
||||
searchShare,
|
||||
searchSuggestions,
|
||||
} from '@/features/search/services/search-service';
|
||||
@@ -55,3 +56,13 @@ export function useAttachmentSearchQuery(
|
||||
enabled: !!params.query,
|
||||
});
|
||||
}
|
||||
|
||||
export function usePublicSpaceSearchQuery(
|
||||
params: IPageSearchParams & { spaceSlug: string },
|
||||
): UseQueryResult<IPageSearch[], Error> {
|
||||
return useQuery({
|
||||
queryKey: ["public-space-search", params],
|
||||
queryFn: () => searchPublicSpace(params),
|
||||
enabled: !!params.query && !!params.spaceSlug,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,6 +28,16 @@ export async function searchShare(
|
||||
return req.data.items;
|
||||
}
|
||||
|
||||
export async function searchPublicSpace(
|
||||
params: IPageSearchParams & { spaceSlug: string },
|
||||
): Promise<IPageSearch[]> {
|
||||
const req = await api.post<{ items: IPageSearch[] }>(
|
||||
"/search/public-space-search",
|
||||
params,
|
||||
);
|
||||
return req.data.items;
|
||||
}
|
||||
|
||||
export async function searchAttachments(
|
||||
params: IPageSearchParams,
|
||||
): Promise<IAttachmentSearch[]> {
|
||||
|
||||
Reference in New Issue
Block a user