Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
150 changes: 38 additions & 112 deletions src/components/NPMVersionSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,31 @@
'use client';
import { isEmpty } from 'lodash';
import { Cascader, Select, Space } from 'antd';
import { Select, Space } from 'antd';
import React from 'react';

import semver from 'semver';

interface VersionNode {
label: string;
value: string;
children: VersionNode[];
}

function sortNodes(nodes: VersionNode[]): void {
nodes.sort((a, b) => {
if ([a.value, b.value].every(v => semver.clean(v) === v)) {
return semver.rcompare(a.value, b.value);
// Simple version sorting - compare by semver descending
function sortVersions(versions: string[]): string[] {
return [...versions].sort((a, b) => {
const aClean = semver.clean(a);
const bClean = semver.clean(b);
if (aClean && bClean) {
return semver.rcompare(aClean, bClean);
}
const aVersion = semver.coerce(a.value);
const bVersion = semver.coerce(b.value);
if (!aVersion || !bVersion) {
return 0;
const aVersion = semver.coerce(a);
const bVersion = semver.coerce(b);
if (aVersion && bVersion) {
return semver.rcompare(aVersion, bVersion);
}
return semver.rcompare(aVersion, bVersion);
return 0;
});
for (const node of nodes) {
sortNodes(node.children);
}
}

function classifyVersions(versions: string[]): VersionNode[] {
const root: VersionNode = { label: '', value: '', children: [] };
const map: Map<string, VersionNode> = new Map();

for (const version of versions) {
const parsed = semver.parse(version);
if (!parsed) {
continue;
}

const major = `${parsed.major}.x`;
const minor = `${parsed.major}.${parsed.minor}.x`;
const patch = parsed.version;

let majorNode = map.get(major);
if (!majorNode) {
majorNode = { label: major, value: major, children: [] };
map.set(major, majorNode);
root.children.push(majorNode);
}

let minorNode = map.get(minor);
if (!minorNode) {
minorNode = { label: minor, value: minor, children: [] };
map.set(minor, minorNode);
majorNode.children.push(minorNode);
}

let patchNode = map.get(patch);
if (!patchNode) {
patchNode = { label: patch, value: version, children: [] };
map.set(patch, patchNode);
minorNode.children.push(patchNode);
}
}

sortNodes(root.children);

// 如果只有 0.0.x 的版本
// 就直接返回 children 不用额外返回 0.x 了
// 0.x
if (root.children.length === 1) {
// 0.0.x
const childrenNode = root.children[0].children;
if (childrenNode.length === 1) {
return childrenNode;
}
}
return root.children;
// Build flat version options for Select with virtual scrolling
function buildVersionOptions(versions: string[]): { label: string; value: string }[] {
const sorted = sortVersions(versions);
return sorted.map(v => ({ label: v, value: v }));
}

interface VersionSelectProps {
Expand All @@ -92,39 +41,15 @@ export default function NPMVersionSelect({
tags = {},
setVersionStr,
}: VersionSelectProps) {
// Update tag in select label
const selectVersionRender = React.useMemo(() => {
const version = targetVersion;
return <Space>{version}</Space>;
}, [targetVersion]);

const targetOptions = React.useMemo(() => {
return classifyVersions(versions);
// Memoize version options - flat list for virtual scrolling
const versionOptions = React.useMemo(() => {
return buildVersionOptions(versions);
}, [versions]);

const targetValue = React.useMemo(() => {
let value: string[] = [];

function findNode(nodes: VersionNode[] | undefined, prefixs: string[]) {
if (isEmpty(nodes) || !nodes) {
return;
}

nodes.some((item) => {
if (item.value === targetVersion) {
value = [...prefixs, targetVersion];
return true;
}
if (item.children) {
return findNode(item.children, [...prefixs, item.value]);
}
return false;
});
}

findNode(targetOptions, []);
return value;
}, [targetOptions, targetVersion]);
// Memoize tag options
const tagOptions = React.useMemo(() => {
return Object.keys(tags || {}).map((t) => ({ label: t, value: tags[t] }));
}, [tags]);

const hasTag = React.useMemo(() => {
if (!targetVersion) {
Expand All @@ -133,31 +58,32 @@ export default function NPMVersionSelect({
return Object.values(tags).includes(targetVersion);
}, [tags, targetVersion]);

if (isEmpty(targetOptions) || !targetVersion) {
if (isEmpty(versionOptions) || !targetVersion) {
return null;
}

// ========== render ==========
if (isEmpty(targetOptions)) return null

return (
<>
<Cascader
<Select
size="small"
options={targetOptions}
value={targetValue}
displayRender={() => selectVersionRender}
options={versionOptions}
value={targetVersion}
allowClear={false}
style={{ width: 'unset' }}
style={{ width: 'auto', minWidth: 100 }}
showSearch
placement={'bottomRight'}
virtual
listHeight={300}
popupMatchSelectWidth={false}
onChange={(val) => {
setVersionStr(val?.pop()?.toString() || '');
setVersionStr(val);
}}
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
<Select
size="small"
options={Object.keys(tags || {}).map((t) => ({ label: t, value: tags[t] }))}
options={tagOptions}
placeholder="选择 Tag"
value={hasTag ? targetVersion : undefined}
popupMatchSelectWidth={180}
Expand All @@ -167,5 +93,5 @@ export default function NPMVersionSelect({
showSearch
/>
</>
)
);
}
139 changes: 84 additions & 55 deletions src/slugs/versions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useVersionTags,
} from '@/hooks/useManifest';

import { Card, Result, Segmented, Space, Tag, Tooltip, Typography } from 'antd';
import { Card, Pagination, Result, Segmented, Space, Tag, Tooltip, Typography } from 'antd';
import dayjs from 'dayjs';
import React from 'react';
import semver from 'semver';
Expand Down Expand Up @@ -50,6 +50,14 @@ function TagsList({ tagsInfo, pkg }: { tagsInfo: Record<string, string[]>; pkg:
const { styles } = useStyles();
const [type, setTags] = useQueryState('tags', 'prod');
const onlyProd = type === 'prod';

// Memoize filtered tags
const filteredTags = React.useMemo(() => {
return Object.keys(tagsInfo || {}).filter(
(item) => !onlyProd || !semver.parse(item)?.prerelease.length
);
}, [tagsInfo, onlyProd]);

return (
<div style={{ position: 'relative' }}>
<Typography.Title
Expand Down Expand Up @@ -85,31 +93,48 @@ function TagsList({ tagsInfo, pkg }: { tagsInfo: Record<string, string[]>; pkg:
<span className={styles.dot}></span>
<span>Tag</span>
</li>
{Object.keys(tagsInfo || {}).map((item) => {
if (onlyProd && semver.parse(item)?.prerelease.length) {
return null;
}
return (
<li className={styles.versionsItem} key={item}>
<span>
<Link prefetch={false} href={`/package/${pkg!.name}/home?version=${item}`}>
{item}
</Link>
</span>
<span className={styles.dot}></span>
<VersionTags tags={tagsInfo[item]} max={8}></VersionTags>
</li>
);
})}
{filteredTags.map((item) => (
<li className={styles.versionsItem} key={item}>
<span>
<Link prefetch={false} href={`/package/${pkg!.name}/home?version=${item}`}>
{item}
</Link>
</span>
<span className={styles.dot}></span>
<VersionTags tags={tagsInfo[item]} max={8}></VersionTags>
</li>
))}
</ul>
</div>
);
}

const PAGE_SIZE = 50;

function VersionsList({ versions, pkg }: { versions: NpmPackageVersion[]; pkg: PackageManifest }) {
const { styles } = useStyles();
const [type, setVersions] = useQueryState('versions', 'prod');
const [page, setPage] = React.useState(1);
const onlyProd = type === 'prod';

// Memoize filtered and sorted versions
const filteredVersions = React.useMemo(() => {
return versions
.filter((item) => !onlyProd || !semver.parse(item.version)?.prerelease.length)
.sort((a, b) => (dayjs(a.publish_time).isAfter(b.publish_time) ? -1 : 1));
}, [versions, onlyProd]);

// Reset page when filter changes
React.useEffect(() => {
setPage(1);
}, [onlyProd]);

// Get current page items
const pageItems = React.useMemo(() => {
const start = (page - 1) * PAGE_SIZE;
return filteredVersions.slice(start, start + PAGE_SIZE);
}, [filteredVersions, page]);

return (
<div style={{ position: 'relative' }}>
<Typography.Title
Expand Down Expand Up @@ -145,46 +170,50 @@ function VersionsList({ versions, pkg }: { versions: NpmPackageVersion[]; pkg: P
<span className={styles.dot}></span>
<span>发布信息</span>
</li>
{versions
.sort((a, b) => (dayjs(a.publish_time).isAfter(b.publish_time) ? -1 : 1))
?.map((item) => {
if (onlyProd && semver.parse(item.version)?.prerelease.length) {
return null;
}

return (
<li className={styles.versionsItem} key={item.version}>
<Typography.Text delete={!!item.deprecated}>
<Link
prefetch={false}
title={item.deprecated}
shallow
href={`/package/${pkg!.name}?version=${item.version}`}
>
{item.version}
</Link>
</Typography.Text>
<span className={styles.dot}></span>
<Typography.Text type="secondary">
<Space size="small">
{item._npmUser?.name ? (
<>
<span>由</span>
<Tooltip title={item._npmUser.name.replace('buc:', '')}>
<Gravatar email={item._npmUser.email} name={item._npmUser.name} />
</Tooltip>
<span>发布于</span>
</>
) : null}
<Tooltip title={dayjs(item.publish_time).format('YYYY-MM-DD HH:mm:ss Z')}>
{dayjs(item.publish_time).format('YYYY-MM-DD')}
{pageItems.map((item) => (
<li className={styles.versionsItem} key={item.version}>
<Typography.Text delete={!!item.deprecated}>
<Link
prefetch={false}
title={item.deprecated}
shallow
href={`/package/${pkg!.name}?version=${item.version}`}
>
{item.version}
</Link>
</Typography.Text>
<span className={styles.dot}></span>
<Typography.Text type="secondary">
<Space size="small">
{item._npmUser?.name ? (
<>
<span>由</span>
<Tooltip title={item._npmUser.name.replace('buc:', '')}>
<Gravatar email={item._npmUser.email} name={item._npmUser.name} />
</Tooltip>
</Space>
</Typography.Text>
</li>
);
})}
<span>发布于</span>
</>
) : null}
<Tooltip title={dayjs(item.publish_time).format('YYYY-MM-DD HH:mm:ss Z')}>
{dayjs(item.publish_time).format('YYYY-MM-DD')}
</Tooltip>
</Space>
</Typography.Text>
</li>
))}
</ul>
{filteredVersions.length > PAGE_SIZE && (
<div style={{ display: 'flex', justifyContent: 'center', marginTop: 16 }}>
<Pagination
current={page}
pageSize={PAGE_SIZE}
total={filteredVersions.length}
onChange={setPage}
showSizeChanger={false}
showTotal={(total) => `共 ${total} 个版本`}
/>
</div>
)}
</div>
);
}
Expand Down