Show SCUM create inputs when adopting servers

This commit is contained in:
npc0-hue
2026-09-08 14:48:17 +08:00
parent fd9dbab669
commit 4f20fcaf5b
2 changed files with 102 additions and 4 deletions
@@ -74,6 +74,34 @@ describe("ServerDeploymentWorkflow", () => {
expect(Array.from(container.querySelectorAll<HTMLInputElement>('input[type="number"]')).map((input) => input.value)).toEqual(["28000", "27015"]); expect(Array.from(container.querySelectorAll<HTMLInputElement>('input[type="number"]')).map((input) => input.value)).toEqual(["28000", "27015"]);
}); });
it("shows plugin startup inputs when editing an adopted SCUM server", async () => {
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
const scumPlugin: GamePluginResponse = { ...plugin, id: "game.scum", name: "SCUM Server", serverType: "scum", serverDisplayName: "SCUM Dedicated Server", createFields: [{ key: "gamePort", label: "游戏端口", type: "port", required: true, defaultValue: "7779" }, { key: "queryPort", label: "查询端口", type: "port", required: true, defaultValue: "27015" }, { key: "maxPlayers", label: "最大玩家数", type: "number", required: true, defaultValue: "128" }] };
const initialForm = { ...defaultServerCreateForm([scumPlugin]), name: "Moon Base", deploymentMode: "existing-server" as const, createInputs: { gamePort: "28888" } };
await act(async () => {
root?.render(
<ServerDeploymentWorkflow
open
kind="edit"
plugins={[scumPlugin]}
initialForm={initialForm}
deployment={{ serverInstanceId: "server-scum", mode: "existing-server", createInputs: { gamePort: "28888" }, serverRootConfigured: true, workingDirectoryConfigured: false, installCommandConfigured: false, startCommandConfigured: false, stopCommandConfigured: false, statusCommandConfigured: false, revision: 1 }}
onClose={() => undefined}
onSubmit={async () => undefined}
/>
);
});
expect(container.textContent).toContain("游戏端口");
expect(container.textContent).toContain("查询端口");
expect(container.textContent).toContain("最大玩家数");
expect(container.textContent).toContain("接管流程不会把这些值写回或覆盖已有游戏配置");
expect(Array.from(container.querySelectorAll<HTMLInputElement>('input[type="number"]')).map((input) => input.value)).toEqual(["28888", "27015", "128"]);
});
it("normalizes the create wizard to canonical plugins with visible guided inputs", async () => { it("normalizes the create wizard to canonical plugins with visible guided inputs", async () => {
container = document.createElement("div"); container = document.createElement("div");
document.body.append(container); document.body.append(container);
@@ -191,6 +219,74 @@ describe("ServerDeploymentWorkflow", () => {
expect(submitted?.deployment && "profileKey" in submitted.deployment).toBe(false); expect(submitted?.deployment && "profileKey" in submitted.deployment).toBe(false);
expect(submitted?.deployment && "runtimeBindings" in submitted.deployment).toBe(false); expect(submitted?.deployment && "runtimeBindings" in submitted.deployment).toBe(false);
}); });
it("submits SCUM port and max player inputs from the existing-server create wizard", async () => {
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
const scumPlugin: GamePluginResponse = { ...plugin, id: "game.scum", name: "SCUM Server", serverType: "scum", serverDisplayName: "SCUM Dedicated Server", createFields: [{ key: "gamePort", label: "游戏端口", type: "port", required: true, defaultValue: "7779" }, { key: "queryPort", label: "查询端口", type: "port", required: true, defaultValue: "27015" }, { key: "maxPlayers", label: "最大玩家数", type: "number", required: true, defaultValue: "128" }] };
const initialForm = defaultServerCreateForm([scumPlugin]);
let submitted: ReturnType<typeof serverCreateRequestFromForm> | undefined;
const onSubmit = vi.fn(async (form: typeof initialForm) => {
submitted = serverCreateRequestFromForm(form, 18);
});
await act(async () => {
root?.render(
<ServerDeploymentWorkflow
open
kind="create"
plugins={[scumPlugin]}
initialForm={initialForm}
onClose={() => undefined}
onSubmit={onSubmit}
/>
);
});
const nameInput = container.querySelector<HTMLInputElement>('input[name="name"]');
if (!nameInput) throw new Error("server name input not found");
await act(async () => {
setInputValue(nameInput, "SCUM Existing Server");
});
await submitWorkflow(container);
await clickButtonByText(container, "接管已有服务器");
await submitWorkflow(container);
expect(container.textContent).toContain("已有服务器目录");
expect(container.textContent).toContain("游戏端口");
expect(container.textContent).toContain("查询端口");
expect(container.textContent).toContain("最大玩家数");
expect(Array.from(container.querySelectorAll<HTMLInputElement>('input[type="number"]')).map((input) => input.value)).toEqual(["7779", "27015", "128"]);
const rootInput = container.querySelector<HTMLInputElement>('input[name="serverRoot"]');
const [gamePortInput, , maxPlayersInput] = Array.from(container.querySelectorAll<HTMLInputElement>('input[type="number"]'));
if (!rootInput || !gamePortInput || !maxPlayersInput) throw new Error("SCUM existing-server inputs not found");
await act(async () => {
setInputValue(rootInput, "/srv/scum-existing");
setInputValue(gamePortInput, "28001");
setInputValue(maxPlayersInput, "64");
});
await submitWorkflow(container);
expect(container.textContent).toContain("本次保存创建向导配置");
expect(container.textContent).toContain("游戏配置");
expect(container.textContent).toContain("3 项已准备");
await submitWorkflow(container);
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(submitted).toMatchObject({
id: "server-scum-existing-server-18",
pluginId: "game.scum",
name: "SCUM Existing Server",
deployment: {
mode: "existing-server",
serverRoot: "/srv/scum-existing",
createInputs: { gamePort: "28001", queryPort: "27015", maxPlayers: "64" }
}
});
});
}); });
async function submitWorkflow(target: HTMLElement) { async function submitWorkflow(target: HTMLElement) {
@@ -28,6 +28,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
const selectablePlugins = useMemo(() => kind === "create" ? serverCreatePluginOptions(plugins) : plugins, [kind, plugins]); const selectablePlugins = useMemo(() => kind === "create" ? serverCreatePluginOptions(plugins) : plugins, [kind, plugins]);
const selectedPlugin = useMemo(() => selectablePlugins.find((plugin) => plugin.id === form.pluginId), [form.pluginId, selectablePlugins]); const selectedPlugin = useMemo(() => selectablePlugins.find((plugin) => plugin.id === form.pluginId), [form.pluginId, selectablePlugins]);
const pluginFields = selectedPlugin?.createFields ?? []; const pluginFields = selectedPlugin?.createFields ?? [];
const showsPluginCreateFields = form.deploymentMode === "guided-install" || form.deploymentMode === "existing-server";
const workflowSteps = kind === "create" const workflowSteps = kind === "create"
? [{ label: "基本信息", icon: Compass }, { label: "部署方式", icon: ServerCog }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }] ? [{ label: "基本信息", icon: Compass }, { label: "部署方式", icon: ServerCog }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }]
: [{ label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }]; : [{ label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }];
@@ -68,7 +69,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
if (step === configurationStep) { if (step === configurationStep) {
if (form.deploymentMode === "existing-server" && !form.serverRoot.trim() && !deployment?.serverRootConfigured) return false; if (form.deploymentMode === "existing-server" && !form.serverRoot.trim() && !deployment?.serverRootConfigured) return false;
if (form.deploymentMode === "custom-command" && !form.startCommand.trim() && !deployment?.startCommandConfigured) return false; if (form.deploymentMode === "custom-command" && !form.startCommand.trim() && !deployment?.startCommandConfigured) return false;
return form.deploymentMode !== "guided-install" || pluginFields.filter((field) => field.required).every((field) => Boolean(form.createInputs[field.key]?.trim())); return !showsPluginCreateFields || pluginFields.filter((field) => field.required).every((field) => Boolean(form.createInputs[field.key]?.trim()));
} }
return true; return true;
} }
@@ -120,7 +121,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
{form.deploymentMode === "guided-install" && <label><input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置安装目录" : "完整绝对路径"} autoComplete="off" /><small className="field-help"></small></label>} {form.deploymentMode === "guided-install" && <label><input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置安装目录" : "完整绝对路径"} autoComplete="off" /><small className="field-help"></small></label>}
{form.deploymentMode === "existing-server" && <label><input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已接管目录" : "完整绝对路径"} autoComplete="off" required={!deployment?.serverRootConfigured} /><small className="field-help">Run </small></label>} {form.deploymentMode === "existing-server" && <label><input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已接管目录" : "完整绝对路径"} autoComplete="off" required={!deployment?.serverRootConfigured} /><small className="field-help">Run </small></label>}
{form.deploymentMode === "custom-command" && <label><input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置目录" : "完整绝对路径"} autoComplete="off" /><small className="field-help"></small></label>} {form.deploymentMode === "custom-command" && <label><input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置目录" : "完整绝对路径"} autoComplete="off" /><small className="field-help"></small></label>}
{form.deploymentMode === "guided-install" && pluginFields.map((field) => ( {showsPluginCreateFields && pluginFields.map((field) => (
<label key={field.key}>{field.label}{field.required ? "(必填)" : ""}{field.type === "select" ? ( <label key={field.key}>{field.label}{field.required ? "(必填)" : ""}{field.type === "select" ? (
<select value={form.createInputs[field.key] ?? ""} onChange={(event) => updateCreateInput(field.key, event.target.value)} required={field.required}>{!field.required && <option value=""></option>}{field.options?.map((option) => <option key={option} value={option}>{option}</option>)}</select> <select value={form.createInputs[field.key] ?? ""} onChange={(event) => updateCreateInput(field.key, event.target.value)} required={field.required}>{!field.required && <option value=""></option>}{field.options?.map((option) => <option key={option} value={option}>{option}</option>)}</select>
) : ( ) : (
@@ -128,12 +129,13 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
)}</label> )}</label>
))} ))}
</div> </div>
{form.deploymentMode === "guided-install" && pluginFields.length === 0 && <div className="form-guidance"><strong></strong><span></span></div>} {showsPluginCreateFields && pluginFields.length === 0 && <div className="form-guidance"><strong></strong><span></span></div>}
{form.deploymentMode === "existing-server" && pluginFields.length > 0 && <div className="form-guidance"><strong></strong><span></span></div>}
{form.deploymentMode === "guided-install" && <GuidedInstallPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />} {form.deploymentMode === "guided-install" && <GuidedInstallPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
{form.deploymentMode === "existing-server" && <ExistingServerAdoptionPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />} {form.deploymentMode === "existing-server" && <ExistingServerAdoptionPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
{form.deploymentMode === "custom-command" && <details className="provider-advanced-settings" open><summary></summary><p className="field-help">Run </p><div className="form-grid"><label><input name="startCommand" value={form.startCommand} onChange={updateForm} placeholder={deployment?.startCommandConfigured ? "留空保持已配置启动命令" : "必填,例如 ./start-server"} autoComplete="off" required={!deployment?.startCommandConfigured} /></label><label><select name="shell" value={form.shell} onChange={updateForm}><option value=""> argv</option><option value="posix-sh">POSIX sh</option><option value="powershell">PowerShell</option><option value="cmd">Windows cmd</option></select></label><label><input name="workingDirectory" value={form.workingDirectory} onChange={updateForm} placeholder={deployment?.workingDirectoryConfigured ? "留空保持已配置执行目录" : "默认使用服务器目录"} autoComplete="off" /></label><label><input name="installCommand" value={form.installCommand} onChange={updateForm} autoComplete="off" placeholder="留空保持原值或不使用" /></label><label><input name="stopCommand" value={form.stopCommand} onChange={updateForm} autoComplete="off" /></label><label><input name="statusCommand" value={form.statusCommand} onChange={updateForm} autoComplete="off" /></label></div></details>} {form.deploymentMode === "custom-command" && <details className="provider-advanced-settings" open><summary></summary><p className="field-help">Run </p><div className="form-grid"><label><input name="startCommand" value={form.startCommand} onChange={updateForm} placeholder={deployment?.startCommandConfigured ? "留空保持已配置启动命令" : "必填,例如 ./start-server"} autoComplete="off" required={!deployment?.startCommandConfigured} /></label><label><select name="shell" value={form.shell} onChange={updateForm}><option value=""> argv</option><option value="posix-sh">POSIX sh</option><option value="powershell">PowerShell</option><option value="cmd">Windows cmd</option></select></label><label><input name="workingDirectory" value={form.workingDirectory} onChange={updateForm} placeholder={deployment?.workingDirectoryConfigured ? "留空保持已配置执行目录" : "默认使用服务器目录"} autoComplete="off" /></label><label><input name="installCommand" value={form.installCommand} onChange={updateForm} autoComplete="off" placeholder="留空保持原值或不使用" /></label><label><input name="stopCommand" value={form.stopCommand} onChange={updateForm} autoComplete="off" /></label><label><input name="statusCommand" value={form.statusCommand} onChange={updateForm} autoComplete="off" /></label></div></details>}
</div>} </div>}
{step === reviewStep && <div className="deployment-workflow-body"><div className="deployment-review"><div><span></span><strong>{pluginLabel(selectedPlugin, form.pluginId)}</strong></div>{kind === "create" && <div><span></span><strong>{form.name.trim() || "未填写"}</strong></div>}<div><span></span><strong>{form.deploymentMode === "guided-install" ? "新建并安装" : form.deploymentMode === "existing-server" ? "接管已有服务器" : "自定义启动方式"}</strong></div><div><span>{form.deploymentMode === "guided-install" ? "安装目录" : form.deploymentMode === "existing-server" ? "已有服务器目录" : "服务器目录"}</span><strong>{protectedState(form.serverRoot, Boolean(deployment?.serverRootConfigured))}</strong></div>{form.deploymentMode === "custom-command" && <><div><span></span><strong>{protectedState(form.startCommand, Boolean(deployment?.startCommandConfigured))}</strong></div><div><span></span><strong>{protectedState(form.workingDirectory, Boolean(deployment?.workingDirectoryConfigured))}</strong></div></>}{form.deploymentMode === "guided-install" && <div><span></span><strong>{Object.keys(form.createInputs).length ? `${Object.keys(form.createInputs).length} 项已准备` : "使用插件默认值"}</strong></div>}<div><span></span><strong></strong></div></div><div className="form-guidance"><strong>{kind === "create" ? "本次保存创建向导配置" : "本次只保存部署设置"}</strong><span>{kind === "create" ? "Run 会自动识别并上报心跳;部署执行按这里的方式和启动项进行。" : form.deploymentMode === "existing-server" ? "Run 将自动识别并预检现有目录;不会重装或覆盖已有游戏配置。" : "保存后由平台保留受保护部署设置;路径和命令仅在本次显式展示后可见。"}</span></div></div>} {step === reviewStep && <div className="deployment-workflow-body"><div className="deployment-review"><div><span></span><strong>{pluginLabel(selectedPlugin, form.pluginId)}</strong></div>{kind === "create" && <div><span></span><strong>{form.name.trim() || "未填写"}</strong></div>}<div><span></span><strong>{form.deploymentMode === "guided-install" ? "新建并安装" : form.deploymentMode === "existing-server" ? "接管已有服务器" : "自定义启动方式"}</strong></div><div><span>{form.deploymentMode === "guided-install" ? "安装目录" : form.deploymentMode === "existing-server" ? "已有服务器目录" : "服务器目录"}</span><strong>{protectedState(form.serverRoot, Boolean(deployment?.serverRootConfigured))}</strong></div>{form.deploymentMode === "custom-command" && <><div><span></span><strong>{protectedState(form.startCommand, Boolean(deployment?.startCommandConfigured))}</strong></div><div><span></span><strong>{protectedState(form.workingDirectory, Boolean(deployment?.workingDirectoryConfigured))}</strong></div></>}{showsPluginCreateFields && <div><span></span><strong>{Object.keys(form.createInputs).length ? `${Object.keys(form.createInputs).length} 项已准备` : "使用插件默认值"}</strong></div>}<div><span></span><strong></strong></div></div><div className="form-guidance"><strong>{kind === "create" ? "本次保存创建向导配置" : "本次只保存部署设置"}</strong><span>{kind === "create" ? "Run 会自动识别并上报心跳;部署执行按这里的方式和启动项进行。" : form.deploymentMode === "existing-server" ? "Run 将自动识别并预检现有目录;不会重装或覆盖已有游戏配置。" : "保存后由平台保留受保护部署设置;路径和命令仅在本次显式展示后可见。"}</span></div></div>}
<div className="confirm-actions"><button type="button" disabled={busy} onClick={() => step === 0 ? closeWorkflow() : setStep((current) => current - 1)}>{step === 0 ? "取消" : "上一步"}</button>{step < reviewStep ? <button type="submit" className="confirm-primary" disabled={busy || !canContinue()}><CircleDashed size={16} /><span></span></button> : <button type="submit" className="confirm-primary" disabled={busy}><Rocket size={16} /><span>{busy ? "保存中…" : actionLabel}</span></button>}</div> <div className="confirm-actions"><button type="button" disabled={busy} onClick={() => step === 0 ? closeWorkflow() : setStep((current) => current - 1)}>{step === 0 ? "取消" : "上一步"}</button>{step < reviewStep ? <button type="submit" className="confirm-primary" disabled={busy || !canContinue()}><CircleDashed size={16} /><span></span></button> : <button type="submit" className="confirm-primary" disabled={busy}><Rocket size={16} /><span>{busy ? "保存中…" : actionLabel}</span></button>}</div>
</form> </form>
</ManagementDialog>; </ManagementDialog>;