From 51b90e7260d86ea5ed34b6114512ea7389ccf7e9 Mon Sep 17 00:00:00 2001 From: Apis Mellifera Date: Mon, 19 Jan 2026 03:44:41 +0300 Subject: [PATCH 1/2] PB-849: empty value bugs --- .../ArraySelectorTrigger/ArraySelectorTrigger.tsx | 6 ++++-- .../QubitsSelectorPopup/QubitsSelectorPopup.tsx | 6 +++--- frontend/src/components/Input/InputField.tsx | 2 +- .../src/components/Parameters/InputElement.tsx | 8 ++++---- .../components/Parameters/ParameterSelector.tsx | 8 ++++---- frontend/src/components/Parameters/Parameters.tsx | 5 +++-- frontend/src/components/Parameters/utils.ts | 8 +++++--- .../Nodes/components/NodeElement/NodeElement.tsx | 2 +- .../components/RunningJob/RunningJobParameters.tsx | 2 +- .../GraphStores/GraphLibrary/GraphLibraryStore.ts | 14 ++++++++++++-- .../src/stores/GraphStores/GraphLibrary/actions.ts | 4 ++-- frontend/src/stores/NodesStore/NodesStore.ts | 7 ++++++- frontend/src/stores/NodesStore/actions.ts | 4 ++-- frontend/src/stores/NodesStore/api/NodesAPI.tsx | 2 +- .../components/NodeElement/NodeElement.test.tsx | 1 + .../tests/unit/modules/Graph/GraphElement.test.tsx | 3 +-- 16 files changed, 51 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/ArraySelector/components/ArraySelectorTrigger/ArraySelectorTrigger.tsx b/frontend/src/components/ArraySelector/components/ArraySelectorTrigger/ArraySelectorTrigger.tsx index e7730197..6a5114c5 100644 --- a/frontend/src/components/ArraySelector/components/ArraySelectorTrigger/ArraySelectorTrigger.tsx +++ b/frontend/src/components/ArraySelector/components/ArraySelectorTrigger/ArraySelectorTrigger.tsx @@ -10,7 +10,7 @@ const ArraySelectorTrigger = ({ onClick, icon, }: { - value: string[] | string; + value?: string[] | string; disabled: boolean onClick: () => void icon?: React.ReactNode @@ -24,7 +24,9 @@ const ArraySelectorTrigger = ({ ? value.slice(0, MAX_SHOWN_VALUES).join(", ") : value } - {(value.length > MAX_SHOWN_VALUES) ? `+${(value.length - MAX_SHOWN_VALUES)}` : ""} + {value && + {(value.length > MAX_SHOWN_VALUES) ? `+${(value.length - MAX_SHOWN_VALUES)}` : ""} + } {!disabled && icon} diff --git a/frontend/src/components/ArraySelector/components/QubitsSelectorPopup/QubitsSelectorPopup.tsx b/frontend/src/components/ArraySelector/components/QubitsSelectorPopup/QubitsSelectorPopup.tsx index 4e88fba4..bb0ff908 100644 --- a/frontend/src/components/ArraySelector/components/QubitsSelectorPopup/QubitsSelectorPopup.tsx +++ b/frontend/src/components/ArraySelector/components/QubitsSelectorPopup/QubitsSelectorPopup.tsx @@ -8,7 +8,7 @@ import { getSearchStringIndex } from "../../utils"; type IProps = { open: boolean; onClose: () => void; - value: string[]; + value?: string[]; metadata: QubitMetadataList; onChange: (value: string[]) => void; } @@ -87,7 +87,7 @@ const QubitsSelectorPopup = ({ onClose(); }; const handleCancel = () => { - setSelection(value); + setSelection(value || []); onClose(); }; @@ -99,7 +99,7 @@ const QubitsSelectorPopup = ({ }, [open]); useEffect(() => { - setSelection(value); + setSelection(value || []); }, [value]); return , "onCh const InputField = (props: InputProps) => { const { name = "", - value, + value = "", onChange, typeOfField, className, diff --git a/frontend/src/components/Parameters/InputElement.tsx b/frontend/src/components/Parameters/InputElement.tsx index 5fdf0ff1..e6cc1393 100644 --- a/frontend/src/components/Parameters/InputElement.tsx +++ b/frontend/src/components/Parameters/InputElement.tsx @@ -21,7 +21,7 @@ const ParameterSelector = ({ const subgraphBreadcrumbs = useSelector(getSubgraphBreadcrumbs); const selectedWorkflowName = useSelector(getSelectedWorkflowName); - const handleChange = (newValue: string | number | boolean) => { + const handleChange = (newValue: string | number | boolean | undefined) => { dispatch(setNodeParameter({ paramKey: parameterKey, newValue, @@ -35,8 +35,8 @@ const ParameterSelector = ({ case "boolean": return ( handleChange(!parameter.default)} + checked={parameter.value as boolean} + onClick={() => handleChange(!parameter.value)} inputProps={{ "aria-label": "controlled" }} /> ); @@ -44,7 +44,7 @@ const ParameterSelector = ({ return ( ); diff --git a/frontend/src/components/Parameters/ParameterSelector.tsx b/frontend/src/components/Parameters/ParameterSelector.tsx index c2ed9dae..47c09804 100644 --- a/frontend/src/components/Parameters/ParameterSelector.tsx +++ b/frontend/src/components/Parameters/ParameterSelector.tsx @@ -22,9 +22,9 @@ const ParameterSelector = ({ onChange: (paramKey: string, newValue: ParamaterValue, isValid: boolean, nodeId?: string | undefined) => void }) => { const [error, setError] = useState(undefined); - const [inputValue, setInputValue] = useState(parameter.default); + const [inputValue, setInputValue] = useState(parameter.value); - const handleBlur = useCallback((value: SingleParameter["default"]) => { + const handleBlur = useCallback((value?: ParamaterValue) => { const { isValid, error } = validate(parameter, value); onChange(parameterKey, value as string, isValid, node?.name); @@ -90,8 +90,8 @@ const ParameterSelector = ({ return ( setInputValue(value || undefined)} onBlur={() => handleBlur(inputValue)} className={styles.input} type={["number", "integer"].includes(parameter.type) ? "number" : "string"} diff --git a/frontend/src/components/Parameters/Parameters.tsx b/frontend/src/components/Parameters/Parameters.tsx index 5d1e92a9..f16b7ccc 100644 --- a/frontend/src/components/Parameters/Parameters.tsx +++ b/frontend/src/components/Parameters/Parameters.tsx @@ -20,7 +20,7 @@ interface IProps { } export type ParameterTypes = "boolean" | "number" | "integer" | "array" | "string"; -export type ParamaterValue = string | boolean | number | string[]; +export type ParamaterValue = string | boolean | number | string[] | undefined; export type QubitMetadata = { active: boolean; fidelity: number; } export type QubitMetadataList = Record export interface SingleParameter { @@ -28,9 +28,10 @@ export interface SingleParameter { name?: string; parameters?: InputParameter; default?: ParamaterValue; + value?: ParamaterValue; items?: { type: string }; enum?: string[] - metadata?: QubitMetadataList; + metadata?: QubitMetadataList | null; options?: { id: string; title: string; diff --git a/frontend/src/components/Parameters/utils.ts b/frontend/src/components/Parameters/utils.ts index add69e59..fa5ed235 100644 --- a/frontend/src/components/Parameters/utils.ts +++ b/frontend/src/components/Parameters/utils.ts @@ -1,7 +1,9 @@ import { SingleParameter } from "./Parameters"; export const validate = (parameter: SingleParameter, value: unknown) => { - if (value === undefined || value === "" || (Array.isArray(value) && value.length === 0)) { + const noDefault = !parameter.default; + + if (noDefault && (value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0))) { return { isValid: false, error: "Must be not empty", @@ -12,7 +14,7 @@ export const validate = (parameter: SingleParameter, value: unknown) => { switch (parameter.type) { case "number": - if (isNaN(num)) { + if (noDefault && isNaN(num)) { return { isValid: false, error: "Must be a number" @@ -21,7 +23,7 @@ export const validate = (parameter: SingleParameter, value: unknown) => { return { isValid: true, error: undefined }; case "integer": - if (isNaN(num) || !Number.isInteger(num)) { + if (noDefault && (isNaN(num) || !Number.isInteger(num))) { return { isValid: false, error: "Must be an integer" diff --git a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx index 88e17e66..15de17af 100644 --- a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx +++ b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx @@ -149,7 +149,7 @@ export const NodeElement: React.FC<{ nodeKey: string }> = ({ nodeKey }) => { * (title, type, description). Triggers a full NodesContext state update, * causing re-render of all consumers. */ - const updateParameter = (paramKey: string, newValue: boolean | number | string | string[], isValid: boolean) => { + const updateParameter = (paramKey: string, newValue: boolean | number | string | string[] | undefined, isValid: boolean) => { handleSetError(paramKey, isValid); dispatch(setNodeParameter({ nodeKey, paramKey, newValue })); }; diff --git a/frontend/src/modules/Nodes/components/RunningJob/RunningJobParameters.tsx b/frontend/src/modules/Nodes/components/RunningJob/RunningJobParameters.tsx index d2bc1e6b..91855455 100644 --- a/frontend/src/modules/Nodes/components/RunningJob/RunningJobParameters.tsx +++ b/frontend/src/modules/Nodes/components/RunningJob/RunningJobParameters.tsx @@ -18,7 +18,7 @@ export const RunningJobParameters: React.FC = () => {
{parameter.title}:
-
{parameter.default?.toString()}
+
{parameter.value?.toString()}
)) } diff --git a/frontend/src/stores/GraphStores/GraphLibrary/GraphLibraryStore.ts b/frontend/src/stores/GraphStores/GraphLibrary/GraphLibraryStore.ts index 6becb915..adedf1f0 100644 --- a/frontend/src/stores/GraphStores/GraphLibrary/GraphLibraryStore.ts +++ b/frontend/src/stores/GraphStores/GraphLibrary/GraphLibraryStore.ts @@ -73,6 +73,16 @@ export const graphLibrarySlice = createSlice({ reducers: { setAllGraphs: (state, action) => { state.allGraphs = action.payload; + + const applyDefaultValue = (workflow?: GraphMap) => + Object.values(workflow || {}).map(graph => { + Object.values(graph.parameters || {}).map(parameter => + parameter.value = parameter.default + ); + graph.nodes && applyDefaultValue(graph.nodes); + }); + + applyDefaultValue(state.allGraphs); }, setSelectedWorkflowName: (state, action) => { state.selectedWorkflowName = action.payload; @@ -94,7 +104,7 @@ export const graphLibrarySlice = createSlice({ }, setNodeParameter: (state, action: PayloadAction<{ paramKey: string - newValue: boolean | number | string | string[] + newValue: boolean | number | string | string[] | undefined nodeId?: string selectedWorkflowName?: string subgraphBreadcrumbs: string[] @@ -114,7 +124,7 @@ export const graphLibrarySlice = createSlice({ } if (graph.parameters) - graph.parameters[paramKey].default = newValue; + graph.parameters[paramKey].value = newValue; }, setErrorObject: (state, action) => { state.errorObject = action.payload; diff --git a/frontend/src/stores/GraphStores/GraphLibrary/actions.ts b/frontend/src/stores/GraphStores/GraphLibrary/actions.ts index 6192ded7..ef22d8fc 100644 --- a/frontend/src/stores/GraphStores/GraphLibrary/actions.ts +++ b/frontend/src/stores/GraphStores/GraphLibrary/actions.ts @@ -120,7 +120,7 @@ export const submitWorkflow = () => async (dispatch: RootDispatch, getState: () if (!params) return transformedParams; for (const key in params) { - transformedParams = { ...transformedParams, [key]: params[key].default }; + transformedParams = { ...transformedParams, [key]: params[key].value }; } return transformedParams; }; @@ -168,7 +168,7 @@ export const submitWorkflow = () => async (dispatch: RootDispatch, getState: () } }; -export const setGraphNodeParameter = (paramKey: string, newValue: boolean | number | string | string[], nodeId?: string) => +export const setGraphNodeParameter = (paramKey: string, newValue: boolean | number | string | string[] | undefined, nodeId?: string) => (dispatch: RootDispatch, getState: () => RootState) => { const subgraphBreadcrumbs = getSubgraphBreadcrumbs(getState()); const selectedWorkflowName = getSelectedWorkflowName(getState()); diff --git a/frontend/src/stores/NodesStore/NodesStore.ts b/frontend/src/stores/NodesStore/NodesStore.ts index 00aa8e45..a9ac8581 100644 --- a/frontend/src/stores/NodesStore/NodesStore.ts +++ b/frontend/src/stores/NodesStore/NodesStore.ts @@ -109,11 +109,16 @@ export const nodesSlice = createSlice({ }, setAllNodes: (state, action) => { state.allNodes = action.payload; + Object.values(state.allNodes || {}).map((node) => + Object.values(node.parameters || {}).map(parameter => + parameter.value = parameter.default + ) + ); }, setNodeParameter: (state, action) => { const { nodeKey, paramKey, newValue } = action.payload; if (state.allNodes && state.allNodes[nodeKey].parameters) - state.allNodes[nodeKey].parameters[paramKey].default = newValue; + state.allNodes[nodeKey].parameters[paramKey].value = newValue; }, setIsNodeRunning: (state, action) => { state.isNodeRunning = action.payload; diff --git a/frontend/src/stores/NodesStore/actions.ts b/frontend/src/stores/NodesStore/actions.ts index 2619a364..b9ca4ced 100644 --- a/frontend/src/stores/NodesStore/actions.ts +++ b/frontend/src/stores/NodesStore/actions.ts @@ -190,10 +190,10 @@ const formatDate = (date: Date) => { const transformInputParameters = (parameters: InputParameter) => { return Object.entries(parameters).reduce( (acc, [key, parameter]) => { - acc[key] = parameter.default ?? null; + acc[key] = parameter.value ?? undefined; return acc; }, - {} as { [key: string]: boolean | number | string | null | string[] } + {} as { [key: string]: boolean | number | string | null | string[] | undefined } ); }; diff --git a/frontend/src/stores/NodesStore/api/NodesAPI.tsx b/frontend/src/stores/NodesStore/api/NodesAPI.tsx index 50efe764..017e12f2 100644 --- a/frontend/src/stores/NodesStore/api/NodesAPI.tsx +++ b/frontend/src/stores/NodesStore/api/NodesAPI.tsx @@ -30,7 +30,7 @@ export class NodesApi extends Api { static submitNodeParameters( nodeName: string, inputParameter: { - [key: string]: string | number | boolean | null | string[]; + [key: string]: string | number | boolean | null | string[] | undefined; } ): Promise> { return this._fetch(this.api(SUBMIT_NODE_RUN()), API_METHODS.POST, { diff --git a/frontend/tests/unit/components/NodeElement/NodeElement.test.tsx b/frontend/tests/unit/components/NodeElement/NodeElement.test.tsx index 54f82f98..0d68b4e3 100644 --- a/frontend/tests/unit/components/NodeElement/NodeElement.test.tsx +++ b/frontend/tests/unit/components/NodeElement/NodeElement.test.tsx @@ -122,6 +122,7 @@ describe("NodeElement - Execution", () => { parameters: { resonator: { default: "q1.resonator", + value: "q1.resonator", title: "Resonator", type: "string" as ParameterTypes, is_targets: false diff --git a/frontend/tests/unit/modules/Graph/GraphElement.test.tsx b/frontend/tests/unit/modules/Graph/GraphElement.test.tsx index 9ee3795e..cf67dfba 100644 --- a/frontend/tests/unit/modules/Graph/GraphElement.test.tsx +++ b/frontend/tests/unit/modules/Graph/GraphElement.test.tsx @@ -121,7 +121,6 @@ describe("GraphElement - Parameter Management", () => { }); //TODO: mock WebSocket event mockStore.dispatch(setSelectedWorkflowName("test_workflow")); - mockStore.dispatch(setAllGraphs({ test_workflow: mockGraph })); render( @@ -163,7 +162,7 @@ describe("GraphElement - Parameter Management", () => { // Verify setAllGraphs was called with updated parameters await waitFor(() => { expect(getAllGraphs(mockStore.getState())?.test_workflow.parameters?.frequency) - .toHaveProperty("default", "6.5"); + .toHaveProperty("value", "6.5"); }); }); From 4887bf0a609bb368a3f8127bc99d7f4bab1fd677 Mon Sep 17 00:00:00 2001 From: Apis Mellifera Date: Fri, 23 Jan 2026 23:05:33 +0300 Subject: [PATCH 2/2] PB-849: allow empty value only when null type is present --- .../components/Parameters/ParameterSelector.tsx | 8 +++++--- .../src/components/Parameters/Parameters.tsx | 5 ++++- frontend/src/components/Parameters/utils.ts | 17 ++++++++++++----- frontend/src/stores/NodesStore/actions.ts | 2 +- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/Parameters/ParameterSelector.tsx b/frontend/src/components/Parameters/ParameterSelector.tsx index 47c09804..821f74ed 100644 --- a/frontend/src/components/Parameters/ParameterSelector.tsx +++ b/frontend/src/components/Parameters/ParameterSelector.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from "react"; import { Checkbox } from "@mui/material"; import { ParamaterValue, SingleParameter } from "./Parameters"; -import { validate } from "./utils"; +import { getParameterType, validate } from "./utils"; // eslint-disable-next-line css-modules/no-unused-class import styles from "./Parameters.module.scss"; import { NodeDTO } from "../../modules/Nodes"; @@ -49,7 +49,9 @@ const ParameterSelector = ({ * time on backend, not during input. Consider adding number input with validation. */ const renderInput = useCallback(() => { - if (parameter.type === "boolean") + const { type } = getParameterType(parameter); + + if (type === "boolean") return ( setInputValue(value || undefined)} onBlur={() => handleBlur(inputValue)} className={styles.input} - type={["number", "integer"].includes(parameter.type) ? "number" : "string"} + type={["number", "integer"].includes(type) ? "number" : "string"} data-testid={`input-field-${parameterKey}`} /> ); diff --git a/frontend/src/components/Parameters/Parameters.tsx b/frontend/src/components/Parameters/Parameters.tsx index f16b7ccc..63dcafa3 100644 --- a/frontend/src/components/Parameters/Parameters.tsx +++ b/frontend/src/components/Parameters/Parameters.tsx @@ -19,7 +19,7 @@ interface IProps { getInputElement: (key: string, parameter: SingleParameter, node?: NodeDTO | GraphWorkflow) => React.JSX.Element; } -export type ParameterTypes = "boolean" | "number" | "integer" | "array" | "string"; +export type ParameterTypes = "boolean" | "number" | "integer" | "array" | "string" | "null"; export type ParamaterValue = string | boolean | number | string[] | undefined; export type QubitMetadata = { active: boolean; fidelity: number; } export type QubitMetadataList = Record @@ -41,6 +41,9 @@ export interface SingleParameter { }[]; title: string; type: ParameterTypes; + anyOf?: Array<{ + type: ParameterTypes + }> is_targets: boolean; description?: string | null; } diff --git a/frontend/src/components/Parameters/utils.ts b/frontend/src/components/Parameters/utils.ts index fa5ed235..e16e710a 100644 --- a/frontend/src/components/Parameters/utils.ts +++ b/frontend/src/components/Parameters/utils.ts @@ -1,9 +1,16 @@ import { SingleParameter } from "./Parameters"; +export const getParameterType = (parameter: SingleParameter) => { + return { + type: parameter.type || (parameter.anyOf || []).find(({ type }) => type !== "null")?.type, + allowEmpty: parameter.type === "null" || (parameter.anyOf || []).some(({ type }) => type === "null"), + }; +}; + export const validate = (parameter: SingleParameter, value: unknown) => { - const noDefault = !parameter.default; + const { type, allowEmpty } = getParameterType(parameter); - if (noDefault && (value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0))) { + if (!allowEmpty && (value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0))) { return { isValid: false, error: "Must be not empty", @@ -12,9 +19,9 @@ export const validate = (parameter: SingleParameter, value: unknown) => { const num = Number(value); - switch (parameter.type) { + switch (type) { case "number": - if (noDefault && isNaN(num)) { + if (!allowEmpty && isNaN(num)) { return { isValid: false, error: "Must be a number" @@ -23,7 +30,7 @@ export const validate = (parameter: SingleParameter, value: unknown) => { return { isValid: true, error: undefined }; case "integer": - if (noDefault && (isNaN(num) || !Number.isInteger(num))) { + if (!allowEmpty && (isNaN(num) || !Number.isInteger(num))) { return { isValid: false, error: "Must be an integer" diff --git a/frontend/src/stores/NodesStore/actions.ts b/frontend/src/stores/NodesStore/actions.ts index b9ca4ced..81fde078 100644 --- a/frontend/src/stores/NodesStore/actions.ts +++ b/frontend/src/stores/NodesStore/actions.ts @@ -190,7 +190,7 @@ const formatDate = (date: Date) => { const transformInputParameters = (parameters: InputParameter) => { return Object.entries(parameters).reduce( (acc, [key, parameter]) => { - acc[key] = parameter.value ?? undefined; + acc[key] = parameter.value ?? null; return acc; }, {} as { [key: string]: boolean | number | string | null | string[] | undefined }