diff --git a/apps/builder/app/builder/features/settings-panel/props-section/use-props-logic.ts b/apps/builder/app/builder/features/settings-panel/props-section/use-props-logic.ts index 4a46c2b613b8..1eff9bb6dbed 100644 --- a/apps/builder/app/builder/features/settings-panel/props-section/use-props-logic.ts +++ b/apps/builder/app/builder/features/settings-panel/props-section/use-props-logic.ts @@ -16,13 +16,14 @@ import { $registeredComponentMetas, } from "~/shared/nano-states"; import { isRichText } from "~/shared/content-model"; -import { $selectedInstancePath } from "~/shared/awareness"; +import { $selectedInstance, $selectedInstancePath } from "~/shared/awareness"; import { $selectedInstanceInitialPropNames, $selectedInstancePropsMetas, showAttributeMeta, type PropValue, } from "../shared"; +import { $instanceTags } from "../../style-panel/shared/model"; type PropOrName = { prop?: Prop; propName: string }; @@ -157,6 +158,21 @@ const $canHaveTextContent = computed( } ); +const contentModePropertiesByTag: Partial> = { + img: ["src", "width", "height", "alt"], + a: ["href"], +}; + +const $selectedInstanceTag = computed( + [$selectedInstance, $instanceTags], + (selectedInstance, instanceTags) => { + if (selectedInstance === undefined) { + return; + } + return instanceTags.get(selectedInstance.id); + } +); + /** usePropsLogic expects that key={instanceId} is used on the ancestor component */ export const usePropsLogic = ({ instance, @@ -164,25 +180,19 @@ export const usePropsLogic = ({ updateProp, }: UsePropsLogicInput) => { const isContentMode = useStore($isContentMode); + const selectedInstanceTag = useStore($selectedInstanceTag); /** * In content edit mode we show only Image and Link props * In the future I hope the only thing we will show will be Components */ const isPropVisible = (propName: string) => { - const contentModeWhiteList: Partial> = { - Image: ["src", "width", "height", "alt"], - Link: ["href"], - RichTextLink: ["href"], - }; - if (!isContentMode) { return true; } - - const propsWhiteList = contentModeWhiteList[instance.component] ?? []; - - return propsWhiteList.includes(propName); + const allowedProperties = + contentModePropertiesByTag[selectedInstanceTag ?? ""] ?? []; + return allowedProperties.includes(propName); }; const savedProps = props; @@ -231,11 +241,7 @@ export const usePropsLogic = ({ const initialProps: PropAndMeta[] = []; for (const name of initialPropNames) { const propMeta = propsMetas.get(name); - if (propMeta === undefined) { - console.error( - `The prop "${name}" is defined in meta.initialProps but not in meta.props` - ); continue; } 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 a902ea12b76d..fa70518756d3 100644 --- a/apps/builder/app/builder/features/settings-panel/resource-panel.tsx +++ b/apps/builder/app/builder/features/settings-panel/resource-panel.tsx @@ -66,18 +66,17 @@ import { type InstancePath, } from "~/shared/awareness"; import { updateWebstudioData } from "~/shared/instance-utils"; -import { - computeExpression, - rebindTreeVariablesMutable, -} from "~/shared/data-variables"; +import { rebindTreeVariablesMutable } from "~/shared/data-variables"; import { parseCurl, type CurlRequest } from "./curl"; export const parseResource = ({ id, + control, name, formData, }: { id: string; + control?: string; name?: string; formData: FormData; }) => { @@ -87,6 +86,7 @@ export const parseResource = ({ const headerValues = formData.getAll("header-value") as string[]; return Resource.parse({ id, + control, name: name ?? formData.get("name"), url: formData.get("url"), searchParams: searchParamNames @@ -253,7 +253,7 @@ const SearchParamPair = ({ }) => { const evaluatedValue = evaluateExpressionWithinScope(value, scope); // expressions with variables or objects cannot be edited from input - const isValueUnbound = + const isValueUnboundString = isLiteralExpression(value) && typeof evaluatedValue === "string"; return ( @@ -284,7 +284,7 @@ const SearchParamPair = ({ onChange(name, newValue)} onRemove={(evaluatedValue) => @@ -377,7 +377,7 @@ const HeaderPair = ({ }) => { const evaluatedValue = evaluateExpressionWithinScope(value, scope); // expressions with variables or objects cannot be edited from input - const isValueUnbound = + const isValueUnboundString = isLiteralExpression(value) && typeof evaluatedValue === "string"; return ( @@ -408,7 +408,7 @@ const HeaderPair = ({ onChange(name, newValue)} onRemove={(evaluatedValue) => @@ -558,7 +558,7 @@ export const getResourceScopeForInstance = ({ const name = encodeDataVariableId(dataSourceId); scope[name] = value; aliases.set(name, dataSource.name); - variableValues.set(dataSource.name, value); + variableValues.set(dataSourceId, value); } } } @@ -621,7 +621,7 @@ export const useResourceScope = ({ variable }: { variable?: DataSource }) => { const key = encodeDataVariableId(variable.id); delete newScope[key]; newAliases.delete(key); - newVariableValues.delete(variable.name); + newVariableValues.delete(variable.id); } return { scope: newScope, @@ -796,7 +796,10 @@ const parseHeaders = (headers: Resource["headers"]) => { let maxAge: undefined | string; let bodyType: BodyType; const newHeaders = headers.filter((header) => { - const value = computeExpression(header.value, new Map()).toLowerCase(); + // cast raw expression result to string + const value = String( + evaluateExpressionWithinScope(header.value, {}) + ).toLowerCase(); if (isCacheControl(header.name)) { // move simple header like Cache-Control: max-age=10 to dedicated input // preserve more complex cache-control @@ -818,7 +821,7 @@ const parseHeaders = (headers: Resource["headers"]) => { return false; } } - return false; + return true; }); return { headers: newHeaders, maxAge, bodyType }; }; @@ -980,8 +983,6 @@ export const SystemResourceForm = forwardRef< ? resources.get(variable.resourceId) : undefined; - const method = "get"; - const localResources = [ { label: "Sitemap", @@ -1006,19 +1007,15 @@ export const SystemResourceForm = forwardRef< if (scopeInstanceId === undefined) { return; } - const name = z.string().parse(formData.get("name")); - const newResource: Resource = { + const newResource: Resource = parseResource({ id: resource?.id ?? nanoid(), - name, control: "system", - url: localResource.value, - method, - headers: [], - }; + formData, + }); const newVariable: DataSource = { id: variable?.id ?? nanoid(), scopeInstanceId, - name, + name: newResource.name, type: "resource", resourceId: newResource.id, }; @@ -1037,6 +1034,8 @@ export const SystemResourceForm = forwardRef< return ( <> + + + {!headers.some(({ name }) => isContentType(name)) && ( + <> + + + + )} + + ({ + ...transaction, + payload: transaction.payload.map((change) => ({ + namespace: change.namespace, + patches: change.patches, + })), + })); const response = await fetch(restPatchPath(), { method: "post", body: JSON.stringify({ - transactions, + transactions: optimizedTransactions, buildId: details.buildId, projectId, // provide latest stored version to server diff --git a/apps/builder/app/routes/rest.patch.ts b/apps/builder/app/routes/rest.patch.ts index 920c07647b1f..162ca34c4b4e 100644 --- a/apps/builder/app/routes/rest.patch.ts +++ b/apps/builder/app/routes/rest.patch.ts @@ -1,6 +1,5 @@ -import { applyPatches, enableMapSet, enablePatches } from "immer"; +import { applyPatches, enableMapSet, enablePatches, type Patch } from "immer"; import type { ActionFunctionArgs } from "@remix-run/server-runtime"; -import type { Change } from "immerhin"; import { Breakpoints, Breakpoint, @@ -51,6 +50,11 @@ import { preventCrossOriginCookie } from "~/services/no-cross-origin-cookie"; import { checkCsrf } from "~/services/csrf-session.server"; import type { Transaction } from "~/shared/sync-client"; +type Change = { + namespace: string; + patches: Array; +}; + type PatchData = { transactions: Transaction[]; buildId: Build["id"]; diff --git a/apps/builder/app/routes/rest.resources-loader.ts b/apps/builder/app/routes/rest.resources-loader.ts index 0954d19295ac..94c239a89b31 100644 --- a/apps/builder/app/routes/rest.resources-loader.ts +++ b/apps/builder/app/routes/rest.resources-loader.ts @@ -25,32 +25,35 @@ export const action = async ({ request }: ActionFunctionArgs) => { }; const requestJson = await request.json(); + const requestList = z.array(z.unknown()).safeParse(requestJson); - const computedResourcesParsed = z - .array(ResourceRequest) - .safeParse(requestJson); - - if (computedResourcesParsed.success === false) { - console.error( - "computedResources.parse", - computedResourcesParsed.error.toString() - ); + if (requestList.success === false) { console.error("data:", requestJson); - - throw data(computedResourcesParsed.error, { + throw data(requestList.error, { status: 400, }); } - const computedResources = computedResourcesParsed.data; - - const responses = await Promise.all( - computedResources.map((resource) => loadResource(customFetch, resource)) + const output = await Promise.all( + requestList.data.map(async (item) => { + const resource = ResourceRequest.safeParse(item); + if (resource.success === false) { + return [ + getResourceKey(item as ResourceRequest), + { + ok: false, + data: resource.error.format(), + status: 403, + statusText: "Resource validation error", + }, + ]; + } + return [ + getResourceKey(resource.data), + await loadResource(customFetch, resource.data), + ]; + }) ); - const output: [string, unknown][] = []; - responses.forEach((response, index) => { - const request = computedResources[index]; - output.push([getResourceKey(request), response]); - }); + return output; }; diff --git a/apps/builder/app/shared/copy-paste.test.tsx b/apps/builder/app/shared/copy-paste.test.tsx index 510e8ea8e000..d12e028744fa 100644 --- a/apps/builder/app/shared/copy-paste.test.tsx +++ b/apps/builder/app/shared/copy-paste.test.tsx @@ -578,6 +578,7 @@ describe("resources", () => { const resourceVariable = new ResourceValue("Box Resource", { url: expression`${boxVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${boxVariable}` }], headers: [{ name: "auth", value: expression`${boxVariable}` }], body: expression`${boxVariable}`, }); @@ -594,6 +595,7 @@ describe("resources", () => { expect(fragment.resources).toEqual([ expect.objectContaining({ url: "$ws$dataSource$1", + searchParams: [{ name: "filter", value: "$ws$dataSource$1" }], headers: [{ name: "auth", value: "$ws$dataSource$1" }], body: "$ws$dataSource$1", }), @@ -605,6 +607,7 @@ describe("resources", () => { const resourceVariable = new ResourceValue("Box Resource", { url: expression`${bodyVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); @@ -620,6 +623,7 @@ describe("resources", () => { expect(fragment.resources).toEqual([ expect.objectContaining({ url: "Body$32$Variable", + searchParams: [{ name: "filter", value: "Body$32$Variable" }], headers: [{ name: "auth", value: "Body$32$Variable" }], body: "Body$32$Variable", }), @@ -631,6 +635,7 @@ describe("resources", () => { const resourceVariable = new ResourceValue("Box Resource", { url: expression`${bodyVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); @@ -654,11 +659,13 @@ describe("resources", () => { expect(Array.from(data.resources.values())).toEqual([ expect.objectContaining({ url: "$ws$dataSource$0", + searchParams: [{ name: "filter", value: "$ws$dataSource$0" }], headers: [{ name: "auth", value: "$ws$dataSource$0" }], body: "$ws$dataSource$0", }), expect.objectContaining({ url: "$ws$dataSource$0", + searchParams: [{ name: "filter", value: "$ws$dataSource$0" }], headers: [{ name: "auth", value: "$ws$dataSource$0" }], body: "$ws$dataSource$0", }), @@ -670,6 +677,7 @@ describe("resources", () => { const resourceProp = new ResourceValue("Box Resource", { url: expression`${boxVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${boxVariable}` }], headers: [{ name: "auth", value: expression`${boxVariable}` }], body: expression`${boxVariable}`, }); @@ -685,6 +693,7 @@ describe("resources", () => { expect(fragment.resources).toEqual([ expect.objectContaining({ url: "$ws$dataSource$1", + searchParams: [{ name: "filter", value: "$ws$dataSource$1" }], headers: [{ name: "auth", value: "$ws$dataSource$1" }], body: "$ws$dataSource$1", }), @@ -696,6 +705,7 @@ describe("resources", () => { const resourceProp = new ResourceValue("Box Resource", { url: expression`${bodyVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); @@ -709,6 +719,7 @@ describe("resources", () => { expect(fragment.resources).toEqual([ expect.objectContaining({ url: "Body$32$Variable", + searchParams: [{ name: "filter", value: "Body$32$Variable" }], headers: [{ name: "auth", value: "Body$32$Variable" }], body: "Body$32$Variable", }), @@ -720,6 +731,7 @@ describe("resources", () => { const resourceProp = new ResourceValue("Box Resource", { url: expression`${bodyVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); @@ -743,11 +755,13 @@ describe("resources", () => { expect(Array.from(data.resources.values())).toEqual([ expect.objectContaining({ url: "$ws$dataSource$0", + searchParams: [{ name: "filter", value: "$ws$dataSource$0" }], headers: [{ name: "auth", value: "$ws$dataSource$0" }], body: "$ws$dataSource$0", }), expect.objectContaining({ url: "$ws$dataSource$0", + searchParams: [{ name: "filter", value: "$ws$dataSource$0" }], headers: [{ name: "auth", value: "$ws$dataSource$0" }], body: "$ws$dataSource$0", }), @@ -759,12 +773,14 @@ describe("resources", () => { const resourceProp = new ResourceValue("Box Resource", { url: expression`${boxVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${boxVariable}` }], headers: [{ name: "auth", value: expression`${boxVariable}` }], body: expression`${boxVariable}`, }); const resourceVariable = new ResourceValue("Box Resource", { url: expression`${boxVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${boxVariable}` }], headers: [{ name: "auth", value: expression`${boxVariable}` }], body: expression`${boxVariable}`, }); @@ -798,12 +814,14 @@ describe("resources", () => { expect.objectContaining({ id: expect.toSatisfy((value) => value !== fragment.resources[0].id), url: newVariableIdentifier, + searchParams: [{ name: "filter", value: newVariableIdentifier }], headers: [{ name: "auth", value: newVariableIdentifier }], body: newVariableIdentifier, }), expect.objectContaining({ id: expect.toSatisfy((value) => value !== fragment.resources[1].id), url: newVariableIdentifier, + searchParams: [{ name: "filter", value: newVariableIdentifier }], headers: [{ name: "auth", value: newVariableIdentifier }], body: newVariableIdentifier, }), @@ -822,12 +840,14 @@ describe("resources", () => { const resourceProp = new ResourceValue("Box Resource", { url: expression`${boxVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${boxVariable}` }], headers: [{ name: "auth", value: expression`${boxVariable}` }], body: expression`${boxVariable}`, }); const resourceVariable = new ResourceValue("Box Resource", { url: expression`${boxVariable}`, method: "get", + searchParams: [{ name: "filter", value: expression`${boxVariable}` }], headers: [{ name: "auth", value: expression`${boxVariable}` }], body: expression`${boxVariable}`, }); @@ -865,12 +885,14 @@ describe("resources", () => { expect.objectContaining({ id: fragment.resources[0].id, url: oldVariableIdentifier, + searchParams: [{ name: "filter", value: oldVariableIdentifier }], headers: [{ name: "auth", value: oldVariableIdentifier }], body: oldVariableIdentifier, }), expect.objectContaining({ id: fragment.resources[1].id, url: oldVariableIdentifier, + searchParams: [{ name: "filter", value: oldVariableIdentifier }], headers: [{ name: "auth", value: oldVariableIdentifier }], body: oldVariableIdentifier, }), diff --git a/apps/builder/app/shared/data-variables.test.tsx b/apps/builder/app/shared/data-variables.test.tsx index 8720bc63691b..a34599eff9a4 100644 --- a/apps/builder/app/shared/data-variables.test.tsx +++ b/apps/builder/app/shared/data-variables.test.tsx @@ -216,14 +216,16 @@ test("find unset variable names", () => { const resourceVariable = new ResourceValue("resourceVariable", { url: expression`six`, method: "post", - headers: [{ name: "auth", value: expression`seven` }], - body: expression`eight`, + searchParams: [{ name: "filter", value: expression`seven` }], + headers: [{ name: "auth", value: expression`eight` }], + body: expression`nine`, }); const resourceProp = new ResourceValue("resourceProp", { - url: expression`nine`, + url: expression`ten`, method: "post", - headers: [{ name: "auth", value: expression`ten` }], - body: expression`eleven`, + searchParams: [{ name: "filter", value: expression`eleven` }], + headers: [{ name: "auth", value: expression`twelve` }], + body: expression`thirteen`, }); const data = renderData( <$.Body ws:id="body" data-prop={expression`two`}> @@ -245,11 +247,13 @@ test("find unset variable names", () => { "three", "four", "six", - "seven", "eight", + "seven", "nine", "ten", + "twelve", "eleven", + "thirteen", ]); }); @@ -403,12 +407,14 @@ test("restore tree variables in resources", () => { const resourceVariable = new ResourceValue("resourceVariable", { url: expression`one + 1`, method: "post", + searchParams: [{ name: "filter", value: expression`one + 1` }], headers: [{ name: "auth", value: expression`one + 1` }], body: expression`one + 1`, }); const resourceProp = new ResourceValue("resourceProp", { url: expression`one + 2`, method: "post", + searchParams: [{ name: "filter", value: expression`one + 2` }], headers: [{ name: "auth", value: expression`one + 2` }], body: expression`one + 2`, }); @@ -439,12 +445,14 @@ test("restore tree variables in resources", () => { expect.objectContaining({ url: `${boxIdentifier} + 1`, method: "post", + searchParams: [{ name: "filter", value: `${boxIdentifier} + 1` }], headers: [{ name: "auth", value: `${boxIdentifier} + 1` }], body: `${boxIdentifier} + 1`, }), expect.objectContaining({ url: `${boxIdentifier} + 2`, method: "post", + searchParams: [{ name: "filter", value: `${boxIdentifier} + 2` }], headers: [{ name: "auth", value: `${boxIdentifier} + 2` }], body: `${boxIdentifier} + 2`, }), @@ -457,12 +465,14 @@ test("rebind tree variables in resources", () => { const resourceVariable = new ResourceValue("resourceVariable", { url: expression`${bodyVariable}`, method: "post", + searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); const resourceProp = new ResourceValue("resourceProp", { url: expression`${bodyVariable}`, method: "post", + searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); @@ -493,12 +503,14 @@ test("rebind tree variables in resources", () => { expect.objectContaining({ url: boxIdentifier, method: "post", + searchParams: [{ name: "filter", value: boxIdentifier }], headers: [{ name: "auth", value: boxIdentifier }], body: boxIdentifier, }), expect.objectContaining({ url: boxIdentifier, method: "post", + searchParams: [{ name: "filter", value: boxIdentifier }], headers: [{ name: "auth", value: boxIdentifier }], body: boxIdentifier, }), @@ -663,12 +675,14 @@ test("delete variable and unset it in resources", () => { const resourceVariable = new ResourceValue("resourceVariable", { url: expression`${bodyVariable}`, method: "post", + searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); const resourceProp = new ResourceValue("resourceProp", { url: expression`${bodyVariable}`, method: "post", + searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); @@ -691,12 +705,14 @@ test("delete variable and unset it in resources", () => { expect.objectContaining({ url: "bodyVariable", method: "post", + searchParams: [{ name: "filter", value: "bodyVariable" }], headers: [{ name: "auth", value: "bodyVariable" }], body: "bodyVariable", }), expect.objectContaining({ url: "bodyVariable", method: "post", + searchParams: [{ name: "filter", value: "bodyVariable" }], headers: [{ name: "auth", value: "bodyVariable" }], body: "bodyVariable", }), diff --git a/apps/builder/app/shared/data-variables.ts b/apps/builder/app/shared/data-variables.ts index 9df59f68a66f..f9a9c49b81c1 100644 --- a/apps/builder/app/shared/data-variables.ts +++ b/apps/builder/app/shared/data-variables.ts @@ -406,6 +406,12 @@ const traverseExpressions = ({ for (const header of resource.headers) { header.value = update(header.value, instanceId) ?? header.value; } + if (resource.searchParams) { + for (const searchParam of resource.searchParams) { + searchParam.value = + update(searchParam.value, instanceId) ?? searchParam.value; + } + } if (resource.body) { resource.body = update(resource.body, instanceId) ?? resource.body; } diff --git a/apps/builder/app/shared/instance-utils.test.tsx b/apps/builder/app/shared/instance-utils.test.tsx index a6db4c0d71e3..4b6a71d1621f 100644 --- a/apps/builder/app/shared/instance-utils.test.tsx +++ b/apps/builder/app/shared/instance-utils.test.tsx @@ -932,6 +932,7 @@ describe("delete instance", () => { const myResource = new ResourceValue("My Resource", { url: expression`""`, method: "get", + searchParams: [], headers: [], }); const data = renderData( @@ -953,6 +954,7 @@ describe("delete instance", () => { const myResource = new ResourceValue("My Resource", { url: expression`""`, method: "get", + searchParams: [], headers: [], }); const data = renderData( diff --git a/apps/builder/app/shared/instance-utils.ts b/apps/builder/app/shared/instance-utils.ts index 0519e9bff493..a251e81521b4 100644 --- a/apps/builder/app/shared/instance-utils.ts +++ b/apps/builder/app/shared/instance-utils.ts @@ -826,6 +826,14 @@ export const extractWebstudioFragment = ( unsetNameById, }); } + if (newResource.searchParams) { + for (const searchParam of newResource.searchParams) { + searchParam.value = unsetExpressionVariables({ + expression: searchParam.value, + unsetNameById, + }); + } + } if (newResource.body) { newResource.body = unsetExpressionVariables({ expression: newResource.body, @@ -1174,6 +1182,18 @@ export const insertWebstudioFragmentCopy = ({ }); header.value = replaceDataSources(header.value, newDataSourceIds); } + if (resource.searchParams) { + for (const searchParam of resource.searchParams) { + searchParam.value = restoreExpressionVariables({ + expression: searchParam.value, + maskedIdByName, + }); + searchParam.value = replaceDataSources( + searchParam.value, + newDataSourceIds + ); + } + } if (resource.body) { resource.body = restoreExpressionVariables({ expression: resource.body, diff --git a/apps/builder/app/shared/nano-states/props.test.tsx b/apps/builder/app/shared/nano-states/props.test.tsx index 0df4b0e58382..507ddafc73b0 100644 --- a/apps/builder/app/shared/nano-states/props.test.tsx +++ b/apps/builder/app/shared/nano-states/props.test.tsx @@ -797,6 +797,7 @@ test("compute resource variable values", () => { const resourceVariable = new ResourceValue("resourceVariable", { url: expression`""`, method: "get", + searchParams: [], headers: [], }); const data = renderData( @@ -868,6 +869,7 @@ test("compute parameter and resource variables without values to make it availab const resourceVariable = new ResourceValue("resourceVariable", { url: expression`""`, method: "get", + searchParams: [], headers: [], }); const parameterVariable = new Parameter("parameterVariable"); diff --git a/apps/builder/app/shared/resources.ts b/apps/builder/app/shared/resources.ts index 815368b595b0..3f4078eb6fc6 100644 --- a/apps/builder/app/shared/resources.ts +++ b/apps/builder/app/shared/resources.ts @@ -7,18 +7,24 @@ import { fetch } from "./fetch.client"; const MAX_PENDING_RESOURCES = 5; -export const getResourceKey = (resource: ResourceRequest) => - hash( - JSON.stringify([ - // explicitly list all fields to keep hash stable - resource.name, - resource.method, - resource.url, - resource.searchParams, - resource.headers, - resource.body, - ]) - ); +export const getResourceKey = (resource: ResourceRequest) => { + try { + return hash( + JSON.stringify([ + // explicitly list all fields to keep hash stable + resource.name, + resource.method, + resource.url, + resource.searchParams, + resource.headers, + resource.body, + ]) + ); + } catch { + // guard from invalid resources + return ""; + } +}; const queue = new Map(); const pending = new Map(); diff --git a/packages/html-data/bin/attributes.ts b/packages/html-data/bin/attributes.ts index fb3072e46250..7e01f686275f 100644 --- a/packages/html-data/bin/attributes.ts +++ b/packages/html-data/bin/attributes.ts @@ -77,6 +77,10 @@ const overrides: Record>> = { closedby: false, }, img: { + src: { required: true }, + alt: { required: true }, + width: { required: true }, + height: { required: true }, ismap: false, }, input: { diff --git a/packages/html-data/src/__generated__/attributes.ts b/packages/html-data/src/__generated__/attributes.ts index 744ea347aa19..05e416d4c7b9 100644 --- a/packages/html-data/src/__generated__/attributes.ts +++ b/packages/html-data/src/__generated__/attributes.ts @@ -873,12 +873,6 @@ const attribute_preserveAspectRatio_19as3ta: Attribute = { type: "string", }; -const attribute_alt_1j06s5r: Attribute = { - name: "alt", - description: "Replacement text for use when images are not available", - type: "string", -}; - const attribute_crossorigin_jl1m2v: Attribute = { name: "crossorigin", description: "How the element handles crossorigin requests", @@ -893,6 +887,12 @@ const attribute_loading_yzzdw4: Attribute = { options: ["lazy", "eager"], }; +const attribute_alt_1j06s5r: Attribute = { + name: "alt", + description: "Replacement text for use when images are not available", + type: "string", +}; + const attribute_disabled_1ceu012: Attribute = { name: "disabled", description: "Whether the form control is disabled", @@ -3824,7 +3824,12 @@ export const attributesByTag: Record = { attribute_y_14t62ez, ], img: [ - attribute_alt_1j06s5r, + { + name: "alt", + description: "Replacement text for use when images are not available", + type: "string", + required: true, + }, attribute_crossorigin_jl1m2v, { name: "decoding", @@ -3839,18 +3844,33 @@ export const attributesByTag: Record = { type: "select", options: ["auto", "high", "low"], }, - attribute_height_10887hn, + { + name: "height", + description: "Vertical dimension", + type: "number", + required: true, + }, attribute_loading_yzzdw4, attribute_referrerpolicy_tpprqt, attribute_sizes_o9chmv, - attribute_src_hol6ri, + { + name: "src", + description: "Address of the resource", + type: "string", + required: true, + }, attribute_srcset_1xpiw3a, { name: "usemap", description: "Name of image map to use", type: "string", }, - attribute_width_d9q964, + { + name: "width", + description: "Horizontal dimension", + type: "number", + required: true, + }, ], input: [ { diff --git a/packages/react-sdk/src/component-generator.test.tsx b/packages/react-sdk/src/component-generator.test.tsx index 8f1520e4190f..19cb23f23850 100644 --- a/packages/react-sdk/src/component-generator.test.tsx +++ b/packages/react-sdk/src/component-generator.test.tsx @@ -707,6 +707,7 @@ test("generate resources loading", () => { const dataResource = new ResourceValue("data", { url: expression`""`, method: "get", + searchParams: [], headers: [], }); expect( @@ -747,6 +748,7 @@ test("avoid generating unused variables", () => { const unusedResource = new ResourceValue("Unused Resource Name", { url: expression`""`, method: "get", + searchParams: [], headers: [], }); const data = renderData( @@ -883,11 +885,13 @@ test("generate resource prop", () => { const myResource = new ResourceValue("myResource", { url: expression`"https://my-url.com?with-secret"`, method: "get", + searchParams: [], headers: [], }); const anotherResource = new ResourceValue("anotherResource", { url: expression`"https://another-url.com?with-secret"`, method: "get", + searchParams: [], headers: [], }); expect( diff --git a/packages/sdk/src/resource-loader.ts b/packages/sdk/src/resource-loader.ts index 26bef82d4cbf..b49d0606eba6 100644 --- a/packages/sdk/src/resource-loader.ts +++ b/packages/sdk/src/resource-loader.ts @@ -25,13 +25,19 @@ export const loadResource = async ( ) => { try { const { method, searchParams, headers, body } = resourceRequest; - // cloudflare workers fail when fetching url contains spaces - // even though new URL suppose to trim them on parsing by spec - const url = new URL(resourceRequest.url.trim()); - if (searchParams) { - for (const { name, value } of searchParams) { - url.searchParams.append(name, serializeValue(value)); + let href = resourceRequest.url; + try { + // cloudflare workers fail when fetching url contains spaces + // even though new URL suppose to trim them on parsing by spec + const url = new URL(resourceRequest.url.trim()); + if (searchParams) { + for (const { name, value } of searchParams) { + url.searchParams.append(name, serializeValue(value)); + } } + href = url.href; + } catch { + // empty block } const requestHeaders = new Headers( headers.map(({ name, value }): [string, string] => [ @@ -46,7 +52,7 @@ export const loadResource = async ( if (method !== "get" && body !== undefined) { requestInit.body = serializeValue(body); } - const response = await customFetch(url.href, requestInit); + const response = await customFetch(href, requestInit); let data = await response.text(); @@ -59,7 +65,7 @@ export const loadResource = async ( if (!response.ok) { console.error( - `Failed to load resource: ${url} - ${response.status}: ${JSON.stringify(data).slice(0, 300)}` + `Failed to load resource: ${href} - ${response.status}: ${JSON.stringify(data).slice(0, 300)}` ); } diff --git a/packages/sdk/src/resources-generator.test.tsx b/packages/sdk/src/resources-generator.test.tsx index 9e621ac58e53..85d50867e0e3 100644 --- a/packages/sdk/src/resources-generator.test.tsx +++ b/packages/sdk/src/resources-generator.test.tsx @@ -199,6 +199,7 @@ test("generate global system variable and use in resources loader", () => { const myResource = new ResourceValue("My Resource", { url: expression`"https://my-json.com/" + $ws$system.params.slug`, method: "post", + searchParams: [{ name: "filter", value: expression`{search:'term'}` }], headers: [{ name: "Content-Type", value: expression`"application/json"` }], body: expression`{ body: true }`, }); @@ -220,6 +221,7 @@ test("generate global system variable and use in resources loader", () => { name: "My Resource", url: "https://my-json.com/" + system?.params?.slug, searchParams: [ + { name: "filter", value: {search:'term'} }, ], method: "post", headers: [ diff --git a/packages/template/src/jsx.test.tsx b/packages/template/src/jsx.test.tsx index 1ea217f940ac..0bddf27cb60a 100644 --- a/packages/template/src/jsx.test.tsx +++ b/packages/template/src/jsx.test.tsx @@ -517,6 +517,7 @@ test("render resource variable", () => { const myResource = new ResourceValue("myResource", { url: expression`"https://my-url.com/" + ${value}`, method: "get", + searchParams: [{ name: "filter", value: expression`${value}` }], headers: [{ name: "auth", value: expression`${value}` }], body: expression`${value}`, }); @@ -545,6 +546,7 @@ test("render resource variable", () => { name: "myResource", url: `"https://my-url.com/" + $ws$dataSource$1`, method: "get", + searchParams: [{ name: "filter", value: `$ws$dataSource$1` }], headers: [{ name: "auth", value: `$ws$dataSource$1` }], body: `$ws$dataSource$1`, }, @@ -556,6 +558,7 @@ test("render resource prop", () => { const myResource = new ResourceValue("myResource", { url: expression`"https://my-url.com/" + ${value}`, method: "get", + searchParams: [{ name: "filter", value: expression`${value}` }], headers: [{ name: "auth", value: expression`${value}` }], body: expression`${value}`, }); @@ -586,6 +589,7 @@ test("render resource prop", () => { name: "myResource", url: `"https://my-url.com/" + $ws$dataSource$1`, method: "get", + searchParams: [{ name: "filter", value: `$ws$dataSource$1` }], headers: [{ name: "auth", value: `$ws$dataSource$1` }], body: `$ws$dataSource$1`, }, diff --git a/packages/template/src/jsx.ts b/packages/template/src/jsx.ts index a55e6af1c8b7..3189da1f9dc0 100644 --- a/packages/template/src/jsx.ts +++ b/packages/template/src/jsx.ts @@ -35,6 +35,7 @@ export class Parameter { type ResourceConfig = { url: Expression; method: Resource["method"]; + searchParams: Array<{ name: string; value: Expression }>; headers: Array<{ name: string; value: Expression }>; body?: Expression; }; @@ -218,6 +219,12 @@ export const renderTemplate = ( name: resourceValue.name, url: compileExpression(instanceId, resourceValue.config.url), method: resourceValue.config.method, + searchParams: resourceValue.config.searchParams.map( + ({ name, value }) => ({ + name, + value: compileExpression(instanceId, value), + }) + ), headers: resourceValue.config.headers.map(({ name, value }) => ({ name, value: compileExpression(instanceId, value),