From 3b8a9ff802a3aa6406c73c8f7278f29062085494 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 28 May 2026 07:42:53 +0200 Subject: [PATCH] feat: add preset query parameter for workspace creation deeplinks (#24328) Co-authored-by: Atif Ali --- site/src/api/queries/workspaces.ts | 3 + .../AutoCreateConsentDialog.stories.tsx | 15 +- .../AutoCreateConsentDialog.tsx | 13 ++ .../CreateWorkspacePage.test.tsx | 171 +++++++++++++++++- .../CreateWorkspacePage.tsx | 117 +++++++++++- .../CreateWorkspacePageView.stories.tsx | 102 ++++++++++- .../CreateWorkspacePageView.tsx | 36 +++- 7 files changed, 441 insertions(+), 16 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 5782c32d18..ea6ec316ad 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -148,6 +148,7 @@ type AutoCreateWorkspaceOptions = { match: string | null; templateVersionId?: string; buildParameters?: WorkspaceBuildParameter[]; + templateVersionPresetId?: string; }; export const autoCreateWorkspace = (queryClient: QueryClient) => { @@ -158,6 +159,7 @@ export const autoCreateWorkspace = (queryClient: QueryClient) => { workspaceName, templateVersionId, buildParameters, + templateVersionPresetId, match, }: AutoCreateWorkspaceOptions) => { if (match) { @@ -185,6 +187,7 @@ export const autoCreateWorkspace = (queryClient: QueryClient) => { ...templateVersionParameters, name: workspaceName, rich_parameter_values: buildParameters, + template_version_preset_id: templateVersionPresetId, }); }, onSuccess: async () => { diff --git a/site/src/pages/CreateWorkspacePage/AutoCreateConsentDialog.stories.tsx b/site/src/pages/CreateWorkspacePage/AutoCreateConsentDialog.stories.tsx index 842d25aee0..8bd0dd78bc 100644 --- a/site/src/pages/CreateWorkspacePage/AutoCreateConsentDialog.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/AutoCreateConsentDialog.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; +import { expect, fn, screen } from "storybook/test"; import { AutoCreateConsentDialog } from "./AutoCreateConsentDialog"; const meta: Meta = { @@ -77,6 +77,19 @@ export const WithLongValues: Story = { }, }; +export const WithPreset: Story = { + args: { + presetName: "gpu-large", + autofillParameters: [ + { name: "instance_type", value: "g6.4xlarge", source: "url" }, + ], + }, + play: async () => { + expect(screen.getAllByText("Preset:").length).toBeGreaterThan(0); + expect(screen.getAllByText("gpu-large").length).toBeGreaterThan(0); + }, +}; + export const NoParameters: Story = { args: { autofillParameters: [], diff --git a/site/src/pages/CreateWorkspacePage/AutoCreateConsentDialog.tsx b/site/src/pages/CreateWorkspacePage/AutoCreateConsentDialog.tsx index 58c3e552fc..8dfc900449 100644 --- a/site/src/pages/CreateWorkspacePage/AutoCreateConsentDialog.tsx +++ b/site/src/pages/CreateWorkspacePage/AutoCreateConsentDialog.tsx @@ -14,6 +14,7 @@ import type { AutofillBuildParameter } from "#/utils/richParameters"; interface AutoCreateConsentDialogProps { open: boolean; autofillParameters: AutofillBuildParameter[]; + presetName?: string; onConfirm: () => void; onDeny: () => void; } @@ -21,6 +22,7 @@ interface AutoCreateConsentDialogProps { export const AutoCreateConsentDialog: FC = ({ open, autofillParameters, + presetName, onConfirm, onDeny, }) => { @@ -43,6 +45,17 @@ export const AutoCreateConsentDialog: FC = ({ + {presetName && ( +
+ + Preset: + + + {presetName} + +
+ )} + {autofillParameters.length > 0 && (
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 97547f9566..1b7963cf2a 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -2,7 +2,7 @@ import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { act } from "react"; import { API } from "#/api/api"; -import type { DynamicParametersResponse } from "#/api/typesGenerated"; +import type { DynamicParametersResponse, Preset } from "#/api/typesGenerated"; import { MockDropdownParameter, MockDynamicParametersResponse, @@ -11,6 +11,7 @@ import { MockPreviewParameter, MockSliderParameter, MockTemplate, + MockTemplateVersion, MockTemplateVersionExternalAuthGithub, MockTemplateVersionExternalAuthGithubAuthenticated, MockUserOwner, @@ -40,10 +41,24 @@ describe("CreateWorkspacePage", () => { }); }; + const mockGpuPreset: Preset = { + ID: "preset-gpu", + Name: "gpu-large", + Parameters: [ + { Name: "instance_type", Value: "t3.medium" }, + { Name: "cpu_count", Value: "4" }, + ], + Default: false, + DesiredPrebuildInstances: null, + Description: "GPU Large preset", + Icon: "", + }; + beforeEach(() => { vi.clearAllMocks(); vi.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); + vi.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion); vi.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]); vi.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([]); vi.spyOn(API, "createWorkspace").mockResolvedValue(MockWorkspace); @@ -446,7 +461,7 @@ describe("CreateWorkspacePage", () => { `/templates/${MockTemplate.name}/workspace?mode=auto`, ); - // Consent dialog appears for mode=auto — confirm to proceed. + // Consent dialog appears for mode=auto. Confirm to proceed. const confirmButton = await screen.findByRole("button", { name: /confirm and create/i, }); @@ -550,6 +565,158 @@ describe("CreateWorkspacePage", () => { }); }); + describe("URL Presets", () => { + it("resolves a preset from the URL and selects it in the form", async () => { + vi.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([ + mockGpuPreset, + ]); + + renderCreateWorkspacePage( + `/templates/${MockTemplate.name}/workspace?preset=gpu-large`, + ); + await waitForLoaderToBeRemoved(); + + expect( + screen.getByRole("button", { name: /gpu-large/i }), + ).toBeInTheDocument(); + }); + + it("resolves a preset against the pinned template version", async () => { + const getTemplateVersionPresetsSpy = vi + .spyOn(API, "getTemplateVersionPresets") + .mockResolvedValue([mockGpuPreset]); + + renderCreateWorkspacePage( + `/templates/${MockTemplate.name}/workspace?version=custom-version&preset=gpu-large`, + ); + + await waitFor(() => { + expect(getTemplateVersionPresetsSpy).toHaveBeenCalledWith( + "custom-version", + ); + }); + }); + + it("falls back to form mode when auto-create cannot resolve the preset", async () => { + vi.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([ + MockTemplateVersionExternalAuthGithubAuthenticated, + ]); + vi.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([ + mockGpuPreset, + ]); + + renderCreateWorkspacePage( + `/templates/${MockTemplate.name}/workspace?mode=auto&preset=missing`, + ); + await waitForLoaderToBeRemoved(); + + expect( + screen.queryByRole("button", { name: /confirm and create/i }), + ).not.toBeInTheDocument(); + expect( + screen.getByText(/auto-creation has been disabled/i), + ).toBeInTheDocument(); + expect( + screen.getByText( + /preset "missing" not found on template version "test-version"/i, + ), + ).toBeInTheDocument(); + expect(API.createWorkspace).not.toHaveBeenCalled(); + }); + + it("falls back to form mode when presets fail to load", async () => { + vi.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([ + MockTemplateVersionExternalAuthGithubAuthenticated, + ]); + vi.spyOn(API, "getTemplateVersionPresets").mockRejectedValue( + new Error("presets unavailable"), + ); + + renderCreateWorkspacePage( + `/templates/${MockTemplate.name}/workspace?mode=auto&preset=gpu-large`, + ); + await waitForLoaderToBeRemoved(); + + expect( + screen.queryByRole("button", { name: /confirm and create/i }), + ).not.toBeInTheDocument(); + expect( + screen.getByText(/auto-creation has been disabled/i), + ).toBeInTheDocument(); + expect( + screen.getByText(/failed to load presets: presets unavailable/i), + ).toBeInTheDocument(); + expect(API.createWorkspace).not.toHaveBeenCalled(); + }); + + it("uses preset parameters instead of param values", async () => { + vi.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([ + mockGpuPreset, + ]); + + renderCreateWorkspacePage( + `/templates/${MockTemplate.name}/workspace?preset=gpu-large¶m.instance_type=t3.small¶m.cpu_count=99`, + ); + await waitForLoaderToBeRemoved(); + + expect(screen.getAllByText(/param\.\*/i).length).toBeGreaterThan(0); + + const nameInput = screen.getByRole("textbox", { + name: /workspace name/i, + }); + await userEvent.type(nameInput, "preset-workspace"); + + await userEvent.click( + screen.getByRole("button", { name: /create workspace/i }), + ); + + await waitFor(() => { + expect(API.createWorkspace).toHaveBeenCalledWith( + "test-user", + expect.objectContaining({ + template_version_preset_id: mockGpuPreset.ID, + rich_parameter_values: expect.arrayContaining([ + expect.objectContaining({ + name: "instance_type", + value: "t3.medium", + }), + expect.objectContaining({ name: "cpu_count", value: "4" }), + ]), + }), + ); + }); + }); + + it("auto-creates with the preset ID after the preset resolves", async () => { + vi.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([ + MockTemplateVersionExternalAuthGithubAuthenticated, + ]); + vi.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([ + mockGpuPreset, + ]); + + renderCreateWorkspacePage( + `/templates/${MockTemplate.name}/workspace?mode=auto&preset=gpu-large&name=preset-workspace`, + ); + + const confirmButton = await screen.findByRole("button", { + name: /confirm and create/i, + }); + await userEvent.click(confirmButton); + + await waitFor(() => { + expect(API.createWorkspace).toHaveBeenCalledWith( + "me", + expect.objectContaining({ + name: "preset-workspace", + template_version_preset_id: mockGpuPreset.ID, + rich_parameter_values: [], + }), + ); + }); + }); + }); + describe("Navigation", () => { it("navigates to workspace after successful creation", async () => { const { router } = renderCreateWorkspacePage(); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index 5dc9ab5f70..74a8356bcd 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -60,6 +60,7 @@ const CreateWorkspacePage: FC = () => { const customVersionId = searchParams.get("version") ?? undefined; const defaultName = searchParams.get("name"); const disabledParams = searchParams.get("disable_params")?.split(","); + const presetName = searchParams.get("preset") || undefined; const [mode, setMode] = useState(() => getWorkspaceMode(searchParams)); const [autoCreateConsented, setAutoCreateConsented] = useState(false); const [autoCreateError, setAutoCreateError] = @@ -76,9 +77,12 @@ const CreateWorkspacePage: FC = () => { const templateQuery = useQuery( templateByName(organizationName, templateName), ); + const realizedVersionId = + customVersionId ?? templateQuery.data?.active_version_id; + const templateVersionPresetsQuery = useQuery({ - ...templateVersionPresets(templateQuery.data?.active_version_id ?? ""), - enabled: Boolean(templateQuery.data), + ...templateVersionPresets(realizedVersionId ?? ""), + enabled: realizedVersionId !== undefined, }); const permissionsQuery = useQuery({ ...checkAuthorization({ @@ -89,15 +93,68 @@ const CreateWorkspacePage: FC = () => { }), enabled: Boolean(templateQuery.data), }); - const realizedVersionId = - customVersionId ?? templateQuery.data?.active_version_id; const templateVersionQuery = useQuery({ ...templateVersion(realizedVersionId ?? ""), enabled: realizedVersionId !== undefined, }); - const autofillParameters = getAutofillParameters(searchParams); + const effectivePresetName = mode === "duplicate" ? undefined : presetName; + + const presets = templateVersionPresetsQuery.data ?? []; + + const urlPresetResult = useMemo(() => { + if (!effectivePresetName) return { preset: undefined, error: undefined }; + + if (templateVersionPresetsQuery.isError) { + return { + preset: undefined, + error: `Failed to load presets: ${templateVersionPresetsQuery.error?.message ?? "unknown error"}. Please try refreshing the page.`, + }; + } + + if (!templateVersionPresetsQuery.isSuccess) { + return { preset: undefined, error: undefined }; // Still loading + } + + const found = presets.find((p) => p.Name === effectivePresetName); + if (!found) { + const versionLabel = templateVersionQuery.data?.name ?? realizedVersionId; + return { + preset: undefined, + error: `Preset "${effectivePresetName}" not found on template version "${versionLabel}". Check that the preset name matches exactly (names are case-sensitive).`, + }; + } + return { preset: found, error: undefined }; + }, [ + effectivePresetName, + presets, + templateVersionPresetsQuery.isSuccess, + templateVersionPresetsQuery.isError, + templateVersionPresetsQuery.error, + realizedVersionId, + templateVersionQuery.data?.name, + ]); + + const urlAutofillParameters = useMemo( + () => getAutofillParameters(searchParams), + [searchParams], + ); + const autofillParameters = useMemo(() => { + if (!urlPresetResult.preset) return urlAutofillParameters; + + const presetParams: AutofillBuildParameter[] = + urlPresetResult.preset.Parameters.map((p) => ({ + name: p.Name, + value: p.Value, + source: "url" as const, + })); + + return presetParams; + }, [urlPresetResult.preset, urlAutofillParameters]); + + const hasIgnoredUrlParams = + urlAutofillParameters.length > 0 && urlPresetResult.preset !== undefined; const sendMessage = ( formValues: Record, @@ -227,10 +284,11 @@ const CreateWorkspacePage: FC = () => { const newWorkspace = await autoCreateWorkspaceMutation.mutateAsync({ organizationId, templateName, - buildParameters: autofillParameters, + buildParameters: urlPresetResult.preset ? [] : autofillParameters, workspaceName: defaultName ?? generateWorkspaceName(), templateVersionId: realizedVersionId, match: searchParams.get("match"), + templateVersionPresetId: urlPresetResult.preset?.ID, }); onCreateWorkspace(newWorkspace); @@ -244,11 +302,22 @@ const CreateWorkspacePage: FC = () => { externalAuth?.every((auth) => auth.optional || auth.authenticated), ); + const presetResolved = + !effectivePresetName || + (templateVersionPresetsQuery.isSuccess && + urlPresetResult.preset !== undefined); + let autoCreateReady = - mode === "auto" && hasAllRequiredExternalAuth && autoCreateConsented; + mode === "auto" && + hasAllRequiredExternalAuth && + autoCreateConsented && + presetResolved; const showAutoCreateConsent = - mode === "auto" && !autoCreateConsented && !autoCreateError; + mode === "auto" && + !autoCreateConsented && + !autoCreateError && + presetResolved; // `mode=auto` was set, but a prerequisite has failed, and so auto-mode should be abandoned. if ( @@ -275,6 +344,23 @@ const CreateWorkspacePage: FC = () => { }); } + if ( + mode === "auto" && + hasAllRequiredExternalAuth && + effectivePresetName && + ((templateVersionPresetsQuery.isSuccess && !urlPresetResult.preset) || + templateVersionPresetsQuery.isError) + ) { + setMode("form"); + autoCreateReady = false; + setAutoCreateError({ + message: "Auto-creation has been disabled.", + detail: + urlPresetResult.error ?? + "The requested preset could not be resolved. Please check the preset value before continuing.", + }); + } + useEffect(() => { if (autoCreateReady) { void automateWorkspaceCreation(); @@ -293,7 +379,10 @@ const CreateWorkspacePage: FC = () => { isLoadingFormData || isLoadingExternalAuth || autoCreateReady || - (!latestResponse && !wsError); + (!latestResponse && !wsError) || + (effectivePresetName && + !templateVersionPresetsQuery.isSuccess && + !templateVersionPresetsQuery.isError); return ( <> @@ -301,6 +390,7 @@ const CreateWorkspacePage: FC = () => { setAutoCreateConsented(true)} onDeny={() => setMode("form")} @@ -336,7 +426,14 @@ const CreateWorkspacePage: FC = () => { hasAllRequiredExternalAuth={hasAllRequiredExternalAuth} permissions={permissionsQuery.data as CreateWorkspacePermissions} parameters={sortedParams} - presets={templateVersionPresetsQuery.data ?? []} + presets={presets} + urlPreset={urlPresetResult.preset} + urlPresetError={ + autoCreateError?.detail === urlPresetResult.error + ? undefined + : urlPresetResult.error + } + hasIgnoredUrlParams={hasIgnoredUrlParams} creatingWorkspace={createWorkspaceMutation.isPending} sendMessage={sendMessage} onCancel={() => { diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index b689d72bd0..ea10e83b2c 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, screen, within } from "storybook/test"; import { DetailedError } from "#/api/errors"; -import type { PreviewParameter } from "#/api/typesGenerated"; +import type { Preset, PreviewParameter } from "#/api/typesGenerated"; import { chromatic } from "#/testHelpers/chromatic"; import { MockTemplate, MockUserOwner } from "#/testHelpers/entities"; import { CreateWorkspacePageView } from "./CreateWorkspacePageView"; @@ -277,6 +277,39 @@ const parameterTextarea: PreviewParameter = { ephemeral: false, }; +const gpuLargePreset: Preset = { + ID: "preset-1", + Name: "GPU Large", + Description: "GPU Large preset", + Parameters: [ + { Name: "instance_type", Value: "t3.large" }, + { Name: "enable_gpu", Value: "true" }, + ], + Default: false, + DesiredPrebuildInstances: null, + Icon: "/emojis/1f4bb.png", +}; + +const cpuSmallPreset: Preset = { + ID: "preset-2", + Name: "CPU Small", + Description: "CPU Small preset", + Parameters: [{ Name: "instance_type", Value: "t3.micro" }], + Default: false, + DesiredPrebuildInstances: null, + Icon: "/emojis/1f4bc.png", +}; + +const urlPreset: Preset = { + ID: "preset-url", + Name: "URL Preset", + Description: "The URL-specified preset", + Parameters: [{ Name: "instance_type", Value: "t3.large" }], + Default: false, + DesiredPrebuildInstances: null, + Icon: "/emojis/1f534.png", +}; + const parameterCheckbox: PreviewParameter = { name: "auto_stop", display_name: "Auto-stop", @@ -356,3 +389,70 @@ export const WithPresets: Story = { parameters: [parameterInput, parameterDropdown], }, }; + +export const WithUrlPreset: Story = { + args: { + presets: [gpuLargePreset, cpuSmallPreset], + urlPreset: gpuLargePreset, + parameters: [parameterDropdown, parameterSwitch], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect( + canvas.getByRole("button", { name: /GPU Large/i }), + ).toBeInTheDocument(); + }, +}; + +export const WithUrlPresetNotFound: Story = { + args: { + presets: [gpuLargePreset], + urlPresetError: + 'Preset "gpu-large" not found on template version "test-version". Check that the preset name matches exactly (names are case-sensitive).', + parameters: [parameterDropdown], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect( + canvas.getByText(/Preset "gpu-large" not found on template version/i), + ).toBeVisible(); + }, +}; + +export const WithUrlPresetAndIgnoredParams: Story = { + args: { + presets: [gpuLargePreset], + urlPreset: gpuLargePreset, + hasIgnoredUrlParams: true, + parameters: [parameterDropdown], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getAllByText(/param\.\*/i).length).toBeGreaterThan(0); + }, +}; + +export const WithUrlPresetOverridesDefault: Story = { + args: { + presets: [ + { + ID: "preset-default", + Name: "Default Preset", + Description: "The default preset", + Parameters: [{ Name: "instance_type", Value: "t3.micro" }], + Default: true, + DesiredPrebuildInstances: null, + Icon: "/emojis/1f7e2.png", + }, + urlPreset, + ], + urlPreset, + parameters: [parameterDropdown], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect( + canvas.getByRole("button", { name: /URL Preset/i }), + ).toBeInTheDocument(); + }, +}; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 167b93838d..fb12e3fc9a 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -68,11 +68,14 @@ interface CreateWorkspacePageViewProps { externalAuth: TypesGen.TemplateVersionExternalAuth[]; externalAuthPollingState: ExternalAuthPollingState; hasAllRequiredExternalAuth: boolean; + hasIgnoredUrlParams?: boolean; mode: CreateWorkspaceMode; parameters: PreviewParameter[]; permissions: CreateWorkspacePermissions; presets: TypesGen.Preset[]; template: TypesGen.Template; + urlPreset?: TypesGen.Preset; + urlPresetError?: string; versionId?: string; versionName?: string; onCancel: () => void; @@ -99,11 +102,14 @@ export const CreateWorkspacePageView: FC = ({ externalAuth, externalAuthPollingState, hasAllRequiredExternalAuth, + hasIgnoredUrlParams, mode, parameters, permissions, presets = [], template, + urlPreset, + urlPresetError, versionId, versionName, onSubmit, @@ -202,6 +208,15 @@ export const CreateWorkspacePageView: FC = ({ })), ]; setPresetOptions(options); + + // URL preset takes precedence over default preset. + if (urlPreset) { + const idx = presets.findIndex((p) => p.ID === urlPreset.ID) + 1; + setSelectedPresetIndex(idx); + form.setFieldValue("template_version_preset_id", urlPreset.ID); + return; + } + const defaultPreset = presets.find((p) => p.Default); if (defaultPreset) { const idx = presets.indexOf(defaultPreset) + 1; // +1 for "None" @@ -211,7 +226,7 @@ export const CreateWorkspacePageView: FC = ({ setSelectedPresetIndex(0); // Explicitly set to "None" form.setFieldValue("template_version_preset_id", undefined); } - }, [presets, form.setFieldValue]); + }, [presets, form.setFieldValue, urlPreset]); const [presetParameterNames, setPresetParameterNames] = useState( [], @@ -451,6 +466,20 @@ export const CreateWorkspacePageView: FC = ({ > {Boolean(error) && } + {urlPresetError && ( + + {urlPresetError} + + )} + + {hasIgnoredUrlParams && urlPreset && ( + + Preset selected. param.* URL parameters have been + ignored. Use either preset or param.*, + not both. + + )} + {mode === "duplicate" && ( = ({ } disabled={isDisabled} isPreset={isPresetParameter} - autofill={autofillByName[parameter.name] !== undefined} + autofill={ + !isPresetParameter && + autofillByName[parameter.name] !== undefined + } value={formValue} /> );