From cd04995b6563249c9e7a0225888a42f72c365341 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:40:41 +0000 Subject: [PATCH] Use link and disconnect icons for template inheritance Overridden values show a disconnect icon. Clicking it stages a link, shown with a link icon, which is applied together with the other changes instead of right away. Part of #200 Co-Authored-By: Claude Opus 5.5 --- .../web_ui/src/components/BaseConfigTree.tsx | 98 ++++++++++--------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/packages/web_ui/src/components/BaseConfigTree.tsx b/packages/web_ui/src/components/BaseConfigTree.tsx index 489bdf7e..1d808916 100644 --- a/packages/web_ui/src/components/BaseConfigTree.tsx +++ b/packages/web_ui/src/components/BaseConfigTree.tsx @@ -1,8 +1,9 @@ import React, { useContext, useEffect, useState } from "react"; import { - Button, Card, Checkbox, Form, FormInstance, Input, InputNumber, Popconfirm, Select, Space, Spin, Tooltip, Tree, - Typography, + Button, Card, Checkbox, Form, FormInstance, Input, InputNumber, Select, Space, Spin, Tooltip, Tree, Typography, } from "antd"; +import DisconnectOutlined from "@ant-design/icons/DisconnectOutlined"; +import LinkOutlined from "@ant-design/icons/LinkOutlined"; import LockOutlined from "@ant-design/icons/LockOutlined"; import ReloadOutlined from "@ant-design/icons/ReloadOutlined"; import UnlockOutlined from "@ant-design/icons/UnlockOutlined"; @@ -76,11 +77,14 @@ export type ConfigInheritance = { notice?: (config: lib.Config) => React.ReactNode; }; +type Linking = Map; + type LockState = { inheritance?: ConfigInheritance; unlocked: Set; setUnlocked: (unlocked: Set) => void; - inherit: (field: string, prop?: string) => void; + linking: Linking; + setLinking: (linking: Linking) => void; }; type BaseConfigTreeProps = { @@ -99,6 +103,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { let [errorFields, setErrorFields] = useState(new Map()); let [applying, setApplying] = useState(false); let [unlocked, setUnlocked] = useState>(new Set()); + let [linking, setLinking] = useState(new Map()); const available = props.available ?? true; const control = useContext(ControlContext); @@ -156,15 +161,16 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { } } - function inherit(field: string, prop?: string) { - props.inheritance!.inherit(field, prop).then(async () => { - const newConfig = await updateConfig(); - const values = getInitialValues(newConfig, props); - const keys = Object.keys(values).filter(key => key === field || key.startsWith(`${field}.`)); - form.resetFields(keys); - form.setFieldsValue(Object.fromEntries(keys.map(key => [key, values[key]]))); - setChangedFields(fields => new Set([...fields].filter(key => !keys.includes(key)))); - }).catch(err => notify(err.message, "error")); + async function applyChanges(fields: Record>) { + if (Object.keys(fields).length) { + await props.setConfig(fields); + } + for (const [field, prop] of linking.values()) { + await props.inheritance!.inherit(field, prop); + } + setChangedFields(new Set()); + setUnlocked(new Set()); + setLinking(new Map()); } const [propsMap, treeData] = computeTreeData( @@ -176,7 +182,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { setConfig, props.ConfigClass, onValuesChange, - { inheritance: props.inheritance, unlocked, setUnlocked, inherit }, + { inheritance: props.inheritance, unlocked, setUnlocked, linking, setLinking }, ); return <> @@ -213,9 +219,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { let applied = false; try { - await props.setConfig(fields); - setChangedFields(new Set()); - setUnlocked(new Set()); + await applyChanges(fields); applied = true; } catch (err: any) { notify(err.message, "error"); @@ -223,7 +227,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { const newConfig = await updateConfig(); if (applied) { - // Linking a template can change other values + // Linking can change other values form.setFieldsValue(getInitialValues(newConfig, props)); } setApplying(false); @@ -232,7 +236,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { props.onApply(); } }} - disabled={changedFields.size === 0} + disabled={changedFields.size === 0 && linking.size === 0} > Apply @@ -242,6 +246,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { form.resetFields(); setChangedFields(new Set()); setUnlocked(new Set()); + setLinking(new Map()); setErrorFields(new Map()); }} > @@ -297,42 +302,45 @@ function computeTreeData( }; function lockButton(key: string, fieldName: string, prop?: string) { - const { inheritance, unlocked, setUnlocked } = lock; + const { inheritance, unlocked, setUnlocked, linking, setLinking } = lock; if (!inheritance?.canInherit(config, fieldName) || !config.canAccess(fieldName, lib.ConfigAccess.write)) { return null; } + const button = (title: string, icon: React.ReactNode, label: string, onClick: () => void) => +