Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Make searchable again #1898

Merged
merged 4 commits into from
Sep 15, 2023
Merged
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: 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>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在 exports.ts 里导出以后应该也是能拿到类型的?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是兜 client 的,打包出的 dist 会跟着 exports 走。

/** @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: [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAllRoutesMeta 的返回值里不包含 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 = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个兜底逻辑在 meta runtime 插件里有的,是不是可以不用做兜底了:https://github.com/umijs/dumi/blob/feature/2.3.0/src/templates/meta-runtime.ts.tpl#L31-L35

以及这里要遍历的应该是 mergedRoutes[routeId].meta.tabs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runtime 里这个已经是空的 tabs 了。这个逻辑其实就是把 runtime 里的搬过来,我个人理解 runtime 那个部分可以删了

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

之后再起个新的 PR 把 runtime 清理掉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runtime 删了会影响页面 Tab 展示的,因为 useRouteMeta 必须得返回 tabs 数据,页面上才有数据源展示 Tab

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
Loading