diff --git a/apps/builder/app/builder/features/settings-panel/variables-section.tsx b/apps/builder/app/builder/features/settings-panel/variables-section.tsx index 7f22ce689ee0..3c808c83688b 100644 --- a/apps/builder/app/builder/features/settings-panel/variables-section.tsx +++ b/apps/builder/app/builder/features/settings-panel/variables-section.tsx @@ -26,14 +26,11 @@ import { } from "@webstudio-is/design-system"; import { EllipsesIcon, PlusIcon } from "@webstudio-is/icons"; import type { DataSource } from "@webstudio-is/sdk"; -import { - decodeDataSourceVariable, - findPageByIdOrPath, - getExpressionIdentifiers, -} from "@webstudio-is/sdk"; +import { findPageByIdOrPath } from "@webstudio-is/sdk"; import { $dataSources, $instances, + $pages, $props, $resources, $variableValuesByInstanceSelector, @@ -59,6 +56,7 @@ import { updateWebstudioData } from "~/shared/instance-utils"; import { deleteVariableMutable, findAvailableVariables, + findUsedVariables, } from "~/shared/data-variables"; /** @@ -91,71 +89,20 @@ const $instanceVariableValues = computed( new Map() ); -/** - * find variables used in - * - * instance children - * expression prop - * action prop - * url resource field - * header resource field - * body resource fiel - */ const $usedVariables = computed( - [$instances, $props, $resources, $selectedPage], - (instances, props, resources, page) => { - const usedVariables = new Map(); - const collectExpressionVariables = (expression: string) => { - const identifiers = getExpressionIdentifiers(expression); - for (const identifier of identifiers) { - const id = decodeDataSourceVariable(identifier); - if (id !== undefined) { - const count = usedVariables.get(id) ?? 0; - usedVariables.set(id, count + 1); - } - } - }; - for (const instance of instances.values()) { - for (const child of instance.children) { - if (child.type === "expression") { - collectExpressionVariables(child.value); - } - } - } - for (const resource of resources.values()) { - collectExpressionVariables(resource.url); - for (const { value } of resource.headers) { - collectExpressionVariables(value); - } - if (resource.body) { - collectExpressionVariables(resource.body); - } - } - for (const prop of props.values()) { - if (prop.type === "expression") { - collectExpressionVariables(prop.value); - } - if (prop.type === "action") { - for (const value of prop.value) { - collectExpressionVariables(value.code); - } - } - } - if (page) { - collectExpressionVariables(page.title); - collectExpressionVariables(page.meta.description ?? ""); - collectExpressionVariables(page.meta.excludePageFromSearch ?? ""); - collectExpressionVariables(page.meta.socialImageUrl ?? ""); - collectExpressionVariables(page.meta.language ?? ""); - collectExpressionVariables(page.meta.status ?? ""); - collectExpressionVariables(page.meta.redirect ?? ""); - if (page.meta.custom) { - for (const { content } of page.meta.custom) { - collectExpressionVariables(content); - } - } + [$selectedInstance, $pages, $instances, $props, $dataSources, $resources], + (selectedInstance, pages, instances, props, dataSources, resources) => { + if (selectedInstance === undefined) { + return new Map(); } - return usedVariables; + return findUsedVariables({ + startingInstanceId: selectedInstance.id, + pages, + instances, + props, + dataSources, + resources, + }); } ); diff --git a/apps/builder/app/shared/data-variables.test.tsx b/apps/builder/app/shared/data-variables.test.tsx index e14835a85c92..817a62abb122 100644 --- a/apps/builder/app/shared/data-variables.test.tsx +++ b/apps/builder/app/shared/data-variables.test.tsx @@ -656,3 +656,85 @@ test("unset global variables in slots when delete", () => { { type: "expression", value: "globalVariable" }, ]); }); + +test("unset body variables in page meta when delete", () => { + const bodyVariable = new Variable("bodyVariable", ""); + const data = { + pages: createDefaultPages({ rootInstanceId: "bodyId" }), + ...renderData( + <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> + ), + }; + expect(data.dataSources.size).toEqual(1); + const [bodyVariableId] = data.dataSources.keys(); + const bodyIdentifier = encodeDataVariableId(bodyVariableId); + data.pages.homePage.title = bodyIdentifier; + data.pages.homePage.meta = { + description: bodyIdentifier, + excludePageFromSearch: bodyIdentifier, + socialImageUrl: bodyIdentifier, + language: bodyIdentifier, + status: bodyIdentifier, + redirect: bodyIdentifier, + custom: [{ property: "auth", content: bodyIdentifier }], + }; + deleteVariableMutable(data, bodyVariableId); + expect(data.pages.homePage.title).toEqual(`bodyVariable`); + expect(data.pages.homePage.meta.description).toEqual(`bodyVariable`); + expect(data.pages.homePage.meta.excludePageFromSearch).toEqual( + `bodyVariable` + ); + expect(data.pages.homePage.meta.socialImageUrl).toEqual(`bodyVariable`); + expect(data.pages.homePage.meta.language).toEqual(`bodyVariable`); + expect(data.pages.homePage.meta.status).toEqual(`bodyVariable`); + expect(data.pages.homePage.meta.redirect).toEqual(`bodyVariable`); + expect(data.pages.homePage.meta.custom?.[0].content).toEqual(`bodyVariable`); +}); + +test("unset global variables in all pages meta when delete", () => { + const globalVariable = new Variable("globalVariable", ""); + const data = { + pages: createDefaultPages({ rootInstanceId: "homeBodyId" }), + ...renderData( + + <$.Body ws:id="homeBodyId"> + <$.Body ws:id="aboutBodyId"> + + ), + }; + data.instances.delete(ROOT_INSTANCE_ID); + data.pages.pages.push({ + id: "", + name: "", + path: "", + title: "", + meta: {}, + rootInstanceId: "aboutBodyId", + }); + expect(data.dataSources.size).toEqual(1); + const [globalVariableId] = data.dataSources.keys(); + const globalIdentifier = encodeDataVariableId(globalVariableId); + for (const page of [data.pages.homePage, ...data.pages.pages]) { + page.title = globalIdentifier; + page.meta = { + description: globalIdentifier, + excludePageFromSearch: globalIdentifier, + socialImageUrl: globalIdentifier, + language: globalIdentifier, + status: globalIdentifier, + redirect: globalIdentifier, + custom: [{ property: "auth", content: globalIdentifier }], + }; + } + deleteVariableMutable(data, globalVariableId); + for (const page of [data.pages.homePage, ...data.pages.pages]) { + expect(page.title).toEqual(`globalVariable`); + expect(page.meta.description).toEqual(`globalVariable`); + expect(page.meta.excludePageFromSearch).toEqual(`globalVariable`); + expect(page.meta.socialImageUrl).toEqual(`globalVariable`); + expect(page.meta.language).toEqual(`globalVariable`); + expect(page.meta.status).toEqual(`globalVariable`); + expect(page.meta.redirect).toEqual(`globalVariable`); + expect(page.meta.custom?.[0].content).toEqual(`globalVariable`); + } +}); diff --git a/apps/builder/app/shared/data-variables.ts b/apps/builder/app/shared/data-variables.ts index 64914c396be9..e71b2f327545 100644 --- a/apps/builder/app/shared/data-variables.ts +++ b/apps/builder/app/shared/data-variables.ts @@ -14,6 +14,7 @@ import { encodeDataVariableId, findTreeInstanceIds, findTreeInstanceIdsExcludingSlotDescendants, + getExpressionIdentifiers, systemParameter, transpileExpression, } from "@webstudio-is/sdk"; @@ -277,18 +278,54 @@ const traverseExpressions = ({ update: (expression: string, args?: string[]) => void | string; }) => { const pagesList = pages ? [pages.homePage, ...pages.pages] : []; + let instanceIds = findTreeInstanceIdsExcludingSlotDescendants( instances, startingInstanceId ); - // global variables can be accessed on all pages and inside of slots - if (startingInstanceId === ROOT_INSTANCE_ID) { - for (const page of pagesList) { + for (const page of pagesList) { + // global variables can be accessed on all pages and inside of slots + if (startingInstanceId === ROOT_INSTANCE_ID) { instanceIds = setUnion( instanceIds, findTreeInstanceIds(instances, page.rootInstanceId) ); } + + // global and body variables can be accessed in pages meta + if ( + startingInstanceId === page.rootInstanceId || + startingInstanceId === ROOT_INSTANCE_ID + ) { + page.title = update(page.title) ?? page.title; + if (page.meta.description) { + page.meta.description = + update(page.meta.description) ?? page.meta.description; + } + if (page.meta.excludePageFromSearch) { + page.meta.excludePageFromSearch = + update(page.meta.excludePageFromSearch) ?? + page.meta.excludePageFromSearch; + } + if (page.meta.socialImageUrl) { + page.meta.socialImageUrl = + update(page.meta.socialImageUrl) ?? page.meta.socialImageUrl; + } + if (page.meta.language) { + page.meta.language = update(page.meta.language) ?? page.meta.language; + } + if (page.meta.status) { + page.meta.status = update(page.meta.status) ?? page.meta.status; + } + if (page.meta.redirect) { + page.meta.redirect = update(page.meta.redirect) ?? page.meta.redirect; + } + if (page.meta.custom) { + for (const item of page.meta.custom) { + item.content = update(item.content) ?? item.content; + } + } + } } const resourceIds = new Set(); @@ -298,10 +335,7 @@ const traverseExpressions = ({ } for (const child of instance.children) { if (child.type === "expression") { - const newExpression = update(child.value); - if (newExpression !== undefined) { - child.value = newExpression; - } + child.value = update(child.value) ?? child.value; } } } @@ -311,18 +345,12 @@ const traverseExpressions = ({ continue; } if (prop.type === "expression") { - const newExpression = update(prop.value); - if (newExpression !== undefined) { - prop.value = newExpression; - } + prop.value = update(prop.value) ?? prop.value; continue; } if (prop.type === "action") { for (const action of prop.value) { - const newExpression = update(action.code, action.args); - if (newExpression !== undefined) { - action.code = newExpression; - } + action.code = update(action.code, action.args) ?? action.code; } continue; } @@ -345,21 +373,12 @@ const traverseExpressions = ({ if (resourceIds.has(resource.id) === false) { continue; } - const newExpression = update(resource.url); - if (newExpression !== undefined) { - resource.url = newExpression; - } + resource.url = update(resource.url) ?? resource.url; for (const header of resource.headers) { - const newExpression = update(header.value); - if (newExpression !== undefined) { - header.value = newExpression; - } + header.value = update(header.value) ?? header.value; } if (resource.body) { - const newExpression = update(resource.body); - if (newExpression !== undefined) { - resource.body = newExpression; - } + resource.body = update(resource.body) ?? resource.body; } } }; @@ -379,7 +398,7 @@ export const findUnsetVariableNames = ({ }) => { const unsetVariables = new Set(); traverseExpressions({ - startingInstanceId: startingInstanceId, + startingInstanceId, pages: undefined, instances, props, @@ -400,6 +419,43 @@ export const findUnsetVariableNames = ({ return Array.from(unsetVariables); }; +export const findUsedVariables = ({ + startingInstanceId, + pages, + instances, + props, + dataSources, + resources, +}: { + startingInstanceId: Instance["id"]; + pages: undefined | Pages; + instances: Instances; + props: Props; + dataSources: DataSources; + resources: Resources; +}) => { + const usedVariables = new Map(); + traverseExpressions({ + startingInstanceId, + pages, + instances, + props, + dataSources, + resources, + update: (expression) => { + const identifiers = getExpressionIdentifiers(expression); + for (const identifier of identifiers) { + const id = decodeDataVariableId(identifier); + if (id !== undefined) { + const count = usedVariables.get(id) ?? 0; + usedVariables.set(id, count + 1); + } + } + }, + }); + return usedVariables; +}; + export const rebindTreeVariablesMutable = ({ startingInstanceId, instances,