mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Have version selectors share a common base component
This commit is contained in:
5 files changed
+64
-132
No files matched your search
@@ -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<string, BaseWebPlugin>) {
|
||||
const inputComponents: Record<string, InputComponent> = {
|
||||
"full_version": InputFullVersion,
|
||||
"partial_version": InputPartialVersion,
|
||||
"target_version": InputTargetVersion,
|
||||
"mod_pack": InputModPack,
|
||||
|
||||
@@ -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<InputComponentProps, "fieldDefinition"> & {
|
||||
fieldDefinition?: lib.FieldDefinition;
|
||||
className?: string;
|
||||
}
|
||||
) {
|
||||
const fieldDefinition = props.fieldDefinition || defaultFieldDefinition;
|
||||
const [versions, setVersions] = useState<readonly lib.PartialVersion[]>(lib.ApiVersions);
|
||||
const [customVersion, setCustomVersion] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const selectRef = useRef<RefSelectProps>(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<lib.PartialVersion, lib.PartialVersion[]>();
|
||||
|
||||
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 <TreeSelect
|
||||
showSearch
|
||||
ref={selectRef}
|
||||
style={{ minWidth: 175 }}
|
||||
onChange={(value) => 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}
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<div style={{ display: "flex", gap: 8, padding: "0 8px 4px" }}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
style={{ flex: 1 }}
|
||||
placeholder="Custom X.Y.Z"
|
||||
value={customVersion}
|
||||
status={customVersion && !isValid ? "error" : undefined}
|
||||
onChange={(e) => setCustomVersion(e.target.value)}
|
||||
onPressEnter={submit}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!customVersion || !isValid}
|
||||
onClick={submit}
|
||||
>
|
||||
Use
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
+60
-6
@@ -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<InputComponentProps, "fieldDefinition"> & {
|
||||
fieldDefinition?: lib.FieldDefinition;
|
||||
className?: string;
|
||||
version: {
|
||||
isValid: (v: string) => v is Version;
|
||||
includeLatest: IncludeLatest;
|
||||
};
|
||||
}
|
||||
) {
|
||||
const fieldDefinition = props.fieldDefinition || defaultFieldDefinition;
|
||||
const [versions, setVersions] = useState<readonly lib.TargetVersion[]>(lib.ApiVersions);
|
||||
const [versions, setVersions] = useState<readonly string[]>(lib.ApiVersions);
|
||||
const [customVersion, setCustomVersion] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
@@ -33,11 +40,11 @@ export default function InputTargetVersion (
|
||||
}, [control]);
|
||||
|
||||
// Split the versions into groups based on major minor
|
||||
const groups = new Map<lib.TargetVersion, lib.TargetVersion[]>();
|
||||
const groups = new Map<string, string[]>();
|
||||
|
||||
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 <TreeSelect
|
||||
showSearch
|
||||
@@ -86,7 +95,7 @@ export default function InputTargetVersion (
|
||||
}
|
||||
}}
|
||||
popupRender={(menu) => {
|
||||
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<InputComponentProps, "fieldDefinition"> & {
|
||||
fieldDefinition?: lib.FieldDefinition;
|
||||
className?: string;
|
||||
}
|
||||
) {
|
||||
return <InputVersion
|
||||
{...props}
|
||||
version={{
|
||||
isValid: lib.isTargetVersion,
|
||||
includeLatest: true,
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
export function InputPartialVersion (
|
||||
props: Omit<InputComponentProps, "fieldDefinition"> & {
|
||||
fieldDefinition?: lib.FieldDefinition;
|
||||
className?: string;
|
||||
}
|
||||
) {
|
||||
return <InputVersion
|
||||
{...props}
|
||||
version={{
|
||||
isValid: lib.isPartialVersion,
|
||||
includeLatest: false,
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
export function InputFullVersion (
|
||||
props: Omit<InputComponentProps, "fieldDefinition"> & {
|
||||
fieldDefinition?: lib.FieldDefinition;
|
||||
className?: string;
|
||||
}
|
||||
) {
|
||||
return <InputVersion
|
||||
{...props}
|
||||
version={{
|
||||
isValid: lib.isFullVersion,
|
||||
includeLatest: false,
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user