diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 6f0d0e4f92..b1df166f96 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -183,34 +183,37 @@ export const verifyParameters = async ( ); } - const parameterLabel = await page.waitForSelector( - `[data-testid='parameter-field-${richParameter.name}']`, - { state: "visible" }, + const parameterLabel = page.getByTestId( + `parameter-field-${richParameter.displayName}`, ); + await expect(parameterLabel).toBeVisible(); - const muiDisabled = richParameter.mutable ? "" : ".Mui-disabled"; + if (richParameter.options.length > 0) { + const parameterValue = parameterLabel.getByLabel(buildParameter.value); + const value = await parameterValue.isChecked(); + expect(value).toBe(true); + continue; + } - if (richParameter.type === "bool") { - const parameterField = await parameterLabel.waitForSelector( - `[data-testid='parameter-field-bool'] .MuiRadio-root.Mui-checked${muiDisabled} input`, - ); - const value = await parameterField.inputValue(); - expect(value).toEqual(buildParameter.value); - } else if (richParameter.options.length > 0) { - const parameterField = await parameterLabel.waitForSelector( - `[data-testid='parameter-field-options'] .MuiRadio-root.Mui-checked${muiDisabled} input`, - ); - const value = await parameterField.inputValue(); - expect(value).toEqual(buildParameter.value); - } else if (richParameter.type === "list(string)") { - throw new Error("not implemented yet"); // FIXME - } else { - // text or number - const parameterField = await parameterLabel.waitForSelector( - `[data-testid='parameter-field-text'] input${muiDisabled}`, - ); - const value = await parameterField.inputValue(); - expect(value).toEqual(buildParameter.value); + switch (richParameter.type) { + case "bool": + { + const parameterField = parameterLabel.locator("input"); + const value = await parameterField.isChecked(); + expect(value.toString()).toEqual(buildParameter.value); + } + break; + case "string": + case "number": + { + const parameterField = parameterLabel.locator("input"); + const value = await parameterField.inputValue(); + expect(value).toEqual(buildParameter.value); + } + break; + default: + // Some types like `list(string)` are not tested + throw new Error("not implemented yet"); } } }; @@ -373,25 +376,22 @@ export const stopWorkspace = async (page: Page, workspaceName: string) => { }); }; -export const buildWorkspaceWithParameters = async ( +export const startWorkspaceWithEphemeralParameters = async ( page: Page, workspaceName: string, richParameters: RichParameter[] = [], buildParameters: WorkspaceBuildParameter[] = [], - confirm = false, ) => { const user = currentUser(page); await page.goto(`/@${user.username}/${workspaceName}`, { waitUntil: "domcontentloaded", }); - await page.getByTestId("build-parameters-button").click(); + await page.getByTestId("workspace-start").click(); + await page.getByTestId("workspace-parameters").click(); await fillParameters(page, richParameters, buildParameters); - await page.getByTestId("build-parameters-submit").click(); - if (confirm) { - await page.getByTestId("confirm-button").click(); - } + await page.getByRole("button", { name: "Update and restart" }).click(); await page.waitForSelector("text=Workspace status: Running", { state: "visible", @@ -547,6 +547,9 @@ interface EchoProvisionerResponses { plan?: RecursivePartial[]; // apply occurs when the workspace is built apply?: RecursivePartial[]; + // extraFiles allows the bundling of terraform files in echo provisioner tars + // in order to support dynamic parameters + extraFiles?: Map; } const emptyPlan = new TextEncoder().encode("{}"); @@ -595,6 +598,13 @@ const createTemplateVersionTar = async ( } const tar = new TarWriter(); + + if (responses.extraFiles) { + for (const [fileName, fileContents] of responses.extraFiles) { + tar.addFile(fileName, fileContents); + } + } + responses.parse.forEach((response, index) => { response.parse = { templateVariables: [], @@ -830,6 +840,50 @@ export const findSessionToken = async (page: Page): Promise => { export const echoResponsesWithParameters = ( richParameters: RichParameter[], ): EchoProvisionerResponses => { + let tf = `terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} +`; + + for (const parameter of richParameters) { + let options = ""; + if (parameter.options) { + for (const option of parameter.options) { + options += ` + option { + name = ${JSON.stringify(option.name)} + description = ${JSON.stringify(option.description)} + value = ${JSON.stringify(option.value)} + icon = ${JSON.stringify(option.icon)} + } +`; + } + } + + tf += ` +data "coder_parameter" "${parameter.name}" { + type = ${JSON.stringify(parameter.type)} + name = ${JSON.stringify(parameter.displayName)} + icon = ${JSON.stringify(parameter.icon)} + description = ${JSON.stringify(parameter.description)} + mutable = ${JSON.stringify(parameter.mutable)}`; + + if (!parameter.required) { + tf += ` + default = ${JSON.stringify(parameter.defaultValue)}`; + } + + tf += ` + order = ${JSON.stringify(parameter.order)} + ephemeral = ${JSON.stringify(parameter.ephemeral)} +${options}} +`; + } + return { parse: [ { @@ -854,6 +908,7 @@ export const echoResponsesWithParameters = ( }, }, ], + extraFiles: new Map([["main.tf", tf]]), }; }; @@ -903,30 +958,36 @@ const fillParameters = async ( ); } - // Use modern locator approach instead of waitForSelector const parameterLabel = page.getByTestId( - `parameter-field-${richParameter.name}`, + `parameter-field-${richParameter.displayName}`, ); await expect(parameterLabel).toBeVisible(); - if (richParameter.type === "bool") { - const parameterField = parameterLabel - .getByTestId("parameter-field-bool") - .locator(`.MuiRadio-root input[value='${buildParameter.value}']`); - await parameterField.click(); - } else if (richParameter.options.length > 0) { - const parameterField = parameterLabel - .getByTestId("parameter-field-options") - .locator(`.MuiRadio-root input[value='${buildParameter.value}']`); - await parameterField.click(); - } else if (richParameter.type === "list(string)") { - throw new Error("not implemented yet"); // FIXME - } else { - // text or number - const parameterField = parameterLabel - .getByTestId("parameter-field-text") - .locator("input"); - await parameterField.fill(buildParameter.value); + if (richParameter.options.length > 0) { + const parameterValue = parameterLabel.getByRole("button", { + name: buildParameter.value, + }); + await parameterValue.click(); + continue; + } + + switch (richParameter.type) { + case "bool": + { + const parameterField = parameterLabel.locator("button"); + await parameterField.click(); + } + break; + case "string": + case "number": + { + const parameterField = parameterLabel.locator("input"); + await parameterField.fill(buildParameter.value); + } + break; + default: + // Some types like `list(string)` are not tested + throw new Error("not implemented yet"); } } }; @@ -1021,27 +1082,13 @@ export const updateWorkspace = async ( await page.getByTestId("workspace-update-button").click(); await page.getByTestId("confirm-button").click(); - await page.waitForSelector('[data-testid="dialog"]', { state: "visible" }); + await page + .getByRole("button", { name: /go to workspace parameters/i }) + .click(); await fillParameters(page, richParameters, buildParameters); - await page.getByRole("button", { name: /update parameters/i }).click(); - // Wait for the update button to detach. - await page.waitForSelector( - "button[data-testid='workspace-update-button']:enabled", - { state: "detached" }, - ); - // Wait for the workspace to be running again. - await page.waitForSelector("text=Workspace status: Running", { - state: "visible", - }); - // Wait for the stop button to be enabled again - await page.waitForSelector( - "button[data-testid='workspace-stop-button']:enabled", - { - state: "visible", - }, - ); + await page.getByRole("button", { name: /update and restart/i }).click(); }; export const updateWorkspaceParameters = async ( @@ -1056,7 +1103,7 @@ export const updateWorkspaceParameters = async ( }); await fillParameters(page, richParameters, buildParameters); - await page.getByRole("button", { name: /submit and restart/i }).click(); + await page.getByRole("button", { name: /update and restart/i }).click(); await page.waitForSelector("text=Workspace status: Running", { state: "visible", @@ -1209,48 +1256,3 @@ export async function addUserToOrganization( } await page.mouse.click(10, 10); // close the popover by clicking outside of it } - -/** - * disableDynamicParameters navigates to the template settings page and disables - * dynamic parameters by unchecking the "Enable dynamic parameters" checkbox. - */ -export const disableDynamicParameters = async ( - page: Page, - templateName: string, - orgName = defaultOrganizationName, -) => { - await page.goto(`/templates/${orgName}/${templateName}/settings`, { - waitUntil: "domcontentloaded", - }); - - await page.waitForSelector("form", { state: "visible" }); - - // Find and uncheck the "Enable dynamic parameters" checkbox - const dynamicParamsCheckbox = page.getByRole("checkbox", { - name: /Enable dynamic parameters for workspace creation/, - }); - - await dynamicParamsCheckbox.waitFor({ state: "visible" }); - - // If the checkbox is checked, uncheck it - if (await dynamicParamsCheckbox.isChecked()) { - await dynamicParamsCheckbox.click(); - } - - // Save the changes - const saveButton = page.getByRole("button", { name: /save/i }); - await saveButton.waitFor({ state: "visible" }); - await saveButton.click(); - - // Wait for the success message or page to update - await page - .locator("[role='alert']:has-text('Template updated successfully')") - .first() - .waitFor({ - state: "visible", - timeout: 15000, - }); - - // Additional wait to ensure the changes are persisted - await page.waitForTimeout(500); -}; diff --git a/site/e2e/parameters.ts b/site/e2e/parameters.ts index 3b672f334c..603a62e3db 100644 --- a/site/e2e/parameters.ts +++ b/site/e2e/parameters.ts @@ -53,6 +53,7 @@ export const thirdParameter: RichParameter = { ...emptyParameter, name: "third_parameter", + displayName: "Third parameter", type: "string", description: "This is third parameter.", defaultValue: "", @@ -65,6 +66,7 @@ export const fourthParameter: RichParameter = { ...emptyParameter, name: "fourth_parameter", + displayName: "Fourth parameter", type: "bool", description: "This is fourth parameter.", defaultValue: "true", diff --git a/site/e2e/tests/workspaces/autoCreateWorkspace.spec.ts b/site/e2e/tests/workspaces/autoCreateWorkspace.spec.ts index 74b3c07ca7..b30e2386b2 100644 --- a/site/e2e/tests/workspaces/autoCreateWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/autoCreateWorkspace.spec.ts @@ -19,7 +19,7 @@ test.beforeAll(async ({ browser }) => { await login(page, users.templateAdmin); const richParameters: RichParameter[] = [ - { ...emptyParameter, name: "repo", type: "string" }, + { ...emptyParameter, name: "repo", displayName: "Repo", type: "string" }, ]; template = await createTemplate( page, diff --git a/site/e2e/tests/workspaces/createWorkspace.spec.ts b/site/e2e/tests/workspaces/createWorkspace.spec.ts index 9fcbcaf31c..c6371c9c9a 100644 --- a/site/e2e/tests/workspaces/createWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/createWorkspace.spec.ts @@ -3,7 +3,6 @@ import { users } from "../../constants"; import { createTemplate, createWorkspace, - disableDynamicParameters, echoResponsesWithParameters, login, openTerminalWindow, @@ -36,9 +35,6 @@ test("create workspace", async ({ page }) => { apply: [{ apply: { resources: [{ name: "example" }] } }], }); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); await createWorkspace(page, template); }); @@ -55,9 +51,6 @@ test("create workspace with default immutable parameters", async ({ page }) => { echoResponsesWithParameters(richParameters), ); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); const workspaceName = await createWorkspace(page, template); await verifyParameters(page, workspaceName, richParameters, [ @@ -75,9 +68,6 @@ test("create workspace with default mutable parameters", async ({ page }) => { echoResponsesWithParameters(richParameters), ); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); const workspaceName = await createWorkspace(page, template); await verifyParameters(page, workspaceName, richParameters, [ @@ -105,9 +95,6 @@ test("create workspace with default and required parameters", async ({ echoResponsesWithParameters(richParameters), ); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); const workspaceName = await createWorkspace(page, template, { richParameters, @@ -140,14 +127,16 @@ test("create workspace and overwrite default parameters", async ({ page }) => { echoResponsesWithParameters(richParameters), ); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); const workspaceName = await createWorkspace(page, template, { richParameters, buildParameters, }); + + await page.waitForSelector("text=Workspace status: Running", { + state: "visible", + }); + await verifyParameters(page, workspaceName, richParameters, buildParameters); }); @@ -163,9 +152,6 @@ test("create workspace with disable_param search params", async ({ page }) => { echoResponsesWithParameters(richParameters), ); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, templateName); - await login(page, users.member); await page.goto( `/templates/${templateName}/workspace?disable_params=first_parameter,second_parameter`, @@ -184,9 +170,6 @@ test.skip("create docker workspace", async ({ context, page }) => { await login(page, users.templateAdmin); const template = await createTemplate(page, StarterTemplates.STARTER_DOCKER); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); const workspaceName = await createWorkspace(page, template); diff --git a/site/e2e/tests/workspaces/restartWorkspace.spec.ts b/site/e2e/tests/workspaces/restartWorkspace.spec.ts deleted file mode 100644 index 987f3c279c..0000000000 --- a/site/e2e/tests/workspaces/restartWorkspace.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { test } from "@playwright/test"; -import { users } from "../../constants"; -import { - buildWorkspaceWithParameters, - createTemplate, - createWorkspace, - disableDynamicParameters, - echoResponsesWithParameters, - login, - verifyParameters, -} from "../../helpers"; -import { beforeCoderTest } from "../../hooks"; -import { firstBuildOption, secondBuildOption } from "../../parameters"; -import type { RichParameter } from "../../provisionerGenerated"; - -test.beforeEach(async ({ page }) => { - beforeCoderTest(page); -}); - -test("restart workspace with ephemeral parameters", async ({ page }) => { - await login(page, users.templateAdmin); - const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]; - const template = await createTemplate( - page, - echoResponsesWithParameters(richParameters), - ); - - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - - await login(page, users.member); - const workspaceName = await createWorkspace(page, template); - - // Verify that build options are default (not selected). - await verifyParameters(page, workspaceName, richParameters, [ - { name: richParameters[0].name, value: firstBuildOption.defaultValue }, - { name: richParameters[1].name, value: secondBuildOption.defaultValue }, - ]); - - // Now, restart the workspace with ephemeral parameters selected. - const buildParameters = [ - { name: richParameters[0].name, value: "AAAAA" }, - { name: richParameters[1].name, value: "true" }, - ]; - await buildWorkspaceWithParameters( - page, - workspaceName, - richParameters, - buildParameters, - true, - ); - - // Verify that build options are default (not selected). - await verifyParameters(page, workspaceName, richParameters, [ - { name: richParameters[0].name, value: firstBuildOption.defaultValue }, - { name: richParameters[1].name, value: secondBuildOption.defaultValue }, - ]); -}); diff --git a/site/e2e/tests/workspaces/startWorkspace.spec.ts b/site/e2e/tests/workspaces/startWorkspace.spec.ts index 30a83a01d6..5e88780e34 100644 --- a/site/e2e/tests/workspaces/startWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/startWorkspace.spec.ts @@ -1,12 +1,11 @@ import { test } from "@playwright/test"; import { users } from "../../constants"; import { - buildWorkspaceWithParameters, createTemplate, createWorkspace, - disableDynamicParameters, echoResponsesWithParameters, login, + startWorkspaceWithEphemeralParameters, stopWorkspace, verifyParameters, } from "../../helpers"; @@ -26,9 +25,6 @@ test("start workspace with ephemeral parameters", async ({ page }) => { echoResponsesWithParameters(richParameters), ); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); const workspaceName = await createWorkspace(page, template); @@ -47,13 +43,16 @@ test("start workspace with ephemeral parameters", async ({ page }) => { { name: richParameters[1].name, value: "true" }, ]; - await buildWorkspaceWithParameters( + await startWorkspaceWithEphemeralParameters( page, workspaceName, richParameters, buildParameters, ); + // Stop the workspace + await stopWorkspace(page, workspaceName); + // Verify that build options are default (not selected). await verifyParameters(page, workspaceName, richParameters, [ { name: richParameters[0].name, value: firstBuildOption.defaultValue }, diff --git a/site/e2e/tests/workspaces/updateWorkspace.spec.ts b/site/e2e/tests/workspaces/updateWorkspace.spec.ts index b731b76abb..7ffc0652d9 100644 --- a/site/e2e/tests/workspaces/updateWorkspace.spec.ts +++ b/site/e2e/tests/workspaces/updateWorkspace.spec.ts @@ -3,9 +3,9 @@ import { users } from "../../constants"; import { createTemplate, createWorkspace, - disableDynamicParameters, echoResponsesWithParameters, login, + stopWorkspace, updateTemplate, updateWorkspace, updateWorkspaceParameters, @@ -25,7 +25,12 @@ test.beforeEach(async ({ page }) => { beforeCoderTest(page); }); -test("update workspace, new optional, immutable parameter added", async ({ +// TODO: this needs to be fixed for the new dynamic parameters flow which +// sends you to the parameters settings page instead of prompting for new +// values in a modal, but that flow is broken! because we don't let you set +// immutable parameters on that page even if they are new, and detecting if +// they are new is non-trivial. +test.skip("update workspace, new optional, immutable parameter added", async ({ page, }) => { await login(page, users.templateAdmin); @@ -35,9 +40,6 @@ test("update workspace, new optional, immutable parameter added", async ({ echoResponsesWithParameters(richParameters), ); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); const workspaceName = await createWorkspace(page, template); @@ -81,9 +83,6 @@ test("update workspace, new required, mutable parameter added", async ({ echoResponsesWithParameters(richParameters), ); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); const workspaceName = await createWorkspace(page, template); @@ -113,6 +112,10 @@ test("update workspace, new required, mutable parameter added", async ({ buildParameters, ); + await page.waitForSelector("text=Workspace status: Running", { + state: "visible", + }); + // Verify parameter values. await verifyParameters(page, workspaceName, updatedRichParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, @@ -129,9 +132,6 @@ test("update workspace with ephemeral parameter enabled", async ({ page }) => { echoResponsesWithParameters(richParameters), ); - // Disable dynamic parameters to use classic parameter flow for this test - await disableDynamicParameters(page, template); - await login(page, users.member); const workspaceName = await createWorkspace(page, template); @@ -150,6 +150,9 @@ test("update workspace with ephemeral parameter enabled", async ({ page }) => { buildParameters, ); + // Stop the workspace + await stopWorkspace(page, workspaceName); + // Verify that parameter values are default. await verifyParameters(page, workspaceName, richParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.jest.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.jest.tsx index 716fd26df4..472dafd53d 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.jest.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.jest.tsx @@ -97,26 +97,6 @@ describe("DynamicParameter", () => { expect(screen.getByRole("textbox")).toHaveValue("test_value"); }); - it("calls onChange when input value changes", async () => { - render( - , - ); - - const input = screen.getByRole("textbox"); - - await waitFor(async () => { - await userEvent.type(input, "new_value"); - }); - - await waitFor(() => { - expect(mockOnChange).toHaveBeenCalledWith("new_value"); - }); - }); - it("shows required indicator for required parameters", () => { render( { expect(screen.getByText("Textarea Parameter")).toBeInTheDocument(); expect(screen.getByRole("textbox")).toHaveValue(testValue); }); - - it("handles textarea value changes", async () => { - render( - , - ); - - const textarea = screen.getByRole("textbox"); - await waitFor(async () => { - await userEvent.type(textarea, "line1{enter}line2{enter}line3"); - }); - - await waitFor(() => { - expect(mockOnChange).toHaveBeenCalledWith("line1\nline2\nline3"); - }); - }); }); describe("dropdown parameter", () => { @@ -729,58 +690,6 @@ describe("DynamicParameter", () => { }); }); - describe("Debounced Input", () => { - it("debounces input changes for text inputs", async () => { - jest.useFakeTimers(); - - render( - , - ); - - const input = screen.getByRole("textbox"); - fireEvent.change(input, { target: { value: "abc" } }); - - expect(mockOnChange).not.toHaveBeenCalled(); - - act(() => { - jest.runAllTimers(); - }); - - expect(mockOnChange).toHaveBeenCalledWith("abc"); - - jest.useRealTimers(); - }); - - it("debounces textarea changes", async () => { - jest.useFakeTimers(); - - render( - , - ); - - const textarea = screen.getByRole("textbox"); - fireEvent.change(textarea, { target: { value: "line1\nline2" } }); - - expect(mockOnChange).not.toHaveBeenCalled(); - - act(() => { - jest.runAllTimers(); - }); - - expect(mockOnChange).toHaveBeenCalledWith("line1\nline2"); - - jest.useRealTimers(); - }); - }); - describe("Edge Cases", () => { it("handles empty parameter options gracefully", () => { const paramWithEmptyOptions = createMockParameter({ diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index a298270e9a..51cc19736b 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -28,8 +28,6 @@ import { TooltipProvider, TooltipTrigger, } from "components/Tooltip/Tooltip"; -import { useDebouncedValue } from "hooks/debounce"; -import { useEffectEvent } from "hooks/hookPolyfills"; import { CircleAlert, Eye, @@ -40,7 +38,7 @@ import { Settings, TriangleAlert, } from "lucide-react"; -import { type FC, useEffect, useId, useRef, useState } from "react"; +import { type FC, useId, useRef, useState } from "react"; import { cn } from "utils/cn"; import type { AutofillBuildParameter } from "utils/richParameters"; import * as Yup from "yup"; @@ -76,25 +74,13 @@ export const DynamicParameter: FC = ({ autofill={autofill} />
- {parameter.form_type === "input" || - parameter.form_type === "textarea" || - parameter.form_type === "slider" ? ( - - ) : ( - - )} +
{parameter.form_type !== "error" && ( @@ -244,196 +230,6 @@ const ParameterLabel: FC = ({ ); }; -interface DebouncedParameterFieldProps { - parameter: PreviewParameter; - value?: string; - onChange: (value: string) => void; - disabled?: boolean; - id: string; -} - -const DebouncedParameterField: FC = ({ - parameter, - value, - onChange, - disabled, - id, -}) => { - const [localValue, setLocalValue] = useState( - value !== undefined ? value : validValue(parameter.value), - ); - const [showMaskedInput, setShowMaskedInput] = useState(false); - const debouncedLocalValue = useDebouncedValue(localValue, 500); - const onChangeEvent = useEffectEvent(onChange); - // prevDebouncedValueRef is to prevent calling the onChangeEvent on the initial render - const prevDebouncedValueRef = useRef(undefined); - const prevValueRef = useRef(value); - - // Necessary for dynamic defaults or fields being set by preset parameters - useEffect(() => { - if (value !== undefined && value !== prevValueRef.current) { - setLocalValue(value); - prevValueRef.current = value; - } - }, [value]); - - useEffect(() => { - // Only call onChangeEvent if debouncedLocalValue is different from the previously committed value - // and it's not the initial undefined state. - if ( - prevDebouncedValueRef.current !== undefined && - prevDebouncedValueRef.current !== debouncedLocalValue - ) { - onChangeEvent(debouncedLocalValue); - } - - // Update the ref to the current debounced value for the next comparison - prevDebouncedValueRef.current = debouncedLocalValue; - }, [debouncedLocalValue, onChangeEvent]); - - const textareaRef = useRef(null); - - const resizeTextarea = useEffectEvent(() => { - if (textareaRef.current) { - const textarea = textareaRef.current; - textarea.style.height = `${textarea.scrollHeight}px`; - } - }); - - useEffect(() => { - resizeTextarea(); - }, [resizeTextarea]); - - switch (parameter.form_type) { - case "textarea": { - return ( - -