Skip to content

Commit

Permalink
feat: Make searchable again (#1898)
Browse files Browse the repository at this point in the history
* chore: search data

* chore: fill content

* chore: fix loading

* chore: support loading state
  • Loading branch information
zombieJ committed Sep 15, 2023
1 parent bf55a95 commit d969e98
Show file tree
Hide file tree
Showing 14 changed files with 227 additions and 85 deletions.
3 changes: 3 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ export * from './dist';
export { defineConfig } from './dist';
export * from './dist/client/theme-api';
export { IApi } from './dist/types';
export function getRouteMetaById(id: string): Promise<any>;
/** @private Internal usage. Safe to remove */
export function loadFilesMeta(): Promise<any>;
9 changes: 8 additions & 1 deletion src/client/theme-api/context/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { PICKED_PKG_FIELDS } from '@/constants';
import type { AtomComponentAsset } from 'dumi-assets-types';
import { createContext, useContext, type ComponentType } from 'react';
import type { ILocalesConfig, IPreviewerProps, IThemeConfig } from '../types';
import type {
ILocalesConfig,
IPreviewerProps,
IRouteMeta,
IThemeConfig,
} from '../types';
import use from './use';

export type DemoInfo = {
Expand All @@ -16,6 +21,7 @@ interface ISiteContext {
entryExports: Record<string, any>;
demos: Record<string, DemoInfo>;
components: Record<string, AtomComponentAsset>;
tabs: IRouteMeta['tabs'];
locales: ILocalesConfig;
themeConfig: IThemeConfig;
hostname?: string;
Expand Down Expand Up @@ -44,6 +50,7 @@ export const SiteContext = createContext<ISiteContext>({
setLoading: () => {},
_2_level_nav_available: true,
getDemoById: async () => null,
tabs: [],
});

export const useSiteData = () => {
Expand Down
3 changes: 3 additions & 0 deletions src/client/theme-api/context/use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ type ReactPromise<T> = Promise<T> & {
reason?: any;
};

/**
* @private Internal usage. Safe to remove
*/
export default function use<T>(promise: ReactPromise<T>): T {
if (promise.status === 'fulfilled') {
return promise.value!;
Expand Down
34 changes: 25 additions & 9 deletions src/client/theme-api/useSiteSearch/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useNavData, useSiteData } from 'dumi';
import { useNavData } from 'dumi';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useLocaleDocRoutes } from '../utils';
// @ts-ignore
import workerCode from '-!../../../../compiled/_internal/searchWorker.min?dumi-raw';
import useSearchData from './useSearchData';

export interface IHighlightText {
highlighted?: boolean;
Expand Down Expand Up @@ -38,17 +38,25 @@ if (typeof window !== 'undefined') {

export const useSiteSearch = () => {
const debounceTimer = useRef<number>();
const routes = useLocaleDocRoutes();
const { demos } = useSiteData();
// const routes = useLocaleDocRoutes();
// const { demos } = useSiteData();
const [loading, setLoading] = useState(false);
const [keywords, setKeywords] = useState('');
const [enabled, setEnabled] = useState(false);
const navData = useNavData();
const [result, setResult] = useState<ISearchResult>([]);
const setter = useCallback((val: string) => {
setLoading(true);
setKeywords(val);

if (val) {
setEnabled(true);
}
}, []);

const [filledRoutes, demos] = useSearchData(enabled);
const mergedLoading = loading || !filledRoutes;

useEffect(() => {
worker.onmessage = (e) => {
setResult(e.data);
Expand All @@ -57,9 +65,13 @@ export const useSiteSearch = () => {
}, []);

useEffect(() => {
if (!filledRoutes || !demos) {
return;
}

// omit demo component for postmessage
const demoData = Object.entries(demos).reduce<
Record<string, Partial<typeof demos[0]>>
Record<string, Partial<(typeof demos)[0]>>
>(
(acc, [key, { asset, routeId }]) => ({
...acc,
Expand All @@ -71,14 +83,18 @@ export const useSiteSearch = () => {
worker.postMessage({
action: 'generate-metadata',
args: {
routes: JSON.parse(JSON.stringify(routes)),
routes: JSON.parse(JSON.stringify(filledRoutes)),
nav: navData,
demos: demoData,
},
});
}, [routes, demos, navData]);
}, [demos, navData, filledRoutes]);

useEffect(() => {
if (!filledRoutes) {
return;
}

const str = keywords.trim();

if (str) {
Expand All @@ -94,7 +110,7 @@ export const useSiteSearch = () => {
} else {
setResult([]);
}
}, [keywords]);
}, [keywords, filledRoutes]);

return { keywords, setKeywords: setter, result, loading };
return { keywords, setKeywords: setter, result, loading: mergedLoading };
};
76 changes: 76 additions & 0 deletions src/client/theme-api/useSiteSearch/useSearchData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { loadFilesMeta, useSiteData } from 'dumi';
import React from 'react';
import type { DemoInfo } from '../context';
import type { IRouteMeta } from '../types';
import { useLocaleDocRoutes } from '../utils';

type RoutesData = Record<string, IRouteMeta & { demos: DemoInfo[] }>;

type Demos = Record<string, DemoInfo>;

type ReturnData = [
routes: ReturnType<typeof useLocaleDocRoutes> | null,
demo: Demos | null,
];

export default function useSearchData(enabled: boolean): ReturnData {
const routes = useLocaleDocRoutes();
const [filesMeta, setFilesMeta] = React.useState<RoutesData | null>(null);
const { tabs } = useSiteData();

React.useEffect(() => {
if (enabled) {
loadFilesMeta().then((data) => {
setFilesMeta(data);
});
}
}, [enabled]);

return React.useMemo(() => {
if (!filesMeta) {
return [null, null];
}

// Route Meta
const mergedRoutes: typeof routes = {};

Object.keys(routes).forEach((routeId) => {
mergedRoutes[routeId] = {
...routes[routeId],
};

// Fill routes meta
if (mergedRoutes[routeId].meta) {
mergedRoutes[routeId].meta = {
...mergedRoutes[routeId].meta,
...filesMeta[routeId],
tabs: mergedRoutes[routeId].tabs?.map((id: string) => {
const meta = {
frontmatter: { title: tabs[id].title },
toc: [],
texts: [],
};
return {
...tabs[id],
meta: filesMeta[id] || meta,
};
}),
};
}
});

// Demos
const demos: Demos = Object.entries(filesMeta).reduce((acc, [id, meta]) => {
// append route id to demo
Object.values(meta.demos).forEach((demo) => {
demo.routeId = id;
});
// merge demos
Object.assign(acc, meta.demos);

return acc;
}, {});

return [mergedRoutes, demos];
}, [filesMeta, routes, tabs]);
}
1 change: 1 addition & 0 deletions src/client/theme-default/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@
"content.footer.actions.previous": "PREV",
"content.footer.actions.next": "NEXT",
"search.not.found": "No content was found",
"search.loading": "Loading...",
"layout.sidebar.btn": "Sidebar"
}
1 change: 1 addition & 0 deletions src/client/theme-default/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@
"content.footer.actions.previous": "上一篇",
"content.footer.actions.next": "下一篇",
"search.not.found": "未找到相关内容",
"search.loading": "加载中...",
"layout.sidebar.btn": "侧边菜单"
}
21 changes: 9 additions & 12 deletions src/client/theme-default/slots/SearchBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { ReactComponent as IconArrowDown } from '@ant-design/icons-svg/inline-sv
import { ReactComponent as IconArrowUp } from '@ant-design/icons-svg/inline-svg/outlined/arrow-up.svg';
import { ReactComponent as IconSearch } from '@ant-design/icons-svg/inline-svg/outlined/search.svg';
import { useSiteSearch } from 'dumi';
import React, { useEffect, useRef, useState, type FC } from 'react';
import SearchResult from 'dumi/theme/slots/SearchResult';
import './index.less';
import React, { useEffect, useRef, useState, type FC } from 'react';
import { Input } from './Input';
import { Mask } from './Mask';
import './index.less';
export { Input as SearchInput } from './Input';
export { Mask as SearchMask } from './Mask';

Expand Down Expand Up @@ -88,16 +88,13 @@ const SearchBar: FC = () => {
ref={inputRef}
/>
<span className="dumi-default-search-shortcut">{symbol} K</span>
{keywords.trim() &&
focusing &&
(result.length || !loading) &&
!modalVisible && (
<div className="dumi-default-search-popover">
<section>
<SearchResult data={result} loading={loading} />
</section>
</div>
)}
{keywords.trim() && focusing && !modalVisible && (
<div className="dumi-default-search-popover">
<section>
<SearchResult data={result} loading={loading} />
</section>
</div>
)}

<Mask
visible={modalVisible}
Expand Down
75 changes: 45 additions & 30 deletions src/client/theme-default/slots/SearchResult/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,50 @@ const SearchResult: FC<{
return () => document.removeEventListener('keydown', handler);
});

let returnNode: React.ReactNode = null;

if (props.loading) {
returnNode = (
<div className="dumi-default-search-empty">
<IconInbox />
<FormattedMessage id="search.loading" />
</div>
);
} else if (props.data.length) {
returnNode = (
<dl>
{data.map((item, i) =>
item.type === 'title' ? (
<dt key={String(i)}>{item.value.title}</dt>
) : (
<dd key={String(i)}>
<Link
to={item.value.link}
data-active={activeIndex === item.activeIndex || undefined}
onClick={() => props.onItemSelect?.(item.value)}
>
{React.createElement(ICONS_MAPPING[item.value.type])}
<h4>
<Highlight texts={item.value.highlightTitleTexts} />
</h4>
<p>
<Highlight texts={item.value.highlightTexts} />
</p>
</Link>
</dd>
),
)}
</dl>
);
} else {
returnNode = (
<div className="dumi-default-search-empty">
<IconInbox />
<FormattedMessage id="search.not.found" />
</div>
);
}

return (
<div
className="dumi-default-search-result"
Expand All @@ -157,36 +201,7 @@ const SearchResult: FC<{
(document.activeElement as HTMLInputElement).blur();
}}
>
{Boolean(props.data.length || props.loading) ? (
<dl>
{data.map((item, i) =>
item.type === 'title' ? (
<dt key={String(i)}>{item.value.title}</dt>
) : (
<dd key={String(i)}>
<Link
to={item.value.link}
data-active={activeIndex === item.activeIndex || undefined}
onClick={() => props.onItemSelect?.(item.value)}
>
{React.createElement(ICONS_MAPPING[item.value.type])}
<h4>
<Highlight texts={item.value.highlightTitleTexts} />
</h4>
<p>
<Highlight texts={item.value.highlightTexts} />
</p>
</Link>
</dd>
),
)}
</dl>
) : (
<div className="dumi-default-search-empty">
<IconInbox />
<FormattedMessage id="search.not.found" />
</div>
)}
{returnNode}
</div>
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/features/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export default (api: IApi) => {
path: 'dumi/exports.ts',
content: `export * from '../exports';
export * from '${winPath(require.resolve('../client/theme-api'))}';
export { getRouteMetaById } from './meta/route-meta';`,
export { getRouteMetaById } from './meta/route-meta';
export { loadFilesMeta } from './meta/search';`,
});
});
};
17 changes: 8 additions & 9 deletions src/features/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,20 @@ export default (api: IApi) => {
content: 'export const components = null;',
});

// [legacy] generate meta entry
const parsedMetaFiles: MetaFiles = await api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'dumi.modifyMetaFiles',
initialValue: JSON.parse(JSON.stringify(metaFiles)),
});

// api.writeTmpFile({
// noPluginDir: true,
// path: 'dumi/meta/index.ts',
// tplPath: winPath(join(TEMPLATES_DIR, 'meta.ts.tpl')),
// context: {
// metaFiles: parsedMetaFiles,
// },
// });
api.writeTmpFile({
noPluginDir: true,
path: 'dumi/meta/search.ts',
tplPath: winPath(join(TEMPLATES_DIR, 'meta-search.ts.tpl')),
context: {
metaFiles: parsedMetaFiles,
},
});

api.writeTmpFile({
noPluginDir: true,
Expand Down
Loading

0 comments on commit d969e98

Please sign in to comment.