From a38bf60df2787e1639fd8e9ed860cce131d4a04d Mon Sep 17 00:00:00 2001 From: AntoineGautier Date: Thu, 22 Jan 2026 11:06:59 +0100 Subject: [PATCH 1/2] Refactor ConfigValues interface with proper typing --- .../src/components/modal/EditDetailsModal.tsx | 106 +++++++++--------- .../src/components/steps/Configs/SlideOut.tsx | 3 +- client/src/interpreter/interpreter.ts | 30 +++-- client/src/utils/modifier-helpers.ts | 27 +++-- client/tests/interpreter/interpreter.test.ts | 34 +++++- 5 files changed, 126 insertions(+), 74 deletions(-) diff --git a/client/src/components/modal/EditDetailsModal.tsx b/client/src/components/modal/EditDetailsModal.tsx index db18736a..f539e407 100644 --- a/client/src/components/modal/EditDetailsModal.tsx +++ b/client/src/components/modal/EditDetailsModal.tsx @@ -9,16 +9,13 @@ import { FlatConfigOption } from "../steps/Configs/SlideOut"; import OptionSelect from "../steps/Configs/OptionSelect"; import { useDebouncedCallback } from "use-debounce"; import { removeEmpty } from "../../utils/utils"; -import { - SystemTypeInterface, - TemplateInterface -} from "../../data/template"; +import { SystemTypeInterface, TemplateInterface } from "../../data/template"; import { applyValueModifiers, applyVisibilityModifiers, - ConfigValues, } from "../../utils/modifier-helpers"; +import { ConfigValues } from "../../utils/modifier-helpers"; interface EditDetailsModalProps extends ModalInterface { afterSubmit?: () => void; @@ -41,22 +38,20 @@ const EditDetailsModal = observer( const projectOptions = templateStore.getOptionsForProject(); const allOptions = templateStore.getAllOptions(); const [formInputs, setFormInputs] = useState({ - name: details?.name || '', - address: details?.address || '', - type: details?.type || '', + name: details?.name || "", + address: details?.address || "", + type: details?.type || "", size: details?.size || 0, - notes: details?.notes || '', + notes: details?.notes || "", }); const [selectedValues, setSelectedValues] = useState( - projectStore.getProjectSelections() + projectStore.getProjectSelections(), ); const evaluatedValues = getEvaluatedValues(projectOptions); - const displayedOptions = getDisplayOptions(projectOptions, 'root'); + const displayedOptions = getDisplayOptions(projectOptions, "root"); - function getEvaluatedValues( - options: OptionInterface[], - ): ConfigValues { + function getEvaluatedValues(options: OptionInterface[]): ConfigValues { let evaluatedValues: ConfigValues = {}; options.forEach((option) => { @@ -78,9 +73,7 @@ const EditDetailsModal = observer( if (option.childOptions?.length) { evaluatedValues = { ...evaluatedValues, - ...getEvaluatedValues( - option.childOptions, - ), + ...getEvaluatedValues(option.childOptions), }; } }); @@ -93,8 +86,8 @@ const EditDetailsModal = observer( parentModelicaPath: string, ): FlatConfigOption[] { const removeNotSpecifiedList = [ - 'Buildings.Templates.Data.AllSystems.ashCliZon', - 'Buildings.Templates.Data.AllSystems.tit24CliZon' + "Buildings.Templates.Data.AllSystems.ashCliZon", + "Buildings.Templates.Data.AllSystems.tit24CliZon", ]; let displayOptions: FlatConfigOption[] = []; @@ -110,8 +103,10 @@ const EditDetailsModal = observer( ); if (isVisible && option.childOptions?.length) { - const modifiedChildOptions = removeNotSpecifiedList.includes(option.modelicaPath) - ? option.childOptions.filter((opt) => opt.name !== 'Not specified') + const modifiedChildOptions = removeNotSpecifiedList.includes( + option.modelicaPath, + ) + ? option.childOptions.filter((opt) => opt.name !== "Not specified") : option.childOptions; displayOptions = [ @@ -128,42 +123,35 @@ const EditDetailsModal = observer( }, ]; - if (selectedValues[selectionPath]) { - const selectedOption = allOptions[ - selectedValues[selectionPath] - ] as OptionInterface; + const selectedValue = selectedValues[selectionPath]; + const evaluatedValue = evaluatedValues[selectionPath]; + // Only string values (Modelica paths) can be used for option lookup + if (typeof selectedValue === "string") { + const selectedOption = allOptions[selectedValue] as OptionInterface; if (selectedOption) { displayOptions = [ ...displayOptions, - ...getDisplayOptions( - [selectedOption], - option.modelicaPath, - ), + ...getDisplayOptions([selectedOption], option.modelicaPath), ]; } - } else if (evaluatedValues[selectionPath]) { + // Only string values (Modelica paths) can be used for option lookup + } else if (typeof evaluatedValue === "string") { const evaluatedOption = allOptions[ - evaluatedValues[selectionPath] + evaluatedValue ] as OptionInterface; if (evaluatedOption) { displayOptions = [ ...displayOptions, - ...getDisplayOptions( - [evaluatedOption], - option.modelicaPath, - ), + ...getDisplayOptions([evaluatedOption], option.modelicaPath), ]; } } } else if (option.childOptions?.length) { displayOptions = [ ...displayOptions, - ...getDisplayOptions( - option.childOptions, - option.modelicaPath, - ), + ...getDisplayOptions(option.childOptions, option.modelicaPath), ]; } }); @@ -172,7 +160,9 @@ const EditDetailsModal = observer( } const updateTextInput = useDebouncedCallback( - (event: ChangeEvent | ChangeEvent) => { + ( + event: ChangeEvent | ChangeEvent, + ) => { setFormInputs((prevState: any) => { return { ...prevState, @@ -190,7 +180,7 @@ const EditDetailsModal = observer( [event.target.name]: event.target.value, }; }); - }; + } function updateSelectedOption( parentModelicaPath: string, @@ -216,20 +206,27 @@ const EditDetailsModal = observer( // This was done due to time and was a bug found last min. function adjustProjectSelections() { const projectSelectedItems = selectedValues; - const energyStandard = projectSelectedItems['Buildings.Templates.Data.AllSystems.stdEne']; + const energyStandard = + projectSelectedItems["Buildings.Templates.Data.AllSystems.stdEne"]; if ( - energyStandard === 'Buildings.Controls.OBC.ASHRAE.G36.Types.EnergyStandard.ASHRAE90_1' && - projectSelectedItems['Buildings.Templates.Data.AllSystems.tit24CliZon'] + energyStandard === + "Buildings.Controls.OBC.ASHRAE.G36.Types.EnergyStandard.ASHRAE90_1" && + projectSelectedItems["Buildings.Templates.Data.AllSystems.tit24CliZon"] ) { - delete projectSelectedItems['Buildings.Templates.Data.AllSystems.tit24CliZon']; + delete projectSelectedItems[ + "Buildings.Templates.Data.AllSystems.tit24CliZon" + ]; } if ( - energyStandard === 'Buildings.Controls.OBC.ASHRAE.G36.Types.EnergyStandard.California_Title_24' && - projectSelectedItems['Buildings.Templates.Data.AllSystems.ashCliZon'] + energyStandard === + "Buildings.Controls.OBC.ASHRAE.G36.Types.EnergyStandard.California_Title_24" && + projectSelectedItems["Buildings.Templates.Data.AllSystems.ashCliZon"] ) { - delete projectSelectedItems['Buildings.Templates.Data.AllSystems.ashCliZon']; + delete projectSelectedItems[ + "Buildings.Templates.Data.AllSystems.ashCliZon" + ]; } return projectSelectedItems; @@ -248,10 +245,15 @@ const EditDetailsModal = observer( // The reason for this is we don't want the user to use saved configs with // changed project details as it will cause issues with evaluated values. templateStore.systemTypes.forEach((systemType: SystemTypeInterface) => { - const templates = templateStore.getTemplatesForSystem(systemType.modelicaPath); + const templates = templateStore.getTemplatesForSystem( + systemType.modelicaPath, + ); templates.forEach((option: TemplateInterface) => { - configStore.removeAllForSystemTemplate(systemType.modelicaPath, option.modelicaPath); + configStore.removeAllForSystemTemplate( + systemType.modelicaPath, + option.modelicaPath, + ); }); }); if (afterSubmit) afterSubmit(); @@ -269,10 +271,10 @@ const EditDetailsModal = observer( updateSelectedOption={updateSelectedOption} /> - ) + ); })} - ) + ); } return ( diff --git a/client/src/components/steps/Configs/SlideOut.tsx b/client/src/components/steps/Configs/SlideOut.tsx index 45ae5ba7..71138d32 100644 --- a/client/src/components/steps/Configs/SlideOut.tsx +++ b/client/src/components/steps/Configs/SlideOut.tsx @@ -6,7 +6,8 @@ import { OptionInterface } from "../../../data/template"; import Modal from "../../modal/Modal"; import OptionSelect from "./OptionSelect"; import { mapToDisplayOptions as mapConfigContextToDisplayOptions } from "../../../interpreter/display-option"; -import { ConfigContext, ConfigValues } from "../../../interpreter/interpreter"; +import { ConfigContext } from "../../../interpreter/interpreter"; +import { ConfigValues } from "../../../utils/modifier-helpers"; import { removeEmpty } from "../../../utils/utils"; import "../../../styles/components/config-slide-out.scss"; diff --git a/client/src/interpreter/interpreter.ts b/client/src/interpreter/interpreter.ts index 014e306a..976bb001 100644 --- a/client/src/interpreter/interpreter.ts +++ b/client/src/interpreter/interpreter.ts @@ -1,13 +1,10 @@ import { ConfigInterface } from "../../src/data/config"; import { TemplateInterface, OptionInterface } from "../../src/data/template"; import { removeEmpty } from "../../src/utils/utils"; +import { ConfigValues } from "../utils/modifier-helpers"; export type Literal = boolean | string | number; -export interface ConfigValues { - [key: string]: string; -} - export type Expression = { operator: string; operands: Array; @@ -149,7 +146,11 @@ const _instancePathToOption = ( if (context.config?.selections) { Object.entries(context.selections).map(([key, value]) => { const [, instancePath] = key.split("-"); - if (instancePath === curInstancePathList.join(".")) { + // Only string values (Modelica paths) can be used for option lookup + if ( + instancePath === curInstancePathList.join(".") && + typeof value === "string" + ) { option = context.options[value]; } }); @@ -612,7 +613,11 @@ const buildModsHelper = ( option.modelicaPath, newBase, ); - redeclaredType = selections[selectionPath]; + const selectionValue = selections[selectionPath]; + // Only string values (Modelica paths) can be used for option lookup + if (typeof selectionValue === "string") { + redeclaredType = selectionValue; + } } if (option.choiceModifiers && redeclaredType) { @@ -640,7 +645,13 @@ const buildModsHelper = ( } else { // if this is a replaceable element, get the redeclared type // (this includes instances of replaceable short classes) - const typeOptionPath = getReplaceableType(newBase, option, mods, selections, options); + const typeOptionPath = getReplaceableType( + newBase, + option, + mods, + selections, + options, + ); const typeOption = options[typeOptionPath as string]; if (typeOption && typeOption.options) { @@ -656,8 +667,7 @@ const buildModsHelper = ( // Each parent class must also be visited // See https://github.com/lbl-srg/ctrl-flow-dev/issues/360 - typeOption - .treeList + typeOption.treeList ?.filter((path) => path !== (typeOptionPath as string)) // Exclude current class from being visited again .map((oPath) => { const o = options[oPath]; @@ -669,7 +679,7 @@ const buildModsHelper = ( selections, selectionModelicaPathsCache, ); - }) + }); // Further populate `mods` with all options belonging to this class typeOption.options.map((path) => { diff --git a/client/src/utils/modifier-helpers.ts b/client/src/utils/modifier-helpers.ts index c6156084..d96a2f6c 100644 --- a/client/src/utils/modifier-helpers.ts +++ b/client/src/utils/modifier-helpers.ts @@ -8,14 +8,14 @@ import { resolveValue, } from "./expression-helpers"; +export interface ConfigValues { + [key: string]: boolean | string | number | null | undefined; +} + export type Modifiers = { [key: string]: { expression: Expression; final: boolean; redeclare: boolean }; }; -export interface ConfigValues { - [key: string]: string; -} - function addToModObject( newModifiers: Modifiers, baseInstancePath: string, @@ -122,7 +122,13 @@ export function buildModifiers( const datAll = options["datAll"]; // project settings updateModifiers(datAll, "", modifiers, "", options); } - updateModifiers(startOption, baseInstancePath, modifiers, selectedType, options); + updateModifiers( + startOption, + baseInstancePath, + modifiers, + selectedType, + options, + ); return modifiers; } @@ -275,15 +281,18 @@ export function getUpdatedModifiers( let updatedModifiers: Modifiers = deepCopy(modifiers); optionKeys.forEach((key) => { - if (values[key] !== null) { - const selectedType = values[key]; + const value = values[key]; + // Only string values (Modelica paths) can be used for option lookup + if (typeof value === "string") { + const selectedType = value; const modelicaPath = key.split("-")[0]; - const instancePath = key.split("-")[1]?.split(".").slice(0, -1).join(".") || ''; + const instancePath = + key.split("-")[1]?.split(".").slice(0, -1).join(".") || ""; const option = allOptions[modelicaPath] as OptionInterface; let choiceModifiers = {}; if (option?.choiceModifiers) { - choiceModifiers = option?.choiceModifiers[values[key]]; + choiceModifiers = option?.choiceModifiers[value]; // take modifiers and change to instace keys, instancePath above and add to updatedModifiers as the last item. } diff --git a/client/tests/interpreter/interpreter.test.ts b/client/tests/interpreter/interpreter.test.ts index 595b10a3..af1c1e90 100644 --- a/client/tests/interpreter/interpreter.test.ts +++ b/client/tests/interpreter/interpreter.test.ts @@ -433,6 +433,37 @@ describe("Path resolution", () => { }); describe("resolveToValue tests using context and evaluation", () => { + it("Handles boolean selections via context", () => { + let config = addNewConfig( + "Config with false boolean selection", + mzTemplate, + createSelections({ + "Buildings.Templates.AirHandlersFans.VAVMultiZone.have_senPreBui-have_senPreBui": false, + }), + ); + let context = new ConfigContext( + mzTemplate as TemplateInterface, + config as ConfigInterface, + allOptions, + config.selections as ConfigValues, + ); + expect(resolveToValue("have_senPreBui", context)).toBe(false); + config = addNewConfig( + "Config with true boolean selection", + mzTemplate, + createSelections({ + "Buildings.Templates.AirHandlersFans.VAVMultiZone.have_senPreBui-have_senPreBui": true, + }), + ); + context = new ConfigContext( + mzTemplate as TemplateInterface, + config as ConfigInterface, + allOptions, + config.selections as ConfigValues, + ); + expect(resolveToValue("have_senPreBui", context)).toBe(true); + }); + it("Handles fanSupBlo.typ", () => { const context = new ConfigContext( mzTemplate as TemplateInterface, @@ -463,8 +494,7 @@ describe("resolveToValue tests using context and evaluation", () => { config.selections as ConfigValues, ); - const expectedVal = - "Buildings.Templates.Components.Fans.ArrayVariable"; + const expectedVal = "Buildings.Templates.Components.Fans.ArrayVariable"; const val = context.getValue(instancePath); expect(val).toEqual(expectedVal); }); From cc5bacb3592c084fd60fe597325385c9c421d414 Mon Sep 17 00:00:00 2001 From: AntoineGautier Date: Thu, 22 Jan 2026 17:27:51 +0100 Subject: [PATCH 2/2] Fix typing --- client/tests/interpreter/interpreter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/tests/interpreter/interpreter.test.ts b/client/tests/interpreter/interpreter.test.ts index af1c1e90..926b6c7f 100644 --- a/client/tests/interpreter/interpreter.test.ts +++ b/client/tests/interpreter/interpreter.test.ts @@ -60,7 +60,7 @@ const createSelections = (selections: ConfigValues = {}) => { const addNewConfig = ( configName: string, template: TemplateInterface, - selections: { [key: string]: string }, + selections: ConfigValues, ) => { store.configStore.add({ name: configName,