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