From 4209d7dcb6596ecd42b562d38254589adc53992f Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 7 May 2025 18:19:07 +0000 Subject: [PATCH 01/11] feat: setup autofill for dynamic parameters --- .../DynamicParameter/DynamicParameter.tsx | 91 ++++++++++++++++--- .../CreateWorkspacePageViewExperimental.tsx | 14 +++ 2 files changed, 91 insertions(+), 14 deletions(-) diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index a41dcf352fd32..278fb122620ad 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -32,23 +32,27 @@ import { TooltipProvider, TooltipTrigger, } from "components/Tooltip/Tooltip"; -import { Info, Settings, TriangleAlert } from "lucide-react"; +import { Info, Link, Settings, TriangleAlert } from "lucide-react"; import { type FC, useEffect, useId, useState } from "react"; import type { AutofillBuildParameter } from "utils/richParameters"; import * as Yup from "yup"; export interface DynamicParameterProps { parameter: PreviewParameter; + value?: string; onChange: (value: string) => void; disabled?: boolean; isPreset?: boolean; + autofill?: AutofillBuildParameter; } export const DynamicParameter: FC = ({ parameter, + value, onChange, disabled, isPreset, + autofill, }) => { const id = useId(); @@ -57,13 +61,18 @@ export const DynamicParameter: FC = ({ className="flex flex-col gap-2" data-testid={`parameter-field-${parameter.name}`} > - +
{parameter.diagnostics.length > 0 && ( @@ -76,9 +85,14 @@ export const DynamicParameter: FC = ({ interface ParameterLabelProps { parameter: PreviewParameter; isPreset?: boolean; + autofill?: AutofillBuildParameter; } -const ParameterLabel: FC = ({ parameter, isPreset }) => { +const ParameterLabel: FC = ({ + parameter, + isPreset, + autofill, +}) => { const hasDescription = parameter.description && parameter.description !== ""; const displayName = parameter.display_name ? parameter.display_name @@ -137,6 +151,23 @@ const ParameterLabel: FC = ({ parameter, isPreset }) => { )} + {autofill && ( + + + + + + + URL Autofill + + + + + Autofilled from the URL + + + + )} {hasDescription && ( @@ -153,6 +184,7 @@ const ParameterLabel: FC = ({ parameter, isPreset }) => { interface ParameterFieldProps { parameter: PreviewParameter; + value?: string; onChange: (value: string) => void; disabled?: boolean; id: string; @@ -160,15 +192,19 @@ interface ParameterFieldProps { const ParameterField: FC = ({ parameter, + value, onChange, disabled, id, }) => { - const value = validValue(parameter.value); - const [localValue, setLocalValue] = useState(value); + const initialValue = + value !== undefined ? value : validValue(parameter.value); + const [localValue, setLocalValue] = useState(initialValue); useEffect(() => { - setLocalValue(value); + if (value !== undefined) { + setLocalValue(value); + } }, [value]); switch (parameter.form_type) { @@ -469,14 +505,14 @@ export const getInitialParameterValues = ( ({ name }) => name === parameter.name, ); + const useAutofill = + autofillParam && + isValidParameterOption(parameter, autofillParam) && + autofillParam.value; + return { name: parameter.name, - value: - autofillParam && - isValidParameterOption(parameter, autofillParam) && - autofillParam.value - ? autofillParam.value - : validValue(parameter.value), + value: useAutofill ? autofillParam.value : validValue(parameter.value), }; }); }; @@ -489,6 +525,32 @@ const isValidParameterOption = ( previewParam: PreviewParameter, buildParam: WorkspaceBuildParameter, ) => { + if (previewParam.form_type === "multi-select") { + console.log("buildParam.value", buildParam.value); + let values: string[] = []; + try { + const parsed = JSON.parse(buildParam.value); + if (Array.isArray(parsed)) { + values = parsed; + } + } catch (e) { + console.error( + "Error parsing parameter value with form_type multi-select", + e, + ); + return false; + } + + // If options exist, validate each value + if (previewParam.options.length > 0) { + const validValues = previewParam.options.map( + (option) => option.value.value, + ); + return values.every((value) => validValues.includes(value)); + } + return false; + } + // For parameters with options (dropdown, radio, etc.) if (previewParam.options.length > 0) { const validValues = previewParam.options.map( (option) => option.value.value, @@ -496,7 +558,8 @@ const isValidParameterOption = ( return validValues.includes(buildParam.value); } - return false; + // For parameters without options (input, textarea, etc.) + return true; }; export const useValidationSchemaForDynamicParameters = ( diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index 0ba3ee9fb77f3..384fb95ae4db0 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -33,6 +33,7 @@ import { useContext, useEffect, useId, + useMemo, useRef, useState, } from "react"; @@ -141,6 +142,14 @@ export const CreateWorkspacePageViewExperimental: FC< }, }); + const autofillByName = useMemo( + () => + Object.fromEntries( + autofillParameters.map((param) => [param.name, param]), + ), + [autofillParameters], + ); + useEffect(() => { if (error) { window.scrollTo(0, 0); @@ -509,6 +518,9 @@ export const CreateWorkspacePageViewExperimental: FC< return null; } + const formValue = + form.values?.rich_parameter_values?.[index]?.value || ""; + return ( ); })} From 1d798ea126c1cb23b629ab62cd9d6f8c58555a77 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 8 May 2025 20:47:16 +0000 Subject: [PATCH 02/11] chore: cleanup --- .../DynamicParameter/DynamicParameter.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index 278fb122620ad..b238bf4c0fab6 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -232,7 +232,7 @@ const ParameterField: FC = ({ ); case "multi-select": { - const values = parseStringArrayValue(value); + const values = parseStringArrayValue(value ?? ""); // Map parameter options to MultiSelectCombobox options format const options: Option[] = parameter.options.map((opt) => ({ @@ -277,7 +277,7 @@ const ParameterField: FC = ({ } case "tag-select": { - const values = parseStringArrayValue(value); + const values = parseStringArrayValue(value ?? ""); return ( { + // multi-select is the only list(string) type with options if (previewParam.form_type === "multi-select") { - console.log("buildParam.value", buildParam.value); let values: string[] = []; try { const parsed = JSON.parse(buildParam.value); @@ -541,16 +541,16 @@ const isValidParameterOption = ( return false; } - // If options exist, validate each value if (previewParam.options.length > 0) { const validValues = previewParam.options.map( (option) => option.value.value, ); - return values.every((value) => validValues.includes(value)); + return values.some((value) => validValues.includes(value)); } return false; } - // For parameters with options (dropdown, radio, etc.) + + // For parameters with options (dropdown, radio) if (previewParam.options.length > 0) { const validValues = previewParam.options.map( (option) => option.value.value, @@ -558,7 +558,7 @@ const isValidParameterOption = ( return validValues.includes(buildParam.value); } - // For parameters without options (input, textarea, etc.) + // For parameters without options (input,textarea,switch,checkbox,tag-select) return true; }; From 9d10195c16ac8492d2b01371a0aae1943c620bd2 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 9 May 2025 12:48:24 +0000 Subject: [PATCH 03/11] feat: set initial param values from autofill --- .../CreateWorkspacePageViewExperimental.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index 384fb95ae4db0..c40b1f46258bc 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -142,6 +142,30 @@ export const CreateWorkspacePageViewExperimental: FC< }, }); + // On component mount, sends all initial parameter values to the websocket + // (including defaults and autofilled from the url) + // This ensures the backend has the complete initial state of the form, + // which is vital for correctly rendering dynamic UI elements where parameter visibility + // or options might depend on the initial values of other parameters. + const hasInitializedWebsocket = useRef(false); + useEffect(() => { + if (hasInitializedWebsocket.current) return; + + const formValues = form.values.rich_parameter_values; + if (parameters.length > 0 && formValues && formValues.length > 0) { + const initialParams: { [k: string]: string } = {}; + for (const param of formValues) { + if (param.name && param.value) { + initialParams[param.name] = param.value; + } + } + if (Object.keys(initialParams).length > 0) { + sendMessage(initialParams); + hasInitializedWebsocket.current = true; + } + } + }, [parameters, form.values.rich_parameter_values, sendMessage]); + const autofillByName = useMemo( () => Object.fromEntries( From ec35a7f4bd47383b88cea03000c90c7ec1c62d4f Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 12 May 2025 10:34:57 +0000 Subject: [PATCH 04/11] fix: setup ParameterLabel to use htmlfor --- .../MultiSelectCombobox.tsx | 1 + site/src/components/Select/Select.tsx | 5 ++- .../DynamicParameter/DynamicParameter.tsx | 39 +++++++++++-------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx index 249af7918df28..64ad185119ac5 100644 --- a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx +++ b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx @@ -514,6 +514,7 @@ export const MultiSelectCombobox = forwardRef< })} {/* Avoid having the "Search" Icon */} , - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { id?: string } +>(({ className, children, id, ...props }, ref) => ( = ({ data-testid={`parameter-field-${parameter.name}`} > = ({ parameter, isPreset, autofill, + id, }) => { const hasDescription = parameter.description && parameter.description !== ""; const displayName = parameter.display_name @@ -109,7 +112,10 @@ const ParameterLabel: FC = ({ )}
-