diff --git a/apps/builder/app/builder/features/pages/page-utils.test.ts b/apps/builder/app/builder/features/pages/page-utils.test.ts index 57424ff8ccf6..188f5e680772 100644 --- a/apps/builder/app/builder/features/pages/page-utils.test.ts +++ b/apps/builder/app/builder/features/pages/page-utils.test.ts @@ -7,6 +7,7 @@ import { ROOT_FOLDER_ID, type Page, SYSTEM_VARIABLE_ID, + Resource, } from "@webstudio-is/sdk"; import { cleanupChildRefsMutable, @@ -23,11 +24,12 @@ import { $dataSourceVariables, $dataSources, $pages, - $resourceValues, + $resources, } from "~/shared/nano-states"; import { registerContainers } from "~/shared/sync"; import { $awareness } from "~/shared/awareness"; import { updateCurrentSystem } from "~/shared/system"; +import { $resourcesCache, getResourceKey } from "~/shared/resources"; setEnv("*"); registerContainers(); @@ -521,7 +523,25 @@ test("page root scope should use variable and resource values", () => { $dataSourceVariables.set( new Map([["valueVariableId", "value variable value"]]) ); - $resourceValues.set(new Map([["resourceId", "resource variable value"]])); + const resourceKey = getResourceKey({ + name: "my-resource", + url: "", + searchParams: [], + method: "get", + headers: [], + }); + $resources.set( + toMap([ + { + id: "resourceId", + name: "my-resource", + url: `""`, + method: "get", + headers: [], + }, + ]) + ); + $resourcesCache.set(new Map([[resourceKey, "resource variable value"]])); expect($pageRootScope.get()).toEqual({ aliases: new Map([ ["$ws$system", "system"], diff --git a/apps/builder/app/builder/features/settings-panel/resource-panel.tsx b/apps/builder/app/builder/features/settings-panel/resource-panel.tsx index 1f32e44cd530..dbbf64c29bd1 100644 --- a/apps/builder/app/builder/features/settings-panel/resource-panel.tsx +++ b/apps/builder/app/builder/features/settings-panel/resource-panel.tsx @@ -75,7 +75,7 @@ export const parseResource = ({ formData, }: { id: string; - name: string; + name?: string; formData: FormData; }) => { const searchParamNames = formData.getAll("search-param-name") as string[]; @@ -84,7 +84,7 @@ export const parseResource = ({ const headerValues = formData.getAll("header-value") as string[]; return Resource.parse({ id, - name, + name: name ?? formData.get("name"), url: formData.get("url"), searchParams: searchParamNames .map((name, index) => ({ name, value: searchParamValues[index] })) @@ -175,17 +175,21 @@ export const UrlField = ({ } try { const url = new URL(value); - const searchParams: Resource["searchParams"] = []; - for (const [name, value] of url.searchParams) { - searchParams.push({ name, value: JSON.stringify(value) }); + if (url.searchParams.size > 0) { + const searchParams: Resource["searchParams"] = []; + for (const [name, value] of url.searchParams) { + searchParams.push({ name, value: JSON.stringify(value) }); + } + // remove all search params from url + url.search = ""; + // update text value as string literal + onChange(JSON.stringify(url.href), searchParams); + return; } - // remove all search params from url - url.search = ""; - // update text value as string literal - onChange(JSON.stringify(url.href), searchParams); } catch { - onChange(JSON.stringify(value)); + // serialize without changes when url is invalid } + onChange(JSON.stringify(value)); }} onBlur={(event) => event.currentTarget.checkValidity()} onInvalid={(event) => @@ -515,9 +519,9 @@ export const getResourceScopeForInstance = ({ } if (dataSource) { const name = encodeDataVariableId(dataSourceId); - variableValues.set(dataSourceId, value); scope[name] = value; aliases.set(name, dataSource.name); + variableValues.set(dataSource.name, value); } } } @@ -544,7 +548,7 @@ const getVariableInstanceKey = ({ return getInstanceKey(instancePath[0].instanceSelector); }; -const useScope = ({ variable }: { variable?: DataSource }) => { +export const useResourceScope = ({ variable }: { variable?: DataSource }) => { return useStore( useMemo( () => @@ -561,22 +565,32 @@ const useScope = ({ variable }: { variable?: DataSource }) => { variableValuesByInstanceSelector, dataSources ) => { - const { scope, aliases } = getResourceScopeForInstance({ - page, - instanceKey: getVariableInstanceKey({ - variable, - instancePath, - }), - dataSources, - variableValuesByInstanceSelector, - }); + const { scope, aliases, variableValues } = + getResourceScopeForInstance({ + page, + instanceKey: getVariableInstanceKey({ + variable, + instancePath, + }), + dataSources, + variableValuesByInstanceSelector, + }); // prevent showing currently edited variable in suggestions // to avoid cirular dependeny const newScope = { ...scope }; + const newAliases = new Map(aliases); + const newVariableValues = new Map(variableValues); if (variable) { - delete newScope[encodeDataVariableId(variable.id)]; + const key = encodeDataVariableId(variable.id); + delete newScope[key]; + newAliases.delete(key); + newVariableValues.delete(variable.name); } - return { scope: newScope, aliases }; + return { + scope: newScope, + aliases: newAliases, + variableValues: newVariableValues, + }; } ), [variable] @@ -706,7 +720,7 @@ export const ResourceForm = forwardRef< undefined | PanelApi, { variable?: DataSource } >(({ variable }, ref) => { - const { scope, aliases } = useScope({ variable }); + const { scope, aliases } = useResourceScope({ variable }); const resources = useStore($resources); const resource = @@ -739,16 +753,14 @@ export const ResourceForm = forwardRef< if (scopeInstanceId === undefined) { return; } - const name = z.string().parse(formData.get("name")); const newResource = parseResource({ id: resource?.id ?? nanoid(), - name, formData, }); const newVariable: DataSource = { id: variable?.id ?? nanoid(), scopeInstanceId, - name, + name: newResource.name, type: "resource", resourceId: newResource.id, }; @@ -942,7 +954,7 @@ export const GraphqlResourceForm = forwardRef< undefined | PanelApi, { variable?: DataSource } >(({ variable }, ref) => { - const { scope, aliases } = useScope({ variable }); + const { scope, aliases } = useResourceScope({ variable }); const resources = useStore($resources); const resource = diff --git a/apps/builder/app/builder/features/settings-panel/variable-popover.tsx b/apps/builder/app/builder/features/settings-panel/variable-popover.tsx index a692f59950df..df3400bc9ecf 100644 --- a/apps/builder/app/builder/features/settings-panel/variable-popover.tsx +++ b/apps/builder/app/builder/features/settings-panel/variable-popover.tsx @@ -12,7 +12,6 @@ import { useState, useImperativeHandle, useRef, - createContext, useEffect, useCallback, useMemo, @@ -48,6 +47,7 @@ import { transpileExpression, lintExpression, SYSTEM_VARIABLE_ID, + ResourceRequest, } from "@webstudio-is/sdk"; import { ExpressionEditor, @@ -56,9 +56,6 @@ import { import { $dataSources, $resources, - $areResourcesLoading, - invalidateResource, - getComputedResourceRequest, $userPlanFeatures, $instances, $props, @@ -68,7 +65,6 @@ import { $selectedInstance, $selectedInstanceKeyWithRoot, } from "~/shared/awareness"; -import { BindingPopoverProvider } from "~/builder/shared/binding-popover"; import { EditorContent, EditorDialog, @@ -83,10 +79,19 @@ import { } from "~/shared/data-variables"; import { GraphqlResourceForm, + parseResource, ResourceForm, SystemResourceForm, + useResourceScope, } from "./resource-panel"; import { generateCurl } from "./curl"; +import { + $hasPendingResources, + $resourcesCache, + computeResourceRequest, + getResourceKey, + invalidateResource, +} from "~/shared/resources"; const NameField = ({ variable, @@ -103,7 +108,7 @@ const NameField = ({ // validate same name on variable instance // and fallback to selected instance for new variables const scopeInstanceId = - variable?.scopeInstanceId ?? $selectedInstance.get(); + variable?.scopeInstanceId ?? $selectedInstance.get()?.id; for (const dataSource of $dataSources.get().values()) { if ( dataSource.scopeInstanceId === scopeInstanceId && @@ -308,42 +313,59 @@ const ParameterForm = forwardRef< }); ParameterForm.displayName = "ParameterForm"; +const saveVariable = (variable: undefined | DataSource, formData: FormData) => { + const dataSourceId = variable?.id ?? nanoid(); + // preserve existing instance scope when edit + const scopeInstanceId = + variable?.scopeInstanceId ?? $selectedInstance.get()?.id; + if (scopeInstanceId === undefined) { + return; + } + const type = z.string().parse(formData.get("type")); + const name = z.string().parse(formData.get("name")); + const value = z.string().nullable().parse(formData.get("value")); + let variableValue: Extract["value"]; + if (type === "string") { + variableValue = { type: "string", value: value ?? "" }; + } else if (type === "number") { + variableValue = { type: "number", value: Number(value || 0) }; + } else if (type === "boolean") { + variableValue = { type: "boolean", value: value != null }; + } else { + variableValue = { + type: "json", + value: value ? parseJsonValue(value) : undefined, + }; + } + updateWebstudioData((data) => { + // cleanup resource when value variable is set + if (variable?.type === "resource") { + data.resources.delete(variable.resourceId); + } + data.dataSources.set(dataSourceId, { + id: dataSourceId, + scopeInstanceId, + name, + type: "variable", + value: variableValue, + }); + rebindTreeVariablesMutable({ + startingInstanceId: scopeInstanceId, + ...data, + }); + }); +}; + const useValuePanelRef = ({ ref, variable, - variableValue, }: { ref: Ref; variable?: DataSource; - variableValue: Extract["value"]; }) => { useImperativeHandle(ref, () => ({ save: (formData) => { - const dataSourceId = variable?.id ?? nanoid(); - // preserve existing instance scope when edit - const scopeInstanceId = - variable?.scopeInstanceId ?? $selectedInstance.get()?.id; - if (scopeInstanceId === undefined) { - return; - } - const name = z.string().parse(formData.get("name")); - updateWebstudioData((data) => { - // cleanup resource when value variable is set - if (variable?.type === "resource") { - data.resources.delete(variable.resourceId); - } - data.dataSources.set(dataSourceId, { - id: dataSourceId, - scopeInstanceId, - name, - type: "variable", - value: variableValue, - }); - rebindTreeVariablesMutable({ - startingInstanceId: scopeInstanceId, - ...data, - }); - }); + saveVariable(variable, formData); }, })); }; @@ -352,49 +374,41 @@ const StringForm = forwardRef< undefined | PanelApi, { variable?: DataSource; + value: unknown; + onChange: (value: unknown) => void; } ->(({ variable }, ref) => { - const [value, setValue] = useState( - variable?.type === "variable" && variable.value.type === "string" - ? variable.value.value - : "" - ); - useValuePanelRef({ - ref, - variable, - variableValue: { type: "string", value }, - }); +>(({ variable, value: unknownValue, onChange }, ref) => { + const value = typeof unknownValue === "string" ? unknownValue : ""; + useValuePanelRef({ ref, variable }); const valueId = useId(); return ( - <> - - - -