Refactor ConfigModal to load metadata independently per select

This commit is contained in:
CPTProgrammer 2026-07-11 12:58:35 +08:00
parent 8d23ae1823
commit a4686c09b5
No known key found for this signature in database

View File

@ -11,7 +11,6 @@ import {
Button,
Space,
App,
Spin,
} from "antd";
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
import { useDebouncedCallback } from "use-debounce";
@ -42,10 +41,103 @@ const RELATION_TYPES = [
{ label: "Tool", value: "tool" },
];
// ---------------------------------------------------------------------------
// Data-fetching hooks — each Select loads independently, no blocking
// ---------------------------------------------------------------------------
function useModrinthLoaders(): {
loading: boolean;
options: { label: string; value: string }[];
} {
const [loading, setLoading] = useState(true);
const [options, setOptions] = useState<{ label: string; value: string }[]>(
[],
);
useEffect(() => {
let cancelled = false;
setLoading(true);
getModrinthLoaders()
.then((list) => {
if (!cancelled) {
setOptions(list.map((l) => ({ label: l, value: l })));
}
})
.catch(() => {})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return { loading, options };
}
function useModrinthEnvironments(): {
loading: boolean;
options: { value: string; label: string }[];
} {
const [loading, setLoading] = useState(true);
const [options, setOptions] = useState<
{ value: string; label: string }[]
>([]);
useEffect(() => {
let cancelled = false;
setLoading(true);
getModrinthEnvironments()
.then((list) => {
if (!cancelled) setOptions(list);
})
.catch(() => {})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return { loading, options };
}
function useCurseforgeMeta(): {
loading: boolean;
environments: string[];
loaders: string[];
} {
const [loading, setLoading] = useState(true);
const [environments, setEnvironments] = useState<string[]>([]);
const [loaders, setLoaders] = useState<string[]>([]);
useEffect(() => {
let cancelled = false;
setLoading(true);
getCurseForgeMeta()
.then((meta) => {
if (!cancelled) {
setEnvironments(meta.environments);
setLoaders(meta.loaders);
}
})
.catch(() => {})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return { loading, environments, loaders };
}
type ConfigValues = Config;
// ---------------------------------------------------------------------------
// Outer component — Modal shell, meta loading, state
// Component
// ---------------------------------------------------------------------------
interface ConfigModalProps {
@ -68,40 +160,6 @@ export function ConfigModal({
const { message } = App.useApp();
const [saving, setSaving] = useState(false);
// --------------- Metadata ---------------
const [metaLoading, setMetaLoading] = useState(false);
const [meta, setMeta] = useState<{
modrinthLoaders: string[];
modrinthEnvironments: { value: string; label: string }[];
curseforgeEnvs: string[];
curseforgeLoaders: string[];
} | null>(null);
const loadMeta = useCallback(async () => {
setMetaLoading(true);
try {
const [loaders, envs, cfMeta] = await Promise.all([
getModrinthLoaders(),
getModrinthEnvironments(),
getCurseForgeMeta(),
]);
setMeta({
modrinthLoaders: loaders,
modrinthEnvironments: envs,
curseforgeEnvs: cfMeta.environments,
curseforgeLoaders: cfMeta.loaders,
});
} catch {
message.warning("部分元数据加载失败,手动输入仍然可用");
} finally {
setMetaLoading(false);
}
}, []);
useEffect(() => {
if (open) loadMeta();
}, [open, loadMeta]);
// --------------- Save ---------------
const handleSave = useCallback(
@ -138,7 +196,7 @@ export function ConfigModal({
setSaving(false);
}
},
[mode, initialName, onSaved],
[mode, initialName, onSaved, message],
);
// --------------- Debounced project name lookup ---------------
@ -180,7 +238,6 @@ export function ConfigModal({
type="primary"
loading={saving}
onClick={() => {
// Trigger form submit via a hidden button inside the form
document
.getElementById("config-modal-submit")
?.click();
@ -191,41 +248,26 @@ export function ConfigModal({
</Space>
}
>
{metaLoading ? (
<div style={{ textAlign: "center", padding: 40 }}>
<Spin description="正在加载元数据…" />
</div>
) : meta ? (
<ConfigModalForm
mode={mode}
initialName={initialName}
initialConfig={initialConfig}
meta={meta}
modrinthProjectName={modrinthProjectName}
curseforgeProjectName={curseforgeProjectName}
lookupModrinthProject={lookupModrinthProject}
lookupCurseforgeProject={lookupCurseforgeProject}
onSave={handleSave}
/>
) : null}
</Modal>
);
}
// ---------------------------------------------------------------------------
// Inner component — Form (only rendered when meta is loaded)
// Inner component — Form with independently-loading Selects
// ---------------------------------------------------------------------------
interface ConfigModalFormProps {
mode: "add" | "edit";
initialName: string | null;
initialConfig: Config | null;
meta: {
modrinthLoaders: string[];
modrinthEnvironments: { value: string; label: string }[];
curseforgeEnvs: string[];
curseforgeLoaders: string[];
};
modrinthProjectName: string | null;
curseforgeProjectName: string | null;
lookupModrinthProject: (projectId: string) => void;
@ -236,7 +278,6 @@ interface ConfigModalFormProps {
function ConfigModalForm({
mode,
initialConfig,
meta,
modrinthProjectName,
curseforgeProjectName,
lookupModrinthProject,
@ -245,6 +286,11 @@ function ConfigModalForm({
}: ConfigModalFormProps) {
const [form] = Form.useForm<ConfigValues>();
// Each Select loads its own data independently
const mrLoaders = useModrinthLoaders();
const mrEnvs = useModrinthEnvironments();
const cfMeta = useCurseforgeMeta();
// Reset form when modal opens or mode/config changes
useEffect(() => {
if (mode === "edit" && initialConfig) {
@ -259,7 +305,8 @@ function ConfigModalForm({
source_filename_format: "",
modrinth: {
project_id: "",
version_name: "ModName v${version} for Minecraft ${mc_version_range}",
version_name:
"ModName v${version} for Minecraft ${mc_version_range}",
version: "${version}-mc${mc_version}",
loaders: [],
environment: "client_and_server",
@ -277,15 +324,13 @@ function ConfigModalForm({
}, [mode, initialConfig, form]);
const handleFinish = useCallback(
async (values: ConfigValues) => {
(values: ConfigValues) => {
onSave(values);
},
[onSave],
);
const handleFinishFailed = useCallback(() => {
// Ant Design shows inline validation errors
}, []);
const handleFinishFailed = useCallback(() => {}, []);
return (
<Form
@ -295,8 +340,11 @@ function ConfigModalForm({
onFinishFailed={handleFinishFailed}
style={{ maxHeight: "60vh", overflow: "auto", paddingRight: 8 }}
>
{/* Hidden submit button triggered by modal footer */}
<button id="config-modal-submit" type="submit" style={{ display: "none" }} />
<button
id="config-modal-submit"
type="submit"
style={{ display: "none" }}
/>
<Tabs
items={[
@ -317,7 +365,9 @@ function ConfigModalForm({
<Form.Item
name="project_dir"
label="项目目录"
rules={[{ required: true, message: "请输入项目目录绝对路径" }]}
rules={[
{ required: true, message: "请输入项目目录绝对路径" },
]}
>
<Input placeholder="/path/to/project" />
</Form.Item>
@ -352,7 +402,7 @@ function ConfigModalForm({
rules={[{ required: true, message: "请输入文件名模板" }]}
extra="支持 ${version} 和 ${mc_version} 占位符"
>
<Input placeholder='modname-${version}-mc${mc_version}.jar' />
<Input placeholder="modname-${version}-mc${mc_version}.jar" />
</Form.Item>
<Form.Item
@ -361,7 +411,7 @@ function ConfigModalForm({
rules={[{ required: true }]}
extra="支持 ${version} 和 ${mc_version} 占位符"
>
<Input placeholder='modname-${version}-mc${mc_version}-sources.jar' />
<Input placeholder="modname-${version}-mc${mc_version}-sources.jar" />
</Form.Item>
</>
),
@ -376,7 +426,9 @@ function ConfigModalForm({
<Form.Item
name={["modrinth", "project_id"]}
label="项目 ID"
rules={[{ required: true, message: "请输入 Modrinth 项目 ID" }]}
rules={[
{ required: true, message: "请输入 Modrinth 项目 ID" },
]}
extra={
modrinthProjectName
? `项目名称: ${modrinthProjectName}`
@ -415,10 +467,11 @@ function ConfigModalForm({
<Select
mode="multiple"
placeholder="选择加载器…"
options={meta.modrinthLoaders.map((l) => ({
label: l,
value: l,
}))}
loading={mrLoaders.loading}
options={mrLoaders.options}
notFoundContent={
mrLoaders.loading ? "加载中…" : "无可用选项"
}
/>
</Form.Item>
@ -429,7 +482,11 @@ function ConfigModalForm({
>
<Select
placeholder="选择运行环境"
options={meta.modrinthEnvironments}
loading={mrEnvs.loading}
options={mrEnvs.options}
notFoundContent={
mrEnvs.loading ? "加载中…" : "无可用选项"
}
/>
</Form.Item>
@ -462,7 +519,10 @@ function ConfigModalForm({
rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0 }}
>
<Input placeholder="项目 ID" style={{ width: 200 }} />
<Input
placeholder="项目 ID"
style={{ width: 200 }}
/>
</Form.Item>
<DeleteOutlined onClick={() => remove(name)} />
</Space>
@ -470,7 +530,10 @@ function ConfigModalForm({
<Button
type="dashed"
onClick={() =>
add({ dependency_type: "required", project_id: "" })
add({
dependency_type: "required",
project_id: "",
})
}
icon={<PlusOutlined />}
block
@ -494,7 +557,12 @@ function ConfigModalForm({
<Form.Item
name={["curseforge", "project_id"]}
label="项目 ID"
rules={[{ required: true, message: "请输入 CurseForge 项目 ID" }]}
rules={[
{
required: true,
message: "请输入 CurseForge 项目 ID",
},
]}
extra={
curseforgeProjectName
? `项目名称: ${curseforgeProjectName}`
@ -529,10 +597,14 @@ function ConfigModalForm({
<Select
mode="multiple"
placeholder="选择运行环境…"
options={meta.curseforgeEnvs.map((e) => ({
loading={cfMeta.loading}
options={cfMeta.environments.map((e) => ({
label: e,
value: e,
}))}
notFoundContent={
cfMeta.loading ? "加载中…" : "无可用选项"
}
/>
</Form.Item>
@ -544,16 +616,22 @@ function ConfigModalForm({
<Select
mode="multiple"
placeholder="选择加载器…"
options={meta.curseforgeLoaders.map((l) => ({
loading={cfMeta.loading}
options={cfMeta.loaders.map((l) => ({
label: l,
value: l,
}))}
notFoundContent={
cfMeta.loading ? "加载中…" : "无可用选项"
}
/>
</Form.Item>
{/* Relations */}
<Form.Item label="关联项目">
<Form.List name={["curseforge", "relations", "projects"]}>
<Form.List
name={["curseforge", "relations", "projects"]}
>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...rest }) => (
@ -568,7 +646,10 @@ function ConfigModalForm({
rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0 }}
>
<Input placeholder="Slug" style={{ width: 160 }} />
<Input
placeholder="Slug"
style={{ width: 160 }}
/>
</Form.Item>
<Form.Item
{...rest}
@ -579,10 +660,12 @@ function ConfigModalForm({
<Select
placeholder="类型"
options={RELATION_TYPES}
style={{ width: 170 }}
style={{ width: 200 }}
/>
</Form.Item>
<DeleteOutlined onClick={() => remove(name)} />
<DeleteOutlined
onClick={() => remove(name)}
/>
</Space>
))}
<Button