ModReleaser/src/components/VersionTable.tsx

237 lines
6.8 KiB
TypeScript

"use client";
import { useEffect, useState, useMemo, useCallback } from "react";
import { Table, Empty, Tag, Typography, Select, Flex, theme } from "antd";
import type { ParsedProject } from "@/services/project";
import { computeVersionRanges, collectAutoRange, type McVersionEntry } from "@/lib/utils/mcVersion";
import { getMcVersionUnion } from "@/services/meta";
interface ArtifactRow {
key: string;
mc_version: string;
artifact: string | null;
sources: string | null;
}
interface VersionTableProps {
project: ParsedProject | null;
loading: boolean;
onLoadingChange: (loading: boolean) => void;
}
export function VersionTable({
project,
loading,
onLoadingChange,
}: VersionTableProps) {
const { token } = theme.useToken();
// ── MC version union from both platforms ──
const [allMcVersions, setAllMcVersions] = useState<string[]>([]);
// Last version end selection (the editable dropdown value)
const [lastVersionEnd, setLastVersionEnd] = useState<string | null>(null);
// Fetch MC version union when project changes
useEffect(() => {
if (!project || project.artifacts.length === 0) {
setAllMcVersions([]);
return;
}
let cancelled = false;
onLoadingChange(true);
getMcVersionUnion()
.then((versions) => {
if (cancelled) return;
setAllMcVersions(versions);
// Default last version end to the latest available version
setLastVersionEnd(versions[versions.length - 1] ?? null);
})
.finally(() => {
if (!cancelled) {
onLoadingChange(false);
}
});
return () => {
cancelled = true;
};
}, [project, onLoadingChange]);
// ── Compute version ranges ──
const versionRanges: McVersionEntry[] | null = useMemo(() => {
if (!project || allMcVersions.length === 0) return null;
const projectVersions = project.artifacts.map((a) => a.mc_version);
// computeVersionRanges expects ascending order; project versions are descending
const ascending = [...projectVersions].reverse();
const entries = computeVersionRanges(
ascending,
allMcVersions,
lastVersionEnd ?? undefined,
);
// Reverse back to match descending display order
return [...entries].reverse();
}, [project, allMcVersions, lastVersionEnd]);
// ── Dropdown options: 同系列中 >= 最后一个项目版本的版本 ──
const lastVersionDropdownOptions = useMemo(() => {
if (!project || allMcVersions.length === 0) return [];
// Descending display, index 0 is the largest (= last in compatibility)
const lastVer = project.artifacts[0]?.mc_version;
if (!lastVer) return [];
const series = collectAutoRange(lastVer, allMcVersions);
return series
.filter(
(v) =>
v.localeCompare(lastVer, undefined, { numeric: true }) >= 0,
)
.map((v) => ({ value: v, label: v }));
}, [project, allMcVersions]);
// Reset lastVersionEnd to latest in the same series
useEffect(() => {
if (allMcVersions.length === 0 || !project) return;
const lastVer = project.artifacts[0]?.mc_version;
if (!lastVer) return;
const series = collectAutoRange(lastVer, allMcVersions);
setLastVersionEnd(series[series.length - 1] ?? null);
}, [allMcVersions, project]);
// ── Table data ──
const dataSource: (ArtifactRow & { rangeEntry?: McVersionEntry })[] =
useMemo(() => {
if (!project) return [];
return project.artifacts.map((a, i) => ({
key: a.mc_version,
mc_version: a.mc_version,
artifact: a.artifact,
sources: a.sources,
rangeEntry: versionRanges?.[i],
}));
}, [project, versionRanges]);
// ── Dropdown change handler ──
const handleLastVersionEndChange = useCallback((value: string) => {
setLastVersionEnd(value);
}, []);
// ── Columns ──
const columns = useMemo(
() => [
{
title: "MC 版本",
dataIndex: "mc_version",
key: "mc_version",
width: 120,
render: (val: string) => <Tag>{val}</Tag>,
},
{
title: "兼容范围",
key: "range",
width: 280,
render: (
_: unknown,
record: ArtifactRow & { rangeEntry?: McVersionEntry },
index: number,
) => {
const entry = record.rangeEntry;
if (!entry) {
return (
<Typography.Text type="secondary">
</Typography.Text>
);
}
// Descending order: index 0 is the largest (= last in compatibility terms)
const isLast = index === 0;
if (!isLast) {
// Non-last: show wildcard if it differs, otherwise explicit
return <Tag color="blue">{entry.wildcard}</Tag>;
}
// Last version: range + editable end-version dropdown
return (
<Flex align="center" gap={6}>
<Tag color="blue">{entry.wildcard}</Tag>
<Typography.Text
type="secondary"
style={{ fontSize: token.fontSizeSM, whiteSpace: "nowrap" }}
>
</Typography.Text>
<Select
size="small"
style={{ width: 110 }}
showSearch
optionFilterProp="label"
value={lastVersionEnd ?? undefined}
onChange={handleLastVersionEndChange}
options={lastVersionDropdownOptions}
/>
</Flex>
);
},
},
{
title: "构建产物",
dataIndex: "artifact",
key: "artifact",
ellipsis: true,
render: (val: string | null) =>
val ? (
<Typography.Text>{val}</Typography.Text>
) : (
<Typography.Text type="danger"></Typography.Text>
),
},
{
title: "源码",
dataIndex: "sources",
key: "sources",
ellipsis: true,
render: (val: string | null) =>
val ? (
<Typography.Text>{val}</Typography.Text>
) : (
<Typography.Text type="danger"></Typography.Text>
),
},
],
[lastVersionEnd, lastVersionDropdownOptions, handleLastVersionEndChange],
);
// ── Empty state ──
if (!project) {
return (
<Empty
description="请选择一个配置以查看版本信息"
style={{ padding: 40 }}
/>
);
}
return (
<Table<ArtifactRow & { rangeEntry?: McVersionEntry }>
dataSource={dataSource}
columns={columns}
loading={loading}
pagination={false}
size="middle"
bordered
locale={{
emptyText: "未找到构建产物",
}}
/>
);
}