mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: setup url autofill for dynamic parameters (#17739)
resolves coder/preview#80 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.
This commit is contained in:
@@ -15,10 +15,13 @@ export const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
export const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||
id?: string;
|
||||
}
|
||||
>(({ className, children, id, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={cn(
|
||||
`flex h-10 w-full font-medium items-center justify-between whitespace-nowrap rounded-md
|
||||
border border-border border-solid bg-transparent px-3 py-2 text-sm shadow-sm
|
||||
|
||||
@@ -32,23 +32,29 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { Info, Settings, TriangleAlert } from "lucide-react";
|
||||
import { type FC, useEffect, useId, useState } from "react";
|
||||
import { useDebouncedValue } from "hooks/debounce";
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import { Info, LinkIcon, Settings, TriangleAlert } from "lucide-react";
|
||||
import { type FC, useEffect, useId, useRef, 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: boolean;
|
||||
}
|
||||
|
||||
export const DynamicParameter: FC<DynamicParameterProps> = ({
|
||||
parameter,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
isPreset,
|
||||
autofill = false,
|
||||
}) => {
|
||||
const id = useId();
|
||||
|
||||
@@ -57,14 +63,31 @@ export const DynamicParameter: FC<DynamicParameterProps> = ({
|
||||
className="flex flex-col gap-2"
|
||||
data-testid={`parameter-field-${parameter.name}`}
|
||||
>
|
||||
<ParameterLabel parameter={parameter} isPreset={isPreset} />
|
||||
<ParameterLabel
|
||||
id={id}
|
||||
parameter={parameter}
|
||||
isPreset={isPreset}
|
||||
autofill={autofill}
|
||||
/>
|
||||
<div className="max-w-lg">
|
||||
<ParameterField
|
||||
parameter={parameter}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
/>
|
||||
{parameter.form_type === "input" ||
|
||||
parameter.form_type === "textarea" ? (
|
||||
<DebouncedParameterField
|
||||
id={id}
|
||||
parameter={parameter}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<ParameterField
|
||||
id={id}
|
||||
parameter={parameter}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{parameter.diagnostics.length > 0 && (
|
||||
<ParameterDiagnostics diagnostics={parameter.diagnostics} />
|
||||
@@ -76,10 +99,16 @@ export const DynamicParameter: FC<DynamicParameterProps> = ({
|
||||
interface ParameterLabelProps {
|
||||
parameter: PreviewParameter;
|
||||
isPreset?: boolean;
|
||||
autofill: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ParameterLabel: FC<ParameterLabelProps> = ({ parameter, isPreset }) => {
|
||||
const hasDescription = parameter.description && parameter.description !== "";
|
||||
const ParameterLabel: FC<ParameterLabelProps> = ({
|
||||
parameter,
|
||||
isPreset,
|
||||
autofill,
|
||||
id,
|
||||
}) => {
|
||||
const displayName = parameter.display_name
|
||||
? parameter.display_name
|
||||
: parameter.name;
|
||||
@@ -95,7 +124,10 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ parameter, isPreset }) => {
|
||||
)}
|
||||
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<Label className="flex gap-2 flex-wrap text-sm font-medium">
|
||||
<Label
|
||||
htmlFor={id}
|
||||
className="flex gap-2 flex-wrap text-sm font-medium"
|
||||
>
|
||||
<span className="flex">
|
||||
{displayName}
|
||||
{parameter.required && (
|
||||
@@ -137,9 +169,26 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ parameter, isPreset }) => {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{autofill && (
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center">
|
||||
<Badge size="sm">
|
||||
<LinkIcon />
|
||||
URL Autofill
|
||||
</Badge>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
Autofilled from the URL
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</Label>
|
||||
|
||||
{hasDescription && (
|
||||
{Boolean(parameter.description) && (
|
||||
<div className="text-content-secondary">
|
||||
<MemoizedMarkdown className="text-xs">
|
||||
{parameter.description}
|
||||
@@ -151,8 +200,96 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ parameter, isPreset }) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface DebouncedParameterFieldProps {
|
||||
parameter: PreviewParameter;
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
|
||||
parameter,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
id,
|
||||
}) => {
|
||||
const [localValue, setLocalValue] = useState(
|
||||
value !== undefined ? value : validValue(parameter.value),
|
||||
);
|
||||
const debouncedLocalValue = useDebouncedValue(localValue, 500);
|
||||
const onChangeEvent = useEffectEvent(onChange);
|
||||
// prevDebouncedValueRef is to prevent calling the onChangeEvent on the initial render
|
||||
const prevDebouncedValueRef = useRef<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (prevDebouncedValueRef.current !== undefined) {
|
||||
onChangeEvent(debouncedLocalValue);
|
||||
}
|
||||
|
||||
prevDebouncedValueRef.current = debouncedLocalValue;
|
||||
}, [debouncedLocalValue, onChangeEvent]);
|
||||
|
||||
switch (parameter.form_type) {
|
||||
case "textarea":
|
||||
return (
|
||||
<Textarea
|
||||
id={id}
|
||||
className="max-w-2xl"
|
||||
value={localValue}
|
||||
onChange={(e) => {
|
||||
const target = e.currentTarget;
|
||||
target.style.height = "auto";
|
||||
target.style.maxHeight = "700px";
|
||||
target.style.height = `${target.scrollHeight}px`;
|
||||
|
||||
setLocalValue(e.target.value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder={parameter.styling?.placeholder}
|
||||
required={parameter.required}
|
||||
/>
|
||||
);
|
||||
|
||||
case "input": {
|
||||
const inputType = parameter.type === "number" ? "number" : "text";
|
||||
const inputProps: Record<string, unknown> = {};
|
||||
|
||||
if (parameter.type === "number") {
|
||||
const validations = parameter.validations[0] || {};
|
||||
const { validation_min, validation_max } = validations;
|
||||
|
||||
if (validation_min !== null) {
|
||||
inputProps.min = validation_min;
|
||||
}
|
||||
|
||||
if (validation_max !== null) {
|
||||
inputProps.max = validation_max;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
type={inputType}
|
||||
value={localValue}
|
||||
onChange={(e) => {
|
||||
setLocalValue(e.target.value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
required={parameter.required}
|
||||
placeholder={parameter.styling?.placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface ParameterFieldProps {
|
||||
parameter: PreviewParameter;
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
id: string;
|
||||
@@ -160,17 +297,11 @@ interface ParameterFieldProps {
|
||||
|
||||
const ParameterField: FC<ParameterFieldProps> = ({
|
||||
parameter,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
id,
|
||||
}) => {
|
||||
const value = validValue(parameter.value);
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
switch (parameter.form_type) {
|
||||
case "dropdown":
|
||||
return (
|
||||
@@ -180,7 +311,7 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
disabled={disabled}
|
||||
required={parameter.required}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger id={id}>
|
||||
<SelectValue
|
||||
placeholder={parameter.styling?.placeholder || "Select option"}
|
||||
/>
|
||||
@@ -196,7 +327,15 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
);
|
||||
|
||||
case "multi-select": {
|
||||
const values = parseStringArrayValue(value);
|
||||
const parsedValues = parseStringArrayValue(value ?? "");
|
||||
|
||||
if (parsedValues.error) {
|
||||
return (
|
||||
<p className="text-sm text-content-destructive">
|
||||
{parsedValues.error}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
// Map parameter options to MultiSelectCombobox options format
|
||||
const options: Option[] = parameter.options.map((opt) => ({
|
||||
@@ -209,7 +348,7 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
parameter.options.map((opt) => [opt.value.value, opt.name]),
|
||||
);
|
||||
|
||||
const selectedOptions: Option[] = values.map((val) => {
|
||||
const selectedOptions: Option[] = parsedValues.values.map((val) => {
|
||||
return {
|
||||
value: val,
|
||||
label: optionMap.get(val) || val,
|
||||
@@ -220,7 +359,7 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
return (
|
||||
<MultiSelectCombobox
|
||||
inputProps={{
|
||||
id: `${id}-${parameter.name}`,
|
||||
id: id,
|
||||
}}
|
||||
options={options}
|
||||
defaultOptions={selectedOptions}
|
||||
@@ -241,13 +380,21 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
}
|
||||
|
||||
case "tag-select": {
|
||||
const values = parseStringArrayValue(value);
|
||||
const parsedValues = parseStringArrayValue(value ?? "");
|
||||
|
||||
if (parsedValues.error) {
|
||||
return (
|
||||
<p className="text-sm text-content-destructive">
|
||||
{parsedValues.error}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TagInput
|
||||
id={parameter.name}
|
||||
id={id}
|
||||
label={parameter.display_name || parameter.name}
|
||||
values={values}
|
||||
values={parsedValues.values}
|
||||
onChange={(values) => {
|
||||
onChange(JSON.stringify(values));
|
||||
}}
|
||||
@@ -258,6 +405,7 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
case "switch":
|
||||
return (
|
||||
<Switch
|
||||
id={id}
|
||||
checked={value === "true"}
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(checked ? "true" : "false");
|
||||
@@ -293,14 +441,14 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={parameter.name}
|
||||
id={id}
|
||||
checked={value === "true"}
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(checked ? "true" : "false");
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Label htmlFor={parameter.name}>{parameter.styling?.label}</Label>
|
||||
<Label htmlFor={id}>{parameter.styling?.label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -308,10 +456,10 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
return (
|
||||
<div className="flex flex-row items-baseline gap-3">
|
||||
<Slider
|
||||
id={id}
|
||||
className="mt-2"
|
||||
value={[Number(localValue ?? 0)]}
|
||||
value={[Number(value)]}
|
||||
onValueChange={([value]) => {
|
||||
setLocalValue(value.toString());
|
||||
onChange(value.toString());
|
||||
}}
|
||||
min={parameter.validations[0]?.validation_min ?? 0}
|
||||
@@ -321,77 +469,32 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
<span className="w-4 font-medium">{parameter.value.value}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "textarea":
|
||||
return (
|
||||
<Textarea
|
||||
className="max-w-2xl"
|
||||
value={localValue}
|
||||
onChange={(e) => {
|
||||
setLocalValue(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const target = e.currentTarget;
|
||||
target.style.maxHeight = "700px";
|
||||
target.style.height = `${target.scrollHeight}px`;
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder={parameter.styling?.placeholder}
|
||||
required={parameter.required}
|
||||
/>
|
||||
);
|
||||
|
||||
case "input": {
|
||||
const inputType = parameter.type === "number" ? "number" : "text";
|
||||
const inputProps: Record<string, unknown> = {};
|
||||
|
||||
if (parameter.type === "number") {
|
||||
const validations = parameter.validations[0] || {};
|
||||
const { validation_min, validation_max } = validations;
|
||||
|
||||
if (validation_min !== null) {
|
||||
inputProps.min = validation_min;
|
||||
}
|
||||
|
||||
if (validation_max !== null) {
|
||||
inputProps.max = validation_max;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
type={inputType}
|
||||
value={localValue}
|
||||
onChange={(e) => {
|
||||
setLocalValue(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
required={parameter.required}
|
||||
placeholder={parameter.styling?.placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parseStringArrayValue = (value: string): string[] => {
|
||||
let values: string[] = [];
|
||||
type ParsedValues = {
|
||||
values: string[];
|
||||
error: string;
|
||||
};
|
||||
|
||||
const parseStringArrayValue = (value: string): ParsedValues => {
|
||||
const parsedValues: ParsedValues = {
|
||||
values: [],
|
||||
error: "",
|
||||
};
|
||||
|
||||
if (value) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed)) {
|
||||
values = parsed;
|
||||
parsedValues.values = parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing parameter of type list(string)", e);
|
||||
parsedValues.error = `Error parsing parameter of type list(string), ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
return parsedValues;
|
||||
};
|
||||
|
||||
interface OptionDisplayProps {
|
||||
@@ -469,14 +572,12 @@ export const getInitialParameterValues = (
|
||||
({ name }) => name === parameter.name,
|
||||
);
|
||||
|
||||
const useAutofill =
|
||||
autofillParam?.value && isValidParameterOption(parameter, autofillParam);
|
||||
|
||||
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 +590,28 @@ const isValidParameterOption = (
|
||||
previewParam: PreviewParameter,
|
||||
buildParam: WorkspaceBuildParameter,
|
||||
) => {
|
||||
// multi-select is the only list(string) type with options
|
||||
if (previewParam.form_type === "multi-select") {
|
||||
let values: string[] = [];
|
||||
try {
|
||||
const parsed = JSON.parse(buildParam.value);
|
||||
if (Array.isArray(parsed)) {
|
||||
values = parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
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,
|
||||
@@ -496,7 +619,8 @@ const isValidParameterOption = (
|
||||
return validValues.includes(buildParam.value);
|
||||
}
|
||||
|
||||
return false;
|
||||
// For parameters without options (input,textarea,switch,checkbox,tag-select)
|
||||
return true;
|
||||
};
|
||||
|
||||
export const useValidationSchemaForDynamicParameters = (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { API } from "api/api";
|
||||
import { type ApiErrorResponse, DetailedError } from "api/errors";
|
||||
import { checkAuthorization } from "api/queries/authCheck";
|
||||
import {
|
||||
@@ -9,11 +10,13 @@ import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
|
||||
import type {
|
||||
DynamicParametersRequest,
|
||||
DynamicParametersResponse,
|
||||
PreviewParameter,
|
||||
Workspace,
|
||||
} from "api/typesGenerated";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import { getInitialParameterValues } from "modules/workspaces/DynamicParameter/DynamicParameter";
|
||||
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName";
|
||||
import {
|
||||
type FC,
|
||||
@@ -29,13 +32,13 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { pageTitle } from "utils/page";
|
||||
import type { AutofillBuildParameter } from "utils/richParameters";
|
||||
import { CreateWorkspacePageViewExperimental } from "./CreateWorkspacePageViewExperimental";
|
||||
const createWorkspaceModes = ["form", "auto", "duplicate"] as const;
|
||||
export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number];
|
||||
import { API } from "api/api";
|
||||
import {
|
||||
type CreateWorkspacePermissions,
|
||||
createWorkspaceChecks,
|
||||
} from "./permissions";
|
||||
|
||||
const createWorkspaceModes = ["form", "auto", "duplicate"] as const;
|
||||
export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number];
|
||||
export type ExternalAuthPollingState = "idle" | "polling" | "abandoned";
|
||||
|
||||
const CreateWorkspacePageExperimental: FC = () => {
|
||||
@@ -45,11 +48,12 @@ const CreateWorkspacePageExperimental: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [currentResponse, setCurrentResponse] =
|
||||
const [latestResponse, setLatestResponse] =
|
||||
useState<DynamicParametersResponse | null>(null);
|
||||
const [wsResponseId, setWSResponseId] = useState<number>(-1);
|
||||
const wsResponseId = useRef<number>(-1);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
const [wsError, setWsError] = useState<Error | null>(null);
|
||||
const initialParamsSentRef = useRef(false);
|
||||
|
||||
const customVersionId = searchParams.get("version") ?? undefined;
|
||||
const defaultName = searchParams.get("name");
|
||||
@@ -84,15 +88,61 @@ const CreateWorkspacePageExperimental: FC = () => {
|
||||
const realizedVersionId =
|
||||
customVersionId ?? templateQuery.data?.active_version_id;
|
||||
|
||||
const onMessage = useCallback((response: DynamicParametersResponse) => {
|
||||
setCurrentResponse((prev) => {
|
||||
if (prev?.id === response.id) {
|
||||
return prev;
|
||||
}
|
||||
return response;
|
||||
});
|
||||
const autofillParameters = getAutofillParameters(searchParams);
|
||||
|
||||
const sendMessage = useCallback((formValues: Record<string, string>) => {
|
||||
const request: DynamicParametersRequest = {
|
||||
id: wsResponseId.current + 1,
|
||||
inputs: formValues,
|
||||
};
|
||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(request));
|
||||
wsResponseId.current = wsResponseId.current + 1;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// On 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 sendInitialParameters = useEffectEvent(
|
||||
(parameters: PreviewParameter[]) => {
|
||||
if (initialParamsSentRef.current) return;
|
||||
if (parameters.length === 0) return;
|
||||
|
||||
const initialFormValues = getInitialParameterValues(
|
||||
parameters,
|
||||
autofillParameters,
|
||||
);
|
||||
if (initialFormValues.length === 0) return;
|
||||
|
||||
const initialParamsToSend: Record<string, string> = {};
|
||||
for (const param of initialFormValues) {
|
||||
if (param.name && param.value) {
|
||||
initialParamsToSend[param.name] = param.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(initialParamsToSend).length === 0) return;
|
||||
|
||||
sendMessage(initialParamsToSend);
|
||||
initialParamsSentRef.current = true;
|
||||
},
|
||||
);
|
||||
|
||||
const onMessage = useEffectEvent((response: DynamicParametersResponse) => {
|
||||
if (latestResponse && latestResponse?.id >= response.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initialParamsSentRef.current && response.parameters.length > 0) {
|
||||
sendInitialParameters([...response.parameters]);
|
||||
}
|
||||
|
||||
setLatestResponse(response);
|
||||
});
|
||||
|
||||
// Initialize the WebSocket connection when there is a valid template version ID
|
||||
useEffect(() => {
|
||||
if (!realizedVersionId) return;
|
||||
@@ -127,20 +177,6 @@ const CreateWorkspacePageExperimental: FC = () => {
|
||||
};
|
||||
}, [owner.id, realizedVersionId, onMessage]);
|
||||
|
||||
const sendMessage = useCallback((formValues: Record<string, string>) => {
|
||||
setWSResponseId((prevId) => {
|
||||
const request: DynamicParametersRequest = {
|
||||
id: prevId + 1,
|
||||
inputs: formValues,
|
||||
};
|
||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(request));
|
||||
return prevId + 1;
|
||||
}
|
||||
return prevId;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const organizationId = templateQuery.data?.organization_id;
|
||||
|
||||
const {
|
||||
@@ -167,9 +203,6 @@ const CreateWorkspacePageExperimental: FC = () => {
|
||||
[navigate],
|
||||
);
|
||||
|
||||
// Auto fill parameters
|
||||
const autofillParameters = getAutofillParameters(searchParams);
|
||||
|
||||
const autoCreationStartedRef = useRef(false);
|
||||
const automateWorkspaceCreation = useEffectEvent(async () => {
|
||||
if (autoCreationStartedRef.current || !organizationId) {
|
||||
@@ -231,18 +264,18 @@ const CreateWorkspacePageExperimental: FC = () => {
|
||||
}, [automateWorkspaceCreation, autoCreateReady]);
|
||||
|
||||
const sortedParams = useMemo(() => {
|
||||
if (!currentResponse?.parameters) {
|
||||
if (!latestResponse?.parameters) {
|
||||
return [];
|
||||
}
|
||||
return [...currentResponse.parameters].sort((a, b) => a.order - b.order);
|
||||
}, [currentResponse?.parameters]);
|
||||
return [...latestResponse.parameters].sort((a, b) => a.order - b.order);
|
||||
}, [latestResponse?.parameters]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle(title)}</title>
|
||||
</Helmet>
|
||||
{!currentResponse ||
|
||||
{!latestResponse ||
|
||||
!templateQuery.data ||
|
||||
isLoadingFormData ||
|
||||
isLoadingExternalAuth ||
|
||||
@@ -252,7 +285,7 @@ const CreateWorkspacePageExperimental: FC = () => {
|
||||
<CreateWorkspacePageViewExperimental
|
||||
mode={mode}
|
||||
defaultName={defaultName}
|
||||
diagnostics={currentResponse?.diagnostics ?? []}
|
||||
diagnostics={latestResponse?.diagnostics ?? []}
|
||||
disabledParams={disabledParams}
|
||||
defaultOwner={defaultOwner}
|
||||
owner={owner}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Switch } from "components/Switch/Switch";
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import { type FormikContextType, useFormik } from "formik";
|
||||
import { useDebouncedFunction } from "hooks/debounce";
|
||||
import { ArrowLeft, CircleAlert, TriangleAlert } from "lucide-react";
|
||||
import {
|
||||
DynamicParameter,
|
||||
@@ -141,6 +140,10 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
},
|
||||
});
|
||||
|
||||
const autofillByName = Object.fromEntries(
|
||||
autofillParameters.map((param) => [param.name, param]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
window.scrollTo(0, 0);
|
||||
@@ -218,7 +221,7 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
parameter: PreviewParameter,
|
||||
value: string,
|
||||
) => {
|
||||
const formInputs: { [k: string]: string } = {};
|
||||
const formInputs: Record<string, string> = {};
|
||||
formInputs[parameter.name] = value;
|
||||
const parameters = form.values.rich_parameter_values ?? [];
|
||||
|
||||
@@ -234,37 +237,17 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
sendMessage(formInputs);
|
||||
};
|
||||
|
||||
const { debounced: handleChangeDebounced } = useDebouncedFunction(
|
||||
async (
|
||||
parameter: PreviewParameter,
|
||||
parameterField: string,
|
||||
value: string,
|
||||
) => {
|
||||
await form.setFieldValue(parameterField, {
|
||||
name: parameter.name,
|
||||
value,
|
||||
});
|
||||
form.setFieldTouched(parameter.name, true);
|
||||
sendDynamicParamsRequest(parameter, value);
|
||||
},
|
||||
500,
|
||||
);
|
||||
|
||||
const handleChange = async (
|
||||
parameter: PreviewParameter,
|
||||
parameterField: string,
|
||||
value: string,
|
||||
) => {
|
||||
if (parameter.form_type === "input" || parameter.form_type === "textarea") {
|
||||
handleChangeDebounced(parameter, parameterField, value);
|
||||
} else {
|
||||
await form.setFieldValue(parameterField, {
|
||||
name: parameter.name,
|
||||
value,
|
||||
});
|
||||
form.setFieldTouched(parameter.name, true);
|
||||
sendDynamicParamsRequest(parameter, value);
|
||||
}
|
||||
await form.setFieldValue(parameterField, {
|
||||
name: parameter.name,
|
||||
value,
|
||||
});
|
||||
form.setFieldTouched(parameter.name, true);
|
||||
sendDynamicParamsRequest(parameter, value);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -509,6 +492,9 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
return null;
|
||||
}
|
||||
|
||||
const formValue =
|
||||
form.values?.rich_parameter_values?.[index]?.value || "";
|
||||
|
||||
return (
|
||||
<DynamicParameter
|
||||
key={parameter.name}
|
||||
@@ -518,6 +504,8 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
}
|
||||
disabled={isDisabled}
|
||||
isPreset={isPresetParameter}
|
||||
autofill={autofillByName[parameter.name] !== undefined}
|
||||
value={formValue}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user