import React, { cloneElement, isValidElement } from 'react'; import { BugOutlined } from '@ant-design/icons'; import { Drawer, Flex, Grid, Popover, Tag, Timeline, Typography } from 'antd'; import type { TimelineItemProps } from 'antd'; import { createStyles } from 'antd-style'; import semver from 'semver'; import deprecatedVersions from '../../../../BUG_VERSIONS.json'; import useFetch from '../../../hooks/useFetch'; import useLocale from '../../../hooks/useLocale'; import useLocation from '../../../hooks/useLocation'; import Link from '../Link'; interface MatchDeprecatedResult { match?: string; reason: string[]; } interface ChangelogInfo { version: string; changelog: string; refs: string[]; releaseDate: string; } function matchDeprecated(v: string): MatchDeprecatedResult { const match = Object.keys(deprecatedVersions).find((depreciated) => semver.satisfies(v, depreciated), ); const reason = deprecatedVersions[match as keyof typeof deprecatedVersions] || []; return { match, reason: Array.isArray(reason) ? reason : [reason], }; } const useStyle = createStyles(({ token, css }) => ({ listWrap: css` > li { line-height: 2; } `, linkRef: css` margin-inline-start: ${token.marginXS}px; `, bug: css` font-size: ${token.fontSize}px; color: #aaa; margin-inline-start: ${token.marginXS}px; display: inline-block; vertical-align: inherit; cursor: pointer; &:hover { color: #333; } `, bugReasonTitle: css` padding: ${token.paddingXXS}px ${token.paddingXS}px; `, bugReasonList: css` width: 100%; max-width: 100%; li { padding: ${token.paddingXXS}px ${token.paddingXS}px; a { display: flex; align-items: center; gap: ${token.marginXXS}px; } } `, extraLink: css` font-size: ${token.fontSize}px; `, drawerContent: { position: 'relative', [`> ${token.antCls}-drawer-body`]: { scrollbarWidth: 'thin', scrollbarColor: 'unset', }, }, versionWrap: css` margin-bottom: 1em; `, versionTitle: css` margin: 0 !important; `, versionTag: css` user-select: none; display: inline-flex; align-items: center; justify-content: center; &:last-child { margin-inline-end: 0; } `, })); const locales = { cn: { full: '查看完整日志', changelog: '更新日志', loading: '加载中...', empty: '暂无更新', bugList: 'Bug 版本', }, en: { full: 'Full Changelog', changelog: 'Changelog', loading: 'loading...', empty: 'Nothing update', bugList: 'Bug Versions', }, }; const ParseChangelog: React.FC<{ changelog: string }> = (props) => { const { changelog = '' } = props; const parsedChangelog = React.useMemo(() => { const nodes: React.ReactNode[] = []; let isQuota = false; let isBold = false; let lastStr = ''; for (let i = 0; i < changelog.length; i += 1) { const char = changelog[i]; if (char !== '`' && char !== '*') { lastStr += char; } else { let node: React.ReactNode = lastStr; if (isQuota) { node = {node}; } else if (isBold) { node = {node}; } nodes.push(node); lastStr = ''; if (char === '`') { isQuota = !isQuota; } else if (char === '*' && changelog[i + 1] === '*') { isBold = !isBold; i += 1; // Skip the next '*' } } } nodes.push(lastStr); return nodes; }, [changelog]); return {parsedChangelog}; }; const RenderChangelogList: React.FC<{ changelogList: ChangelogInfo[] }> = ({ changelogList }) => { const elements: React.ReactNode[] = []; const { styles } = useStyle(); const len = changelogList.length; for (let i = 0; i < len; i += 1) { const { refs, changelog } = changelogList[i]; // Check if the next line is an image link and append it to the current line if (i + 1 < len && changelogList[i + 1].changelog.trim().startsWith('('img'); elements.push(
  • {refs?.map((ref) => ( #{ref.match(/^.*\/(\d+)$/)?.[1]} ))}
    {imgElement?.getAttribute('alt')
  • , ); i += 1; // Skip the next line } else { elements.push(
  • , ); } } return ; }; const useChangelog = (componentPath: string, lang: 'cn' | 'en'): ChangelogInfo[] => { const logFileName = `components-changelog-${lang}.json`; const data = useFetch({ key: `component-changelog-${lang}`, request: () => import(`../../../preset/${logFileName}`), }); return React.useMemo(() => { const component = componentPath.replace(/-/g, ''); const componentName = Object.keys(data).find( (name) => name.toLowerCase() === component.toLowerCase(), ); return data[componentName as keyof typeof data] as ChangelogInfo[]; }, [data, componentPath]); }; const ComponentChangelog: React.FC> = (props) => { const { children } = props; const [locale, lang] = useLocale(locales); const [show, setShow] = React.useState(false); const { pathname } = useLocation(); const { styles } = useStyle(); const componentPath = pathname.match(/\/components\/([^/]+)/)?.[1] || ''; const list = useChangelog(componentPath, lang); const timelineItems = React.useMemo(() => { const changelogMap: Record = {}; list?.forEach((info) => { changelogMap[info.version] = changelogMap[info.version] || []; changelogMap[info.version].push(info); }); return Object.keys(changelogMap).map((version) => { const changelogList = changelogMap[version]; const bugVersionInfo = matchDeprecated(version); return { children: ( {version} {bugVersionInfo.match && ( {locale.bugList}} content={ } > )} {changelogList[0]?.releaseDate} ), }; }); }, [list]); const screens = Grid.useBreakpoint(); const width = screens.md ? '48vw' : '90vw'; if (!pathname.startsWith('/components/') || !list || !list.length) { return null; } return ( <> {isValidElement(children) && cloneElement(children as React.ReactElement, { onClick: () => setShow(true), })} {locale.full} } open={show} width={width} onClose={() => setShow(false)} > ); }; export default ComponentChangelog;