Refactor ConfigModal to load metadata independently per select
This commit is contained in:
parent
8d23ae1823
commit
a4686c09b5
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user