import React, { useContext, useState } from "react"; import { Button, Checkbox, Col, ConfigProvider, Divider, Input, InputNumber, Row, Select, Space, Spin, Switch } from "antd"; import * as lib from "@clusterio/lib"; import { ControlContext, PageHeader, PageLayout, notifyErrorHandler, useAccount } from "@clusterio/web_ui"; import { Feature, FeatureField, FeatureValue, features, isDefaultValue, sameValue } from "../../features.js"; import { FeatureRecord, FeatureUpdateRequest } from "../../messages.js"; import { useFeatures } from "../index"; type FeatureState = { enabled: boolean, values: Record }; type PendingChanges = Record }>; // Same colours as the role and permission group pages const borderColor = "#424242"; const modifiedColor = "#2a1912"; /** The stored state of a feature, with defaults for values which were not set. */ function storedState(feature: Feature, record: FeatureRecord | undefined): FeatureState { return { enabled: record?.enabled ?? true, values: { ...Object.fromEntries(feature.fields.map(field => [field.name, field.default])), ...record?.values, }, }; } /** Group fields by the section in their name, "turrets.enabled" is in the "turrets" section. */ function groupFields(fields: FeatureField[]) { const groups = new Map(); for (const field of fields) { const section = field.name.includes(".") ? field.name.slice(0, field.name.lastIndexOf(".")) : ""; if (!groups.has(section)) { groups.set(section, []); } groups.get(section)!.push(field); } return [...groups.entries()]; } /** "afk_belts" to "Afk Belts", like the group titles on the role page */ function formatSectionTitle(name: string) { return name .replace(/\./g, " / ") .replace(/_/g, " ") .split(" ") .filter(Boolean) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(" "); } function matches(text: string, query: string) { return text.toLowerCase().includes(query); } /** Fields matching the search, every field when the feature itself matches */ function searchFields(feature: Feature, query: string) { if (!query || matches(feature.title, query) || matches(feature.description, query) || matches(feature.name, query)) { return feature.fields; } return feature.fields.filter(field => ( matches(field.title, query) || matches(field.description, query) || matches(field.name, query) )); } /** Renders a setting like the config pages do, registered input components take priority */ function FieldInput(props: { field: FeatureField, value: FeatureValue, disabled: boolean, onChange: (value: FeatureValue) => void }) { const { field, value, disabled, onChange } = props; const control = useContext(ControlContext); if (field.type !== "string_list" && field.inputComponent) { const CustomInput = control.inputComponents.get(field.inputComponent); if (CustomInput) { const fieldDefinition: lib.FieldDefinition = { type: field.type, title: field.title, description: field.description, optional: field.optional, inputComponent: field.inputComponent, }; return } onChange={onChange} disabled={disabled} />; } } switch (field.type) { case "number": return onChange(next ?? (field.optional ? null : field.default))} />; case "string": if (field.enum) { return onChange(e.target.value)} style={{ width: 300 }} />; case "string_list": return