diff --git a/apps/builder/app/builder/shared/binding-popover.test.ts b/apps/builder/app/builder/shared/binding-popover.test.ts index e4c398491a12..a8485adedfd8 100644 --- a/apps/builder/app/builder/shared/binding-popover.test.ts +++ b/apps/builder/app/builder/shared/binding-popover.test.ts @@ -1,6 +1,9 @@ import { expect, test } from "vitest"; import { encodeDataSourceVariable } from "@webstudio-is/sdk"; -import { evaluateExpressionWithinScope } from "./binding-popover"; +import { + evaluateExpressionWithinScope, + validateExpressionScope, +} from "./binding-popover"; test("evaluateExpressionWithinScope works", () => { const variableName = "jsonVariable"; @@ -13,3 +16,15 @@ test("evaluateExpressionWithinScope works", () => { }) ).toEqual(2); }); + +test("validateExpressionScope reports unavailable variables", () => { + const availableVariable = encodeDataSourceVariable("available"); + const missingVariable = encodeDataSourceVariable("missing"); + + expect( + validateExpressionScope( + `${availableVariable} + ${missingVariable}`, + new Map([[availableVariable, "available"]]) + ) + ).toEqual(`"${missingVariable}" is not defined in the scope`); +}); diff --git a/apps/builder/app/builder/shared/binding-popover.tsx b/apps/builder/app/builder/shared/binding-popover.tsx index 6c0afac1c3be..25124baad108 100644 --- a/apps/builder/app/builder/shared/binding-popover.tsx +++ b/apps/builder/app/builder/shared/binding-popover.tsx @@ -89,6 +89,17 @@ export const evaluateExpressionWithinScope = ( return computeExpression(expression, variables); }; +export const validateExpressionScope = ( + expression: string, + aliases: Map +) => { + const diagnostics = lintExpression({ + expression, + availableVariables: new Set(aliases.keys()), + }); + return diagnostics[0]?.message; +}; + const BindingPanel = ({ scope, aliases, @@ -385,7 +396,9 @@ export const BindingPopover = ({ return; } - const valueError = validate?.(evaluateExpressionWithinScope(value, scope)); + const valueError = + validateExpressionScope(value, aliases) ?? + validate?.(evaluateExpressionWithinScope(value, scope)); return ( { ]); }); -test("find global variables in slots", () => { +test("find parent variables in slots", () => { const globalVariable = new Variable("globalVariable", ""); const bodyVariable = new Variable("bodyVariable", ""); const boxVariable = new Variable("boxVariable", ""); @@ -128,6 +128,7 @@ test("find global variables in slots", () => { ).toEqual([ expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }), expect.objectContaining({ name: "globalVariable" }), + expect.objectContaining({ name: "bodyVariable" }), expect.objectContaining({ name: "boxVariable" }), ]); }); @@ -569,7 +570,7 @@ test("preserve other variables when rebind", () => { ]); }); -test("prevent rebinding tree variables from slots", () => { +test("rebind tree variables from slot ancestors", () => { const bodyVariable = new Variable("myVariable", "one value of body"); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}> @@ -585,8 +586,9 @@ test("prevent rebinding tree variables from slots", () => { pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); + const [bodyVariableId] = data.dataSources.keys(); expect(data.instances.get("boxId")?.children).toEqual([ - { type: "expression", value: "myVariable" }, + { type: "expression", value: encodeDataVariableId(bodyVariableId) }, ]); }); @@ -744,7 +746,7 @@ test("rebind expressions with parent variable when delete variable on child", () ]); }); -test("prevent rebinding with variables outside of slot content scope", () => { +test("rebind with variables outside of slot content scope", () => { const bodyVariable = new Variable("myVariable", "one value of body"); const boxVariable = new Variable("myVariable", "one value of body"); const data = renderData( @@ -762,10 +764,10 @@ test("prevent rebinding with variables outside of slot content scope", () => { expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "boxId" }), ]); - const [_bodyVariableId, boxVariableId] = data.dataSources.keys(); + const [bodyVariableId, boxVariableId] = data.dataSources.keys(); deleteVariableMutable(data, boxVariableId); expect(data.instances.get("textId")?.children).toEqual([ - { type: "expression", value: "myVariable" }, + { type: "expression", value: encodeDataVariableId(bodyVariableId) }, ]); }); diff --git a/apps/builder/app/shared/data-variables.ts b/apps/builder/app/shared/data-variables.ts index c9e31dda388c..f41facf59444 100644 --- a/apps/builder/app/shared/data-variables.ts +++ b/apps/builder/app/shared/data-variables.ts @@ -194,10 +194,6 @@ export const computeExpression = ( const getParentInstanceById = (instances: Instances) => { const parentInstanceById = new Map(); for (const instance of instances.values()) { - // interrupt lookup because slot variables cannot be passed to slot content - if (instance.component === "Slot") { - continue; - } for (const child of instance.children) { if (child.type === "id") { parentInstanceById.set(child.value, instance.id); diff --git a/apps/builder/app/shared/nano-states/props.test.tsx b/apps/builder/app/shared/nano-states/props.test.tsx index d59b50bbff28..7e7f56110157 100644 --- a/apps/builder/app/shared/nano-states/props.test.tsx +++ b/apps/builder/app/shared/nano-states/props.test.tsx @@ -1016,7 +1016,7 @@ test("compute resource variable values", () => { ).toEqual("my-value"); }); -test("stop variables lookup outside of slots", () => { +test("inherit variables from outside of slots", () => { const bodyVariable = new Variable("bodyVariable", "body"); const slotVariable = new Variable("slotVariable", "slot"); const boxVariable = new Variable("boxVariable", "box"); @@ -1041,7 +1041,7 @@ test("stop variables lookup outside of slots", () => { values.get( getInstanceKey(["fragmentId", "slotId", "bodyId", ROOT_INSTANCE_ID]) )?.size - ).toEqual(1); + ).toEqual(3); expect( values.get( getInstanceKey([ @@ -1052,8 +1052,7 @@ test("stop variables lookup outside of slots", () => { ROOT_INSTANCE_ID, ]) )?.size - // global system and box variable - ).toEqual(2); + ).toEqual(4); }); test("compute parameter and resource variables without values to make it available in scope", () => { @@ -1259,7 +1258,7 @@ test("inherit variables from global root inside slots", () => { $instances.set(data.instances); $dataSources.set(data.dataSources); $props.set(data.props); - const [rootVariableId, _bodyVariableId, boxVariableId] = + const [rootVariableId, bodyVariableId, boxVariableId] = data.dataSources.keys(); selectPageRoot("bodyId"); expect( @@ -1278,6 +1277,7 @@ test("inherit variables from global root inside slots", () => { new Map([ [SYSTEM_VARIABLE_ID, initialSystem], [rootVariableId, "root"], + [bodyVariableId, "body"], [boxVariableId, "box"], ]) ); diff --git a/apps/builder/app/shared/nano-states/props.ts b/apps/builder/app/shared/nano-states/props.ts index d4326fe2d327..b2447f11dd61 100644 --- a/apps/builder/app/shared/nano-states/props.ts +++ b/apps/builder/app/shared/nano-states/props.ts @@ -11,7 +11,6 @@ import { encodeDataSourceVariable, transpileExpression, collectionComponent, - portalComponent, ROOT_INSTANCE_ID, SYSTEM_VARIABLE_ID, findTreeInstanceIds, @@ -548,12 +547,6 @@ export const $variableValuesByInstanceSelector = computed( } return; } - // reset values for slot children to let slots behave as isolated components - if (instance.component === portalComponent) { - // allow accessing global variables in slots - variableValues = globalVariableValues; - variableNames = globalVariableNames; - } for (const child of instance.children) { if (child.type === "id") { traverseInstances(