Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

jaaydenh
Copy link
Contributor

@jaaydenh jaaydenh commented May 9, 2025

Parameter autofill allows setting parameters from the url using the format param.[param name]=["purple","green"]

Example:
http://localhost:8080/templates/coder/scratch/workspace?param.list=%5b%22purple%22%2c%22green%22%5d%0a

The goal is to maintain feature parity of for autofill with dynamic parameters.

Note: user history autofill is no longer being used and is being removed.

@jaaydenh jaaydenh self-assigned this May 9, 2025
@@ -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") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also could have been if (previewParam.type === "list(string) && param.options.length > 0)
but it feels like checking the form_type is more specific and the only case that needs this logic

@jaaydenh jaaydenh requested a review from Parkreiner May 9, 2025 15:08
@jaaydenh jaaydenh marked this pull request as ready for review May 9, 2025 15:08
// 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);
Copy link
Contributor Author

@jaaydenh jaaydenh May 9, 2025

Choose a reason for hiding this comment

The 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

const currentParameters = initialParametersRef.current;
const currentRichParams = initialRichParameterValuesRef.current;
const currentSendMessage = sendMessageRef.current;

Copy link
Member

@Parkreiner Parkreiner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a few changes I think we should make. I'm happy to pair on Monday

Comment on lines +200 to 208
const initialValue =
value !== undefined ? value : validValue(parameter.value);
const [localValue, setLocalValue] = useState(initialValue);

useEffect(() => {
setLocalValue(value);
if (value !== undefined) {
setLocalValue(value);
}
}, [value]);
Copy link
Member

@Parkreiner Parkreiner May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For something like this, you don't want to sync state via useEffect, because that means the UI completes a full render with the wrong data (including painting the screen), the state sync happens, and then you have to redo the whole render (possibly introducing screen flickering)

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:

  1. Let's say Component A is defined in terms of Component B and Component C
  2. Component A starts rendering
  3. The state changes mid-render
  4. The render for Component A finishes. Any effects and event handlers that were defined inside the component are created as monadic functions, and the JSX object output is returned, which includes component references for Component B and Component C
  5. React sees that the state changed, and knows that the result it produced is invalid
  6. It throws away the effects, event handlers, and JSX objects. At this point, Component B and Component C have not been allowed to start rendering
  7. React redoes the render for Component A
  8. The state stays stable this time around
  9. React knows that the output is fine this time, so it proceeds to use the JSX object output to render Component B and Component C – for the very first time in this specific render cycle
  10. The whole subtree finishes rendering, and then paints the whole updated output to the screen

Copy link
Member

Choose a reason for hiding this comment

The 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 value prop: value ?? validValue(parameter.value)

Copy link
Member

Choose a reason for hiding this comment

The 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 localValue, even though it's guaranteed to always be a string

Comment on lines +64 to +68
<ParameterLabel
parameter={parameter}
isPreset={isPreset}
autofill={autofill}
/>
Copy link
Member

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?

@@ -76,9 +85,14 @@ export const DynamicParameter: FC<DynamicParameterProps> = ({
interface ParameterLabelProps {
parameter: PreviewParameter;
isPreset?: boolean;
autofill?: AutofillBuildParameter;
Copy link
Member

Choose a reason for hiding this comment

The 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 autofill boolean?

parameter,
isPreset,
autofill,
}) => {
const hasDescription = parameter.description && parameter.description !== "";
Copy link
Member

Choose a reason for hiding this comment

The 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);

<TooltipTrigger asChild>
<span className="flex items-center">
<Badge size="sm">
<Link />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I feel like Link is a confusing name without a rename on import, since it sounds like it's a non-icon component

Comment on lines +508 to +511
const useAutofill =
autofillParam &&
isValidParameterOption(parameter, autofillParam) &&
autofillParam.value;
Copy link
Member

@Parkreiner Parkreiner May 9, 2025

Choose a reason for hiding this comment

The 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 Boolean? The current type signature is string | false | undefined

Comment on lines +537 to +540
console.error(
"Error parsing parameter value with form_type multi-select",
e,
);
Copy link
Member

Choose a reason for hiding this comment

The 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?

Comment on lines +169 to +175
const autofillByName = useMemo(
() =>
Object.fromEntries(
autofillParameters.map((param) => [param.name, param]),
),
[autofillParameters],
);
Copy link
Member

Choose a reason for hiding this comment

The 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 useMemo would actually make performance worse

Comment on lines +150 to +167
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]);
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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 formValues = form.values.rich_parameter_values;
if (parameters.length > 0 && formValues && formValues.length > 0) {
const initialParams: { [k: string]: string } = {};
Copy link
Member

@Parkreiner Parkreiner May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Record<string, string> is fully equivalent here, since we're not relying on recursion over type index signatures

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants