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
3 changes: 2 additions & 1 deletion locale/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@
"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:"
},
"tools": {
"tidbOperator": {
Expand Down
3 changes: 2 additions & 1 deletion locale/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@
"content1": "请尝试使用其他关键词再次搜索。",
"content2": "如果您无法找到需要的内容,请尝试前往 <0>AskTUG (TiDB User Group)</0> 进行提问。"
},
"searchTip": "使用半角双引号进行精确搜索。例如,\"time_zone\"。"
"searchTip": "使用半角双引号进行精确搜索。例如,\"time_zone\"。",
"filters": "过滤搜索结果:"
},
"tools": {
"tidbOperator": {
Expand Down
103 changes: 99 additions & 4 deletions src/components/Search/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import Chip from "@mui/material/Chip";
import {
getSearchCategoryLabelKey,
resolveSearchCategory,
type SearchCategory,
} from "shared/utils/searchCategory";

export default function SearchResults(props: {
loading: boolean;
className?: string;
data: any[];
onFilterChange?: (category: SearchCategory | null) => void;
}) {
const { data, loading } = props;
const { data, loading, onFilterChange } = props;

const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
Expand Down Expand Up @@ -101,7 +103,11 @@ export default function SearchResults(props: {
</Typography> */}
<Stack spacing={4}>
{filteredDataMemo.map((item) => (
<SearchItem key={item.objectID} data={item} />
<SearchItem
key={item.objectID}
data={item}
onFilterChange={onFilterChange}
/>
))}
</Stack>
{data.length === 0 && !loading && (
Expand Down Expand Up @@ -177,8 +183,11 @@ function SearchItemSkeleton() {
);
}

function SearchItem(props: { data: any }) {
const { data } = props;
function SearchItem(props: {
data: any;
onFilterChange?: (category: SearchCategory | null) => void;
}) {
const { data, onFilterChange } = props;
const { t } = useI18next();
const category = React.useMemo(
() => resolveSearchCategory(data.url),
Expand Down Expand Up @@ -214,12 +223,27 @@ function SearchItem(props: { data: any }) {
size="small"
variant="outlined"
label={categoryLabel}
clickable={!!onFilterChange}
onClick={
onFilterChange
? (e) => {
e.preventDefault();
onFilterChange(category);
}
: undefined
}
sx={{
height: "20px",
fontSize: "12px",
borderRadius: "10px",
backgroundColor: "carbon.100",
color: "carbon.800",
...(onFilterChange && {
cursor: "pointer",
"&:hover": {
backgroundColor: "carbon.200",
},
}),
}}
/>
)}
Expand Down Expand Up @@ -267,3 +291,74 @@ function SearchItem(props: { data: any }) {
</Stack>
);
}

export function SearchFilterBar(props: {
categoryCountMap: Map<SearchCategory, number>;
activeFilter: SearchCategory | null;
onFilterChange: (category: SearchCategory | null) => void;
visible: boolean;
}) {
const { categoryCountMap, 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>
{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={{
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",
},
}),
}}
/>
);
})}
</Stack>
);
}
48 changes: 47 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,49 @@ 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 +101,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 +119,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 +163,20 @@ export default function DocSearchTemplate({
}}
/>
</Stack>
<SearchFilterBar
categoryCountMap={categoryCountMap}
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}
onFilterChange={handleFilterChange}
/>
<Box
sx={{
width: "fit-content",
Expand Down