Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion locale/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@
"content1": "Try changing your keywords and search again.",
"content2": "If you can't find what you're looking for, seek help from our <0>Discord channels</0>."
},
"searchTip": "Use double quotes for exact match. For example, \"time_zone\"."
"searchTip": "Use double quotes for exact match. For example, \"time_zone\".",
"filters": "Filter search results:",
"allResults": "All"
},
"tools": {
"tidbOperator": {
Expand Down
4 changes: 3 additions & 1 deletion locale/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@
"content1": "現時点では日本語の検索は提供されていないため、英語と中国語のドキュメントに行ってもう一度試してください。",
"content2": "お探しのものが見つからない場合は、<0>Discord Channels</0>がお勧めです。"
},
"searchTip": "フレーズを正確に検索したい場合に「\"」で囲って利用します。例えば、\"time_zone\"。"
"searchTip": "フレーズを正確に検索したい場合に「\"」で囲って利用します。例えば、\"time_zone\"。",
"filters": "検索結果を絞り込む:",
"allResults": "すべての結果"
},
"tools": {
"tidbOperator": {
Expand Down
4 changes: 3 additions & 1 deletion locale/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@
"content1": "请尝试使用其他关键词再次搜索。",
"content2": "如果您无法找到需要的内容,请尝试前往 <0>AskTUG (TiDB User Group)</0> 进行提问。"
},
"searchTip": "使用半角双引号进行精确搜索。例如,\"time_zone\"。"
"searchTip": "使用半角双引号进行精确搜索。例如,\"time_zone\"。",
"filters": "过滤搜索结果:",
"allResults": "所有结果"
},
"tools": {
"tidbOperator": {
Expand Down
92 changes: 92 additions & 0 deletions src/components/Search/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import Skeleton from "@mui/material/Skeleton";
import clsx from "clsx";
import TablePagination from "@mui/material/TablePagination";
import Chip from "@mui/material/Chip";
import type { SxProps, Theme } from "@mui/material/styles";
import {
getSearchCategoryLabelKey,
resolveSearchCategory,
type SearchCategory,
} from "shared/utils/searchCategory";

export default function SearchResults(props: {
Expand Down Expand Up @@ -267,3 +269,93 @@ function SearchItem(props: { data: any }) {
</Stack>
);
}

function getSearchFilterChipSx(isActive: boolean): SxProps<Theme> {
return {
height: "24px",
fontSize: "12px",
borderRadius: "12px",
cursor: "pointer",
...(isActive
? {
backgroundColor: "carbon.800",
color: "white",
"&:hover": {
backgroundColor: "carbon.700",
},
}
: {
backgroundColor: "carbon.100",
color: "carbon.800",
"&:hover": {
backgroundColor: "carbon.200",
},
}),
};
}

export function SearchFilterBar(props: {
categoryCountMap: Map<SearchCategory, number>;
totalCount: number;
activeFilter: SearchCategory | null;
onFilterChange: (category: SearchCategory | null) => void;
visible: boolean;
}) {
const {
categoryCountMap,
totalCount,
activeFilter,
onFilterChange,
visible,
} = props;
const { t } = useI18next();

const entries = Array.from(categoryCountMap.entries()).filter(
([category]) => {
const labelKey = getSearchCategoryLabelKey(category);
return labelKey && t(labelKey);
}
);

if (!visible || entries.length <= 1) {
return null;
}

return (
<Stack
direction="row"
alignItems="center"
sx={{ paddingTop: "0.75rem", flexWrap: "wrap", gap: "0.5rem" }}
>
<Typography
variant="body2"
sx={{ fontWeight: 500, color: "carbon.700", whiteSpace: "nowrap" }}
>
<Trans i18nKey="search.filters" />
</Typography>
<Chip
size="small"
variant={activeFilter === null ? "filled" : "outlined"}
label={`${t("search.allResults")} (${totalCount})`}
clickable
onClick={() => onFilterChange(null)}
sx={getSearchFilterChipSx(activeFilter === null)}
/>
{entries.map(([category, count]) => {
const label = t(getSearchCategoryLabelKey(category));
const isActive = activeFilter === category;
return (
<Chip
key={category}
size="small"
variant={isActive ? "filled" : "outlined"}
label={`${label} (${count})`}
clickable
onClick={() => onFilterChange(category)}
sx={getSearchFilterChipSx(isActive)}
/>
);
})}
</Stack>
);
}
46 changes: 45 additions & 1 deletion src/templates/DocSearchTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import "styles/docTemplate.css";

import Layout from "components/Layout";
import SearchResults from "components/Search/Results";
import { SearchFilterBar } from "components/Search/Results";
import SearchInput from "components/Search";
import { Tip } from "components/MDXComponents";
import Seo from "components/Seo";
Expand All @@ -20,6 +21,10 @@ import { Locale, TOCNamespace } from "shared/interface";
import { FeedbackSurveyCampaign } from "components/Campaign/FeedbackSurvey";
import ScrollToTopBtn from "components/Button/ScrollToTopBtn";
import { useIsAutoTranslation } from "shared/useIsAutoTranslation";
import {
resolveSearchCategory,
type SearchCategory,
} from "shared/utils/searchCategory";

const SEARCH_INDEX_BY_LANGUAGE: Partial<Record<Locale, string>> = {
[Locale.en]: "en-tidb-all-stable",
Expand All @@ -40,22 +45,50 @@ export default function DocSearchTemplate({
}: DocSearchTemplateProps) {
const [isLoading, setIsLoading] = React.useState(false);
const [results, setResults] = React.useState<any[]>([]);
const [activeFilter, setActiveFilter] = React.useState<SearchCategory | null>(
null
);

const { language } = useI18next();
const { search } = useLocation();

const categoryCountMap = React.useMemo(() => {
const map = new Map<SearchCategory, number>();
for (const item of results) {
const cat = resolveSearchCategory(item.url);
if (cat) map.set(cat, (map.get(cat) || 0) + 1);
}
return map;
}, [results]);

const filteredResults = React.useMemo(() => {
if (!activeFilter) return results;
return results.filter(
(item) => resolveSearchCategory(item.url) === activeFilter
);
}, [results, activeFilter]);

const handleFilterChange = React.useCallback(
(category: SearchCategory | null) => {
setActiveFilter((prev) => (prev === category ? null : category));
},
[]
);

const execSearch = React.useCallback(
(query: string) => {
const trimmedQuery = query.trim();
if (!trimmedQuery) {
setResults([]);
setActiveFilter(null);
setIsLoading(false);
return;
}

const indexName = SEARCH_INDEX_BY_LANGUAGE[language as Locale];
if (!indexName) {
setResults([]);
setActiveFilter(null);
setIsLoading(false);
return;
}
Expand All @@ -69,11 +102,13 @@ export default function DocSearchTemplate({
})
.then(({ hits }) => {
setResults(hits);
setActiveFilter(null);
setIsLoading(false);
})
.catch((reason: any) => {
console.error(reason);
setResults([]);
setActiveFilter(null);
setIsLoading(false);
});
},
Expand All @@ -85,12 +120,14 @@ export default function DocSearchTemplate({
const query = searchParams.get("q") || "";
if (language === Locale.ja) {
setResults([]);
setActiveFilter(null);
setIsLoading(false);
return;
}

if (!query.trim()) {
setResults([]);
setActiveFilter(null);
setIsLoading(false);
return;
}
Expand Down Expand Up @@ -127,10 +164,17 @@ export default function DocSearchTemplate({
}}
/>
</Stack>
<SearchFilterBar
categoryCountMap={categoryCountMap}
totalCount={results.length}
activeFilter={activeFilter}
onFilterChange={handleFilterChange}
visible={!isLoading && results.length > 0}
/>
<Tip>
<Trans i18nKey="search.searchTip" />
</Tip>
<SearchResults loading={isLoading} data={results} />
<SearchResults loading={isLoading} data={filteredResults} />
<Box
sx={{
width: "fit-content",
Expand Down