diff --git a/cli/ssh.go b/cli/ssh.go index 5cc81284ca317..51f53e10bcbd2 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -1594,12 +1594,14 @@ func writeCoderConnectNetInfo(ctx context.Context, networkInfoDir string) error // Converts workspace name input to owner/workspace.agent format // Possible valid input formats: // workspace +// workspace.agent // owner/workspace // owner--workspace // owner/workspace--agent // owner/workspace.agent // owner--workspace--agent // owner--workspace.agent +// agent.workspace.owner - for parity with Coder Connect func normalizeWorkspaceInput(input string) string { // Split on "/", "--", and "." parts := workspaceNameRe.Split(input, -1) @@ -1608,8 +1610,15 @@ func normalizeWorkspaceInput(input string) string { case 1: return input // "workspace" case 2: + if strings.Contains(input, ".") { + return fmt.Sprintf("%s.%s", parts[0], parts[1]) // "workspace.agent" + } return fmt.Sprintf("%s/%s", parts[0], parts[1]) // "owner/workspace" case 3: + // If the only separator is a dot, it's the Coder Connect format + if !strings.Contains(input, "/") && !strings.Contains(input, "--") { + return fmt.Sprintf("%s/%s.%s", parts[2], parts[1], parts[0]) // "owner/workspace.agent" + } return fmt.Sprintf("%s/%s.%s", parts[0], parts[1], parts[2]) // "owner/workspace.agent" default: return input // Fallback diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 147fc07372032..8845200273697 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -107,12 +107,14 @@ func TestSSH(t *testing.T) { cases := []string{ "myworkspace", + "myworkspace.dev", "myuser/myworkspace", "myuser--myworkspace", "myuser/myworkspace--dev", "myuser/myworkspace.dev", "myuser--myworkspace--dev", "myuser--myworkspace.dev", + "dev.myworkspace.myuser", } for _, tc := range cases { diff --git a/coderd/coderd.go b/coderd/coderd.go index 37e7d22a6d080..3c88383fd2ea5 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -860,7 +860,7 @@ func New(options *Options) *API { next.ServeHTTP(w, r) }) }, - // httpmw.CSRF(options.DeploymentValues.HTTPCookies), + httpmw.CSRF(options.DeploymentValues.HTTPCookies), ) // This incurs a performance hit from the middleware, but is required to make sure diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go index 91809d3a037d6..98a5d546eaffc 100644 --- a/coderd/parameters_test.go +++ b/coderd/parameters_test.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/wsjson" "github.com/coder/coder/v2/provisioner/echo" @@ -211,6 +212,86 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) { require.Zero(t, setup.api.FileCache.Count()) }) + t.Run("RebuildParameters", func(t *testing.T) { + t.Parallel() + + dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/modules/main.tf") + require.NoError(t, err) + + modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules")) + require.NoError(t, err) + + setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{ + provisionerDaemonVersion: provProto.CurrentVersion.String(), + mainTF: dynamicParametersTerraformSource, + modulesArchive: modulesArchive, + plan: nil, + static: nil, + }) + + ctx := testutil.Context(t, testutil.WaitMedium) + stream := setup.stream + previews := stream.Chan() + + // Should see the output of the module represented + preview := testutil.RequireReceive(ctx, t, previews) + require.Equal(t, -1, preview.ID) + require.Empty(t, preview.Diagnostics) + + require.Len(t, preview.Parameters, 1) + require.Equal(t, "jetbrains_ide", preview.Parameters[0].Name) + require.True(t, preview.Parameters[0].Value.Valid) + require.Equal(t, "CL", preview.Parameters[0].Value.Value) + _ = stream.Close(websocket.StatusGoingAway) + + wrk := coderdtest.CreateWorkspace(t, setup.client, setup.template.ID, func(request *codersdk.CreateWorkspaceRequest) { + request.RichParameterValues = []codersdk.WorkspaceBuildParameter{ + { + Name: preview.Parameters[0].Name, + Value: "GO", + }, + } + }) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, setup.client, wrk.LatestBuild.ID) + + params, err := setup.client.WorkspaceBuildParameters(ctx, wrk.LatestBuild.ID) + require.NoError(t, err) + require.Len(t, params, 1) + require.Equal(t, "jetbrains_ide", params[0].Name) + require.Equal(t, "GO", params[0].Value) + + // A helper function to assert params + doTransition := func(t *testing.T, trans codersdk.WorkspaceTransition) { + t.Helper() + + fooVal := coderdtest.RandomUsername(t) + bld, err := setup.client.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: setup.template.ActiveVersionID, + Transition: trans, + RichParameterValues: []codersdk.WorkspaceBuildParameter{ + // No validation, so this should work as is. + // Overwrite the value on each transition + {Name: "foo", Value: fooVal}, + }, + EnableDynamicParameters: ptr.Ref(true), + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, setup.client, wrk.LatestBuild.ID) + + latestParams, err := setup.client.WorkspaceBuildParameters(ctx, bld.ID) + require.NoError(t, err) + require.ElementsMatch(t, latestParams, []codersdk.WorkspaceBuildParameter{ + {Name: "jetbrains_ide", Value: "GO"}, + {Name: "foo", Value: fooVal}, + }) + } + + // Restart the workspace, then delete. Asserting params on all builds. + doTransition(t, codersdk.WorkspaceTransitionStop) + doTransition(t, codersdk.WorkspaceTransitionStart) + doTransition(t, codersdk.WorkspaceTransitionDelete) + }) + t.Run("BadOwner", func(t *testing.T) { t.Parallel() @@ -266,9 +347,10 @@ type setupDynamicParamsTestParams struct { } type dynamicParamsTest struct { - client *codersdk.Client - api *coderd.API - stream *wsjson.Stream[codersdk.DynamicParametersResponse, codersdk.DynamicParametersRequest] + client *codersdk.Client + api *coderd.API + stream *wsjson.Stream[codersdk.DynamicParametersResponse, codersdk.DynamicParametersRequest] + template codersdk.Template } func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dynamicParamsTest { @@ -300,7 +382,7 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn version := coderdtest.CreateTemplateVersion(t, templateAdmin, owner.OrganizationID, files) coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID) - _ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) + tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitShort) stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID) @@ -321,9 +403,10 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn }) return dynamicParamsTest{ - client: ownerClient, - stream: stream, - api: api, + client: ownerClient, + api: api, + stream: stream, + template: tpl, } } diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 46035f28dda77..bcc2cef40ebdc 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -623,6 +623,11 @@ func (b *Builder) getParameters() (names, values []string, err error) { return nil, nil, BuildError{http.StatusBadRequest, "Unable to build workspace with unsupported parameters", err} } + lastBuildParameterValues := db2sdk.WorkspaceBuildParameters(lastBuildParameters) + resolver := codersdk.ParameterResolver{ + Rich: lastBuildParameterValues, + } + // Dynamic parameters skip all parameter validation. // Deleting a workspace also should skip parameter validation. // Pass the user's input as is. @@ -632,19 +637,34 @@ func (b *Builder) getParameters() (names, values []string, err error) { // conditional parameter existence, the static frame of reference // is not sufficient. So assume the user is correct, or pull in the // dynamic param code to find the actual parameters. + latestValues := make(map[string]string, len(b.richParameterValues)) + for _, latest := range b.richParameterValues { + latestValues[latest.Name] = latest.Value + } + + // Merge the inputs with values from the previous build. + for _, last := range lastBuildParameterValues { + // TODO: Ideally we use the resolver here and look at parameter + // fields such as 'ephemeral'. This requires loading the terraform + // files. For now, just send the previous inputs as is. + if _, exists := latestValues[last.Name]; exists { + // latestValues take priority, so skip this previous value. + continue + } + names = append(names, last.Name) + values = append(values, last.Value) + } + for _, value := range b.richParameterValues { names = append(names, value.Name) values = append(values, value.Value) } + b.parameterNames = &names b.parameterValues = &values return names, values, nil } - resolver := codersdk.ParameterResolver{ - Rich: db2sdk.WorkspaceBuildParameters(lastBuildParameters), - } - for _, templateVersionParameter := range templateVersionParameters { tvp, err := db2sdk.TemplateVersionParameter(templateVersionParameter) if err != nil { diff --git a/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx b/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx index 330b3c9a41105..c0f3aad774473 100644 --- a/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx +++ b/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx @@ -12,27 +12,30 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const MediumBeta: Story = { +export const SmallBeta: Story = { args: { - size: "md", + size: "sm", + contentType: "beta", }, }; -export const SmallBeta: Story = { +export const MediumBeta: Story = { args: { - size: "sm", + size: "md", + contentType: "beta", }, }; -export const LargeBeta: Story = { +export const SmallEarlyAccess: Story = { args: { - size: "lg", + size: "sm", + contentType: "early_access", }, }; -export const MediumExperimental: Story = { +export const MediumEarlyAccess: Story = { args: { size: "md", - contentType: "experimental", + contentType: "early_access", }, }; diff --git a/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx b/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx index 18b03b2e93661..78ad6c0311c06 100644 --- a/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx +++ b/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx @@ -1,9 +1,12 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import Link from "@mui/material/Link"; -import { visuallyHidden } from "@mui/utils"; -import { HelpTooltipContent } from "components/HelpTooltip/HelpTooltip"; -import { Popover, PopoverTrigger } from "components/deprecated/Popover/Popover"; +import { Link } from "components/Link/Link"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import type { FC, HTMLAttributes, ReactNode } from "react"; +import { cn } from "utils/cn"; import { docs } from "utils/docs"; /** @@ -11,132 +14,73 @@ import { docs } from "utils/docs"; * ensure that we can't accidentally make typos when writing the badge text. */ export const featureStageBadgeTypes = { + early_access: "early access", beta: "beta", - experimental: "experimental", } as const satisfies Record; type FeatureStageBadgeProps = Readonly< Omit, "children"> & { contentType: keyof typeof featureStageBadgeTypes; labelText?: string; - size?: "sm" | "md" | "lg"; - showTooltip?: boolean; + size?: "sm" | "md"; } >; +const badgeColorClasses = { + early_access: "bg-surface-orange text-content-warning", + beta: "bg-surface-sky text-highlight-sky", +} as const; + +const badgeSizeClasses = { + sm: "text-xs font-medium px-2 py-1", + md: "text-base px-2 py-1", +} as const; + export const FeatureStageBadge: FC = ({ contentType, labelText = "", size = "md", - showTooltip = true, // This is a temporary until the deprecated popover is removed + className, ...delegatedProps }) => { + const colorClasses = badgeColorClasses[contentType]; + const sizeClasses = badgeSizeClasses[size]; + return ( - - - {({ isOpen }) => ( + + + - (This is a + (This is a {labelText && `${labelText} `} {featureStageBadgeTypes[contentType]} - feature) + feature) - )} - - - {showTooltip && ( - -

+ + +

This feature has not yet reached general availability (GA).

Learn about feature stages - (link opens in new tab) + (link opens in new tab) -
- )} -
+ + + ); }; - -const styles = { - badge: (theme) => ({ - // Base type is based on a span so that the element can be placed inside - // more types of HTML elements without creating invalid markdown, but we - // still want the default display behavior to be div-like - display: "block", - maxWidth: "fit-content", - - // Base style assumes that medium badges will be the default - fontSize: "0.75rem", - - cursor: "default", - flexShrink: 0, - padding: "4px 8px", - lineHeight: 1, - whiteSpace: "nowrap", - border: `1px solid ${theme.branding.featureStage.border}`, - color: theme.branding.featureStage.text, - backgroundColor: theme.branding.featureStage.background, - borderRadius: "6px", - transition: - "color 0.2s ease-in-out, border-color 0.2s ease-in-out, background-color 0.2s ease-in-out", - }), - - badgeHover: (theme) => ({ - color: theme.branding.featureStage.hover.text, - borderColor: theme.branding.featureStage.hover.border, - backgroundColor: theme.branding.featureStage.hover.background, - }), - - badgeLargeText: { - fontSize: "1rem", - }, - - badgeSmallText: { - // Have to beef up font weight so that the letters still maintain the - // same relative thickness as all our other main UI text - fontWeight: 500, - fontSize: "0.625rem", - }, - - tooltipTitle: (theme) => ({ - color: theme.palette.text.primary, - fontWeight: 600, - fontFamily: "inherit", - fontSize: 18, - margin: 0, - lineHeight: 1, - paddingBottom: "8px", - }), - - tooltipDescription: { - margin: 0, - lineHeight: 1.4, - paddingBottom: "8px", - }, - - tooltipLink: { - fontWeight: 600, - lineHeight: 1.2, - }, -} as const satisfies Record>; diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index 72113ce8f504b..35c5763c23d25 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -84,6 +84,7 @@ export const DynamicParameter: FC = ({ value={value} onChange={onChange} disabled={disabled} + isPreset={isPreset} /> ) : ( void; disabled?: boolean; id: string; + isPreset?: boolean; } const DebouncedParameterField: FC = ({ @@ -239,6 +241,7 @@ const DebouncedParameterField: FC = ({ onChange, disabled, id, + isPreset, }) => { const [localValue, setLocalValue] = useState( value !== undefined ? value : validValue(parameter.value), @@ -251,19 +254,26 @@ const DebouncedParameterField: FC = ({ // This is necessary in the case of fields being set by preset parameters useEffect(() => { - if (value !== undefined && value !== prevValueRef.current) { + if (isPreset && value !== undefined && value !== prevValueRef.current) { setLocalValue(value); prevValueRef.current = value; } - }, [value]); + }, [value, isPreset]); useEffect(() => { - if (prevDebouncedValueRef.current !== undefined) { + // 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(() => { @@ -513,7 +523,9 @@ const ParameterField: FC = ({ max={parameter.validations[0]?.validation_max ?? 100} disabled={disabled} /> - {parameter.value.value} + + {Number.isFinite(Number(value)) ? value : "0"} + ); case "error": diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index 817a7abfccb09..55b507e219c56 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -3,12 +3,12 @@ import type { FriendlyDiagnostic, PreviewParameter } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; +import { Badge } from "components/Badge/Badge"; import { Button } from "components/Button/Button"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Input } from "components/Input/Input"; import { Label } from "components/Label/Label"; import { Link } from "components/Link/Link"; -import { Pill } from "components/Pill/Pill"; import { Select, SelectContent, @@ -353,21 +353,39 @@ export const CreateWorkspacePageViewExperimental: FC<
-
- -

- {template.display_name.length > 0 - ? template.display_name - : template.name} -

+
+ + +

+ {template.display_name.length > 0 + ? template.display_name + : template.name} +

+ {template.deprecated && ( + + Deprecated + + )} +
+ {experimentalFormContext && ( + + )}

New workspace

+ @@ -389,19 +407,11 @@ export const CreateWorkspacePageViewExperimental: FC<
- - {template.deprecated && Deprecated} - - {experimentalFormContext && ( - - )} +
- +
diff --git a/site/src/pages/UserSettingsPage/Section.tsx b/site/src/pages/UserSettingsPage/Section.tsx index 0d162b73a7f6b..2227dcd9cdbf8 100644 --- a/site/src/pages/UserSettingsPage/Section.tsx +++ b/site/src/pages/UserSettingsPage/Section.tsx @@ -53,7 +53,7 @@ export const Section: FC = ({ {featureStage && ( )} diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index 6dac6536b5bfb..56720292957ff 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -117,18 +117,18 @@ export const WorkspaceParametersPageView: FC< return (
- +

Workspace parameters

+ {experimentalFormContext && ( + + Try out the new workspace parameters ✨ + + )}
- {experimentalFormContext && ( - - Try out the new workspace parameters ✨ - - )}
{submitError && !isApiValidationError(submitError) ? ( diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx index 781f8b12e8c67..37baf9c3a1240 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx @@ -9,6 +9,7 @@ import type { import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; +import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Link } from "components/Link/Link"; import { Loader } from "components/Loader/Loader"; import { @@ -26,6 +27,7 @@ import { useMutation, useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; +import type { AutofillBuildParameter } from "utils/richParameters"; import { type WorkspacePermissions, workspaceChecks, @@ -39,11 +41,27 @@ const WorkspaceParametersPageExperimental: FC = () => { const navigate = useNavigate(); const experimentalFormContext = useContext(ExperimentalFormContext); + // autofill the form with the workspace build parameters from the latest build + const { + data: latestBuildParameters, + isLoading: latestBuildParametersLoading, + } = useQuery({ + queryKey: ["workspaceBuilds", workspace.latest_build.id, "parameters"], + queryFn: () => API.getWorkspaceBuildParameters(workspace.latest_build.id), + }); + const [latestResponse, setLatestResponse] = useState(null); const wsResponseId = useRef(-1); const ws = useRef(null); const [wsError, setWsError] = useState(null); + const initialParamsSentRef = useRef(false); + + const autofillParameters: AutofillBuildParameter[] = + latestBuildParameters?.map((p) => ({ + ...p, + source: "active_build", + })) ?? []; const sendMessage = useEffectEvent((formValues: Record) => { const request: DynamicParametersRequest = { @@ -57,11 +75,34 @@ const WorkspaceParametersPageExperimental: FC = () => { } }); + // On page load, sends initial workspace build parameters to the websocket. + // This ensures the backend has the form's complete initial state, + // vital for rendering dynamic UI elements dependent on initial parameter values. + const sendInitialParameters = useEffectEvent(() => { + if (initialParamsSentRef.current) return; + if (autofillParameters.length === 0) return; + + const initialParamsToSend: Record = {}; + for (const param of autofillParameters) { + 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(); + } + setLatestResponse(response); }); @@ -149,6 +190,7 @@ const WorkspaceParametersPageExperimental: FC = () => { const error = wsError || updateParameters.error; if ( + latestBuildParametersLoading || !latestResponse || (ws.current && ws.current.readyState === WebSocket.CONNECTING) ) { @@ -162,39 +204,46 @@ const WorkspaceParametersPageExperimental: FC = () => {
- -

Workspace parameters

- - - - - - - Dynamic Parameters enhances Coder's existing parameter system - with real-time validation, conditional parameter behavior, and - richer input types. -
- - View docs - -
-
-
+ + +

Workspace parameters

+ + + + + + + Dynamic Parameters enhances Coder's existing parameter system + with real-time validation, conditional parameter behavior, and + richer input types. +
+ + View docs + +
+
+
+
+ {experimentalFormContext && ( + + )}
- {experimentalFormContext && ( - - )} +
{Boolean(error) && } @@ -202,6 +251,7 @@ const WorkspaceParametersPageExperimental: FC = () => { {sortedParams.length > 0 ? ( = ({ workspace, + autofillParameters, parameters, diagnostics, canChangeVersions, @@ -42,17 +45,32 @@ export const WorkspaceParametersPageViewExperimental: FC< sendMessage, onCancel, }) => { + const autofillByName = Object.fromEntries( + autofillParameters.map((param) => [param.name, param]), + ); + const initialTouched = parameters.reduce( + (touched, parameter) => { + if (autofillByName[parameter.name] !== undefined) { + touched[parameter.name] = true; + } + return touched; + }, + {} as Record, + ); const form = useFormik({ onSubmit, initialValues: { - rich_parameter_values: getInitialParameterValues(parameters), + rich_parameter_values: getInitialParameterValues( + parameters, + autofillParameters, + ), }, + initialTouched, validationSchema: useValidationSchemaForDynamicParameters(parameters), enableReinitialize: false, validateOnChange: true, validateOnBlur: true, }); - // Group parameters by ephemeral status const ephemeralParameters = parameters.filter((p) => p.ephemeral); const standardParameters = parameters.filter((p) => !p.ephemeral); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy