-
Notifications
You must be signed in to change notification settings - Fork 881
feat: setup url autofill for dynamic parameters #17739
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<DynamicParameterProps> = ({ | ||
parameter, | ||
value, | ||
onChange, | ||
disabled, | ||
isPreset, | ||
autofill, | ||
}) => { | ||
const id = useId(); | ||
|
||
|
@@ -57,13 +61,18 @@ export const DynamicParameter: FC<DynamicParameterProps> = ({ | |
className="flex flex-col gap-2" | ||
data-testid={`parameter-field-${parameter.name}`} | ||
> | ||
<ParameterLabel parameter={parameter} isPreset={isPreset} /> | ||
<ParameterLabel | ||
parameter={parameter} | ||
isPreset={isPreset} | ||
autofill={autofill} | ||
/> | ||
<div className="max-w-lg"> | ||
<ParameterField | ||
id={id} | ||
parameter={parameter} | ||
value={value} | ||
onChange={onChange} | ||
disabled={disabled} | ||
id={id} | ||
/> | ||
</div> | ||
{parameter.diagnostics.length > 0 && ( | ||
|
@@ -76,9 +85,14 @@ export const DynamicParameter: FC<DynamicParameterProps> = ({ | |
interface ParameterLabelProps { | ||
parameter: PreviewParameter; | ||
isPreset?: boolean; | ||
autofill?: AutofillBuildParameter; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is only being used for a truthy check to influence the conditional rendering, can we replace this with an |
||
} | ||
|
||
const ParameterLabel: FC<ParameterLabelProps> = ({ parameter, isPreset }) => { | ||
const ParameterLabel: FC<ParameterLabelProps> = ({ | ||
parameter, | ||
isPreset, | ||
autofill, | ||
}) => { | ||
const hasDescription = parameter.description && parameter.description !== ""; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this wasn't part of the changes, but could we fix this? The second condition will never trigger, because an empty string is inherently falsey const hasDescription = Boolean(parameter.description); |
||
const displayName = parameter.display_name | ||
? parameter.display_name | ||
|
@@ -137,6 +151,23 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ parameter, isPreset }) => { | |
</Tooltip> | ||
</TooltipProvider> | ||
)} | ||
{autofill && ( | ||
<TooltipProvider delayDuration={100}> | ||
<Tooltip> | ||
<TooltipTrigger asChild> | ||
<span className="flex items-center"> | ||
<Badge size="sm"> | ||
<Link /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I feel like |
||
URL Autofill | ||
</Badge> | ||
</span> | ||
</TooltipTrigger> | ||
<TooltipContent className="max-w-xs"> | ||
Autofilled from the URL | ||
</TooltipContent> | ||
</Tooltip> | ||
</TooltipProvider> | ||
)} | ||
</Label> | ||
|
||
{hasDescription && ( | ||
|
@@ -153,22 +184,27 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ parameter, isPreset }) => { | |
|
||
interface ParameterFieldProps { | ||
parameter: PreviewParameter; | ||
value?: string; | ||
onChange: (value: string) => void; | ||
disabled?: boolean; | ||
id: string; | ||
} | ||
|
||
const ParameterField: FC<ParameterFieldProps> = ({ | ||
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]); | ||
Comment on lines
+200
to
208
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For something like this, you don't want to sync state via This is how I'd do it, and the React Docs recommend it, too: const [localValue, setLocalValue] = useState(
value !== undefined ? value : validValue(parameter.value),
);
if (value !== undefined && value !== localValue) {
setLocalValue(value);
} Setting state mid-render is valid, as long as you eventually hit a case where you stop calling the state setter. Inside an event handler, the state setter will bail out of re-renders if you dispatch a state value that's equal to the value currently in state. That protection is removed during a render to make sure the user doesn't call a state setter unconditionally This approach limits the "scope of the redo", because what happens is:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though (lol), after writing all that out, I think the better option would be to remove the state entirely, and then pass this to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How much churn has this component had? I see a bunch of undefined checks for |
||
|
||
switch (parameter.form_type) { | ||
|
@@ -196,7 +232,7 @@ const ParameterField: FC<ParameterFieldProps> = ({ | |
); | ||
|
||
case "multi-select": { | ||
const values = parseStringArrayValue(value); | ||
const values = parseStringArrayValue(value ?? ""); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason for using |
||
|
||
// Map parameter options to MultiSelectCombobox options format | ||
const options: Option[] = parameter.options.map((opt) => ({ | ||
|
@@ -241,7 +277,7 @@ const ParameterField: FC<ParameterFieldProps> = ({ | |
} | ||
|
||
case "tag-select": { | ||
const values = parseStringArrayValue(value); | ||
const values = parseStringArrayValue(value ?? ""); | ||
|
||
return ( | ||
<TagInput | ||
|
@@ -469,14 +505,14 @@ export const getInitialParameterValues = ( | |
({ name }) => name === parameter.name, | ||
); | ||
|
||
const useAutofill = | ||
autofillParam && | ||
isValidParameterOption(parameter, autofillParam) && | ||
autofillParam.value; | ||
Comment on lines
+508
to
+511
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: if we're going to extract this into a separate variable, can we wrap it in |
||
|
||
return { | ||
name: parameter.name, | ||
value: | ||
autofillParam && | ||
isValidParameterOption(parameter, autofillParam) && | ||
autofillParam.value | ||
? autofillParam.value | ||
: validValue(parameter.value), | ||
value: useAutofill ? autofillParam.value : validValue(parameter.value), | ||
}; | ||
}); | ||
}; | ||
|
@@ -489,14 +525,41 @@ const isValidParameterOption = ( | |
previewParam: PreviewParameter, | ||
buildParam: WorkspaceBuildParameter, | ||
) => { | ||
// multi-select is the only list(string) type with options | ||
if (previewParam.form_type === "multi-select") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this also could have been |
||
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, | ||
); | ||
Comment on lines
+537
to
+540
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't be logging anything from within code that gets run in a render path. If there's an error, could we derive an error value, and then wire things up so that we log that via an effect? |
||
return false; | ||
} | ||
|
||
if (previewParam.options.length > 0) { | ||
const validValues = previewParam.options.map( | ||
(option) => option.value.value, | ||
); | ||
return values.some((value) => validValues.includes(value)); | ||
} | ||
return false; | ||
} | ||
|
||
// For parameters with options (dropdown, radio) | ||
if (previewParam.options.length > 0) { | ||
const validValues = previewParam.options.map( | ||
(option) => option.value.value, | ||
); | ||
return validValues.includes(buildParam.value); | ||
} | ||
|
||
return false; | ||
// For parameters without options (input,textarea,switch,checkbox,tag-select) | ||
return true; | ||
}; | ||
|
||
export const useValidationSchemaForDynamicParameters = ( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,7 @@ import { | |
useContext, | ||
useEffect, | ||
useId, | ||
useMemo, | ||
useRef, | ||
useState, | ||
} from "react"; | ||
|
@@ -141,6 +142,38 @@ 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative to avoid the boolean ref and the dependency array could be to add refs for the dependencies
|
||
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 } = {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
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]); | ||
Comment on lines
+150
to
+167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to be honest: an effect like this strikes me as a massive code smell. This might be a case where the React primitives are falling apart, and we do have to do this, but my first instinct is to try remodeling the state in the parent component to make this kind of logic unnecessary There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like it might be better if we pair on Monday, so that I can get a sense for what you were going for. At the very least, I feel like there are ways to shrink the dependency array to minimize the ways that it's allowed to re-run: const initialParams = ???;
useEffect(() => {
// ???
}, [sendMessage, initialParams]); |
||
|
||
const autofillByName = useMemo( | ||
() => | ||
Object.fromEntries( | ||
autofillParameters.map((param) => [param.name, param]), | ||
), | ||
[autofillParameters], | ||
); | ||
Comment on lines
+169
to
+175
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have we measured whether this actually helps performance? My gut instinct is that initializing an object like this would be pretty cheap, and that adding |
||
|
||
useEffect(() => { | ||
if (error) { | ||
window.scrollTo(0, 0); | ||
|
@@ -509,6 +542,9 @@ export const CreateWorkspacePageViewExperimental: FC< | |
return null; | ||
} | ||
|
||
const formValue = | ||
form.values?.rich_parameter_values?.[index]?.value || ""; | ||
|
||
return ( | ||
<DynamicParameter | ||
key={parameter.name} | ||
|
@@ -518,6 +554,8 @@ export const CreateWorkspacePageViewExperimental: FC< | |
} | ||
disabled={isDisabled} | ||
isPreset={isPresetParameter} | ||
autofill={autofillByName[parameter.name]} | ||
value={formValue} | ||
/> | ||
); | ||
})} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there anything associating this label to the field? Would we want to pass the ID down, as well?