From 16a249d5bf9f5b287cabbd76ca459dfd5331ef7c Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:32:16 +0100 Subject: [PATCH] Have version selectors share a common base component --- packages/web_ui/src/bootstrap.tsx | 4 +- .../src/components/InputPartialVersion.tsx | 122 ------------------ ...nputTargetVersion.tsx => InputVersion.tsx} | 66 +++++++++- .../web_ui/src/components/ModPackViewPage.tsx | 2 +- packages/web_ui/src/components/ModsPage.tsx | 2 +- 5 files changed, 64 insertions(+), 132 deletions(-) delete mode 100644 packages/web_ui/src/components/InputPartialVersion.tsx rename packages/web_ui/src/components/{InputTargetVersion.tsx => InputVersion.tsx} (68%) diff --git a/packages/web_ui/src/bootstrap.tsx b/packages/web_ui/src/bootstrap.tsx index e1b6b1c3..b4954413 100644 --- a/packages/web_ui/src/bootstrap.tsx +++ b/packages/web_ui/src/bootstrap.tsx @@ -9,8 +9,7 @@ import App from "./components/App"; import BaseWebPlugin, { InputComponent } from "./BaseWebPlugin"; import InputRole from "./components/InputRole"; import InputModPack from "./components/InputModPack"; -import InputTargetVersion from "./components/InputTargetVersion"; -import InputPartialVersion from "./components/InputPartialVersion"; +import { InputTargetVersion, InputPartialVersion, InputFullVersion } from "./components/InputVersion"; import { Control, ControlConnector } from "./util/websocket"; const { ConsoleTransport, WebConsoleFormat, logger } = lib; @@ -111,6 +110,7 @@ async function loadPlugins(pluginInfos: lib.PluginWebpackEnvInfo[], control: Con function inputComponentsFromPlugins(plugins: Map) { const inputComponents: Record = { + "full_version": InputFullVersion, "partial_version": InputPartialVersion, "target_version": InputTargetVersion, "mod_pack": InputModPack, diff --git a/packages/web_ui/src/components/InputPartialVersion.tsx b/packages/web_ui/src/components/InputPartialVersion.tsx deleted file mode 100644 index 022fedaa..00000000 --- a/packages/web_ui/src/components/InputPartialVersion.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useContext, useEffect, useState, useRef } from "react"; -import { Button, Divider, Input, InputRef, RefSelectProps, TreeSelect } from "antd"; - -import * as lib from "@clusterio/lib"; -import { InputComponentProps } from "../BaseWebPlugin"; -import { useAccount } from "../model/account"; -import ControlContext from "./ControlContext"; - -const defaultFieldDefinition: lib.FieldDefinition = { type: "string", optional: false }; - -export default function InputPartialVersion ( - props: Omit & { - fieldDefinition?: lib.FieldDefinition; - className?: string; - } -) { - const fieldDefinition = props.fieldDefinition || defaultFieldDefinition; - const [versions, setVersions] = useState(lib.ApiVersions); - const [customVersion, setCustomVersion] = useState(""); - const [open, setOpen] = useState(false); - const inputRef = useRef(null); - const selectRef = useRef(null); - const control = useContext(ControlContext); - const account = useAccount(); - - useEffect(() => { - (async () => { - if (account.hasPermission("core.external.get_factorio_versions")) { - const res = await control.factorioVersions.get(5 * 60 * 1000); - setVersions(res.map(v => v.version)); - } - })(); - }, [control]); - - // Split the versions into groups based on major minor - const groups = new Map(); - - for (const version of versions) { - const [major, minor] = version.split("."); - const key = `${major}.${minor}` as lib.PartialVersion; - - if (!groups.has(key)) { - groups.set(key, []); - } - groups.get(key)!.push(version); - } - - // Construct the tree to be disabled - const tree = [...groups.entries()].map(([majorMinor, patchVersions]) => ( - { - title: majorMinor, - value: majorMinor, - key: majorMinor, - children: patchVersions.map((v) => ({ - title: v, - value: v, - key: v, - })), - } - )); - - return props.onChange(value ?? null)} - value={props.value} - treeData={tree} - allowClear={fieldDefinition.optional} - className={props.className} - disabled={props.disabled} - open={open} - onOpenChange={newOpen => { - setOpen(newOpen); - if (newOpen && typeof props.value === "string") { - setCustomVersion(props.value); - } - }} - onKeyDown={e => { - if (open && e.key === "Tab") { - e.preventDefault(); - inputRef.current?.focus(); - } - }} - popupRender={(menu) => { - const isValid = lib.isPartialVersion(customVersion); - - const submit = () => { - if (customVersion && isValid) { - setTimeout(() => selectRef.current?.focus(), 0); - props.onChange(customVersion); - setOpen(false); - } - }; - - return ( - <> - {menu} - -
- setCustomVersion(e.target.value)} - onPressEnter={submit} - /> - -
- - ); - }} - />; -} diff --git a/packages/web_ui/src/components/InputTargetVersion.tsx b/packages/web_ui/src/components/InputVersion.tsx similarity index 68% rename from packages/web_ui/src/components/InputTargetVersion.tsx rename to packages/web_ui/src/components/InputVersion.tsx index 13defdea..bcb7beeb 100644 --- a/packages/web_ui/src/components/InputTargetVersion.tsx +++ b/packages/web_ui/src/components/InputVersion.tsx @@ -8,14 +8,21 @@ import ControlContext from "./ControlContext"; const defaultFieldDefinition: lib.FieldDefinition = { type: "string", optional: false }; -export default function InputTargetVersion ( +function InputVersion< + Version extends lib.TargetVersion, + IncludeLatest extends (Version extends "latest" ? boolean : false), +> ( props: Omit & { fieldDefinition?: lib.FieldDefinition; className?: string; + version: { + isValid: (v: string) => v is Version; + includeLatest: IncludeLatest; + }; } ) { const fieldDefinition = props.fieldDefinition || defaultFieldDefinition; - const [versions, setVersions] = useState(lib.ApiVersions); + const [versions, setVersions] = useState(lib.ApiVersions); const [customVersion, setCustomVersion] = useState(""); const [open, setOpen] = useState(false); const inputRef = useRef(null); @@ -33,11 +40,11 @@ export default function InputTargetVersion ( }, [control]); // Split the versions into groups based on major minor - const groups = new Map(); + const groups = new Map(); for (const version of versions) { const [major, minor] = version.split("."); - const key = `${major}.${minor}` as lib.PartialVersion; + const key = `${major}.${minor}`; if (!groups.has(key)) { groups.set(key, []); @@ -60,7 +67,9 @@ export default function InputTargetVersion ( )); // Add "latest" option for TargetVersion - tree.unshift({ title: "latest", value: "latest", key: "latest", children: [] }); + if (props.version.includeLatest) { + tree.unshift({ title: "latest", value: "latest", key: "latest", children: [] }); + } return { - const isValid = lib.isTargetVersion(customVersion); + const isValid = props.version.isValid(customVersion); const submit = () => { if (customVersion && isValid) { @@ -123,3 +132,48 @@ export default function InputTargetVersion ( }} />; } + +export function InputTargetVersion ( + props: Omit & { + fieldDefinition?: lib.FieldDefinition; + className?: string; + } +) { + return ; +} + +export function InputPartialVersion ( + props: Omit & { + fieldDefinition?: lib.FieldDefinition; + className?: string; + } +) { + return ; +} + +export function InputFullVersion ( + props: Omit & { + fieldDefinition?: lib.FieldDefinition; + className?: string; + } +) { + return ; +} diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index caea5202..d2640d18 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -26,7 +26,7 @@ import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; import SectionHeader from "./SectionHeader"; import ModDetails from "./ModDetails"; -import InputPartialVersion from "./InputPartialVersion"; +import { InputPartialVersion } from "./InputVersion"; import Link from "./Link"; const { logger } = lib; diff --git a/packages/web_ui/src/components/ModsPage.tsx b/packages/web_ui/src/components/ModsPage.tsx index 0c526b9d..a6ece310 100644 --- a/packages/web_ui/src/components/ModsPage.tsx +++ b/packages/web_ui/src/components/ModsPage.tsx @@ -21,7 +21,7 @@ import SectionHeader from "./SectionHeader"; import ModDetails from "./ModDetails"; import { Dropzone } from "./Dropzone"; import UploadButton from "./UploadButton"; -import InputPartialVersion from "./InputPartialVersion"; +import { InputPartialVersion } from "./InputVersion"; const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;