diff --git a/apps/builder/app/builder/features/pages/page-settings.tsx b/apps/builder/app/builder/features/pages/page-settings.tsx index e3be96d1a6d3..df94c7e956b0 100644 --- a/apps/builder/app/builder/features/pages/page-settings.tsx +++ b/apps/builder/app/builder/features/pages/page-settings.tsx @@ -71,7 +71,6 @@ import { $assets, $instances, $pages, - $dataSources, $publishedOrigin, $project, $userPlanFeatures, @@ -1365,36 +1364,26 @@ const NewPageSettingsView = ({ const createPage = (pageId: Page["id"], values: Values) => { serverSyncStore.createTransaction( - [$pages, $instances, $dataSources], - (pages, instances, dataSources) => { + [$pages, $instances], + (pages, instances) => { if (pages === undefined) { return; } const rootInstanceId = nanoid(); - const systemDataSourceId = nanoid(); pages.pages.push({ id: pageId, name: values.name, path: values.path, title: values.title, rootInstanceId, - systemDataSourceId, meta: {}, }); - instances.set(rootInstanceId, { type: "instance", id: rootInstanceId, component: "Body", children: [], }); - dataSources.set(systemDataSourceId, { - id: systemDataSourceId, - scopeInstanceId: rootInstanceId, - name: "system", - type: "parameter", - }); - registerFolderChildMutable(pages.folders, pageId, values.parentFolderId); selectInstance(undefined); } 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 881fc48ca40c..172ebe39eaa5 100644 --- a/apps/builder/app/builder/features/pages/page-utils.test.ts +++ b/apps/builder/app/builder/features/pages/page-utils.test.ts @@ -6,6 +6,7 @@ import { type Folder, ROOT_FOLDER_ID, type Page, + SYSTEM_VARIABLE_ID, } from "@webstudio-is/sdk"; import { cleanupChildRefsMutable, @@ -31,10 +32,15 @@ import { updateCurrentSystem } from "~/shared/system"; setEnv("*"); registerContainers(); +const initialSystem = { + origin: "https://undefined.wstd.work", + params: {}, + search: {}, +}; + const createPages = () => { const data = createDefaultPages({ rootInstanceId: "rootInstanceId", - systemDataSourceId: "systemDataSourceId", homePageId: "homePageId", }); @@ -65,7 +71,6 @@ const createPages = () => { name: id, path, rootInstanceId: "rootInstanceId", - systemDataSourceId: "systemDataSourceId", title: `"${id}"`, }; return page; @@ -108,7 +113,6 @@ describe("reparentOrphansMutable", () => { name: "Page", path: "/page", rootInstanceId: "rootInstanceId", - systemDataSourceId: "systemDataSourceId", title: `"Page"`, }); pages.folders.push({ @@ -402,7 +406,6 @@ test("page root scope should rely on selected page", () => { const pages = createDefaultPages({ rootInstanceId: "homeRootId", homePageId: "homePageId", - systemDataSourceId: "system", }); pages.pages.push({ id: "pageId", @@ -411,7 +414,6 @@ test("page root scope should rely on selected page", () => { path: "/", title: `"My Title"`, meta: {}, - systemDataSourceId: "system", }); $pages.set(pages); $awareness.set({ pageId: "pageId" }); @@ -434,9 +436,18 @@ test("page root scope should rely on selected page", () => { ]) ); expect($pageRootScope.get()).toEqual({ - aliases: new Map([["$ws$dataSource$2", "page variable"]]), - scope: { $ws$dataSource$2: "" }, - variableValues: new Map([["2", ""]]), + aliases: new Map([ + ["$ws$system", "system"], + ["$ws$dataSource$2", "page variable"], + ]), + scope: { + $ws$system: initialSystem, + $ws$dataSource$2: "", + }, + variableValues: new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], + ["2", ""], + ]), }); }); @@ -445,7 +456,6 @@ test("page root scope should use variable and resource values", () => { createDefaultPages({ rootInstanceId: "homeRootId", homePageId: "homePageId", - systemDataSourceId: "system", }) ); $awareness.set({ pageId: "homePageId" }); @@ -473,21 +483,24 @@ test("page root scope should use variable and resource values", () => { $resourceValues.set(new Map([["resourceId", "resource variable value"]])); expect($pageRootScope.get()).toEqual({ aliases: new Map([ + ["$ws$system", "system"], ["$ws$dataSource$valueVariableId", "value variable"], ["$ws$dataSource$resourceVariableId", "resource variable"], ]), scope: { + $ws$system: initialSystem, $ws$dataSource$resourceVariableId: "resource variable value", $ws$dataSource$valueVariableId: "value variable value", }, - variableValues: new Map([ + variableValues: new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], ["valueVariableId", "value variable value"], ["resourceVariableId", "resource variable value"], ]), }); }); -test("page root scope should prefill default system variable value", () => { +test("page root scope should provide page system variable value", () => { $pages.set( createDefaultPages({ rootInstanceId: "homeRootId", diff --git a/apps/builder/app/builder/features/pages/page-utils.ts b/apps/builder/app/builder/features/pages/page-utils.ts index 08ca73c7ea2f..cbdb726f1fcf 100644 --- a/apps/builder/app/builder/features/pages/page-utils.ts +++ b/apps/builder/app/builder/features/pages/page-utils.ts @@ -12,6 +12,8 @@ import { ROOT_FOLDER_ID, isRootFolder, ROOT_INSTANCE_ID, + systemParameter, + SYSTEM_VARIABLE_ID, } from "@webstudio-is/sdk"; import { removeByMutable } from "~/shared/array-utils"; import { @@ -259,7 +261,10 @@ export const $pageRootScope = computed( getInstanceKey([page.rootInstanceId, ROOT_INSTANCE_ID]) ) ?? new Map(); for (const [dataSourceId, value] of values) { - const dataSource = dataSources.get(dataSourceId); + let dataSource = dataSources.get(dataSourceId); + if (dataSourceId === SYSTEM_VARIABLE_ID) { + dataSource = systemParameter; + } if (dataSource === undefined) { 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 e2e92a7f42ac..0d045490f84f 100644 --- a/apps/builder/app/builder/features/settings-panel/resource-panel.tsx +++ b/apps/builder/app/builder/features/settings-panel/resource-panel.tsx @@ -17,6 +17,8 @@ import { generateObjectExpression, isLiteralExpression, parseObjectExpression, + SYSTEM_VARIABLE_ID, + systemParameter, } from "@webstudio-is/sdk"; import { sitemapResourceUrl } from "@webstudio-is/sdk/runtime"; import { @@ -34,7 +36,6 @@ import { theme, } from "@webstudio-is/design-system"; import { TrashIcon, InfoCircleIcon, PlusIcon } from "@webstudio-is/icons"; -import { isFeatureEnabled } from "@webstudio-is/feature-flags"; import { humanizeString } from "~/shared/string-utils"; import { $dataSources, @@ -375,7 +376,7 @@ const $hiddenDataSourceIds = computed( dataSourceIds.add(dataSource.id); } } - if (page?.systemDataSourceId && isFeatureEnabled("filters")) { + if (page?.systemDataSourceId) { dataSourceIds.delete(page.systemDataSourceId); } return dataSourceIds; @@ -406,7 +407,10 @@ const $selectedInstanceScope = computed( if (hiddenDataSourceIds.has(dataSourceId)) { continue; } - const dataSource = dataSources.get(dataSourceId); + let dataSource = dataSources.get(dataSourceId); + if (dataSourceId === SYSTEM_VARIABLE_ID) { + dataSource = systemParameter; + } if (dataSource === undefined) { continue; } diff --git a/apps/builder/app/builder/features/settings-panel/shared.tsx b/apps/builder/app/builder/features/settings-panel/shared.tsx index e74bcad9d2bd..b340ac9d4369 100644 --- a/apps/builder/app/builder/features/settings-panel/shared.tsx +++ b/apps/builder/app/builder/features/settings-panel/shared.tsx @@ -14,6 +14,8 @@ import equal from "fast-deep-equal"; import { decodeDataSourceVariable, encodeDataSourceVariable, + SYSTEM_VARIABLE_ID, + systemParameter, } from "@webstudio-is/sdk"; import type { PropMeta, Prop, Asset } from "@webstudio-is/sdk"; import { InfoCircleIcon, MinusIcon } from "@webstudio-is/icons"; @@ -328,7 +330,10 @@ export const $selectedInstanceScope = computed( const values = variableValuesByInstanceSelector.get(instanceKey); if (values) { for (const [dataSourceId, value] of values) { - const dataSource = dataSources.get(dataSourceId); + let dataSource = dataSources.get(dataSourceId); + if (dataSourceId === SYSTEM_VARIABLE_ID) { + dataSource = systemParameter; + } if (dataSource === undefined) { continue; } 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 f23f26cfc25b..f7f0cf1195c9 100644 --- a/apps/builder/app/builder/features/settings-panel/variable-popover.tsx +++ b/apps/builder/app/builder/features/settings-panel/variable-popover.tsx @@ -44,6 +44,7 @@ import { type DataSource, transpileExpression, lintExpression, + SYSTEM_VARIABLE_ID, } from "@webstudio-is/sdk"; import { ExpressionEditor, @@ -735,6 +736,7 @@ export const VariablePopoverTrigger = ({ const { allowDynamicData } = useStore($userPlanFeatures); const [isResource, setIsResource] = useState(variable?.type === "resource"); const requiresUpgrade = allowDynamicData === false && isResource; + const isSystemVariable = variable?.id === SYSTEM_VARIABLE_ID; return ( { event.preventDefault(); - if (requiresUpgrade) { + if (requiresUpgrade || isSystemVariable) { return; } const nameElement = @@ -879,11 +881,17 @@ export const VariablePopoverTrigger = ({ > {/* submit is not triggered when press enter on input without submit button */} - - - + + + + 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 c5778c9ac946..7f22ce689ee0 100644 --- a/apps/builder/app/builder/features/settings-panel/variables-section.tsx +++ b/apps/builder/app/builder/features/settings-panel/variables-section.tsx @@ -28,6 +28,7 @@ import { EllipsesIcon, PlusIcon } from "@webstudio-is/icons"; import type { DataSource } from "@webstudio-is/sdk"; import { decodeDataSourceVariable, + findPageByIdOrPath, getExpressionIdentifiers, } from "@webstudio-is/sdk"; import { @@ -196,6 +197,7 @@ const VariablesItem = ({ value: unknown; usageCount: number; }) => { + const selectedPage = useStore($selectedPage); const [inspectDialogOpen, setInspectDialogOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); @@ -265,6 +267,23 @@ const VariablesItem = ({ Delete {usageCount > 0 && `(${usageCount} bindings)`} )} + {source === "local" && + variable.id === selectedPage?.systemDataSourceId && ( + { + updateWebstudioData((data) => { + const page = findPageByIdOrPath( + selectedPage.id, + data.pages + ); + delete page?.systemDataSourceId; + deleteVariableMutable(data, variable.id); + }); + }} + > + Delete + + )} diff --git a/apps/builder/app/shared/data-variables.test.tsx b/apps/builder/app/shared/data-variables.test.tsx index ccce7602738e..f46ed1eb0d4e 100644 --- a/apps/builder/app/shared/data-variables.test.tsx +++ b/apps/builder/app/shared/data-variables.test.tsx @@ -8,7 +8,11 @@ import { Variable, ws, } from "@webstudio-is/template"; -import { encodeDataVariableId, ROOT_INSTANCE_ID } from "@webstudio-is/sdk"; +import { + encodeDataVariableId, + ROOT_INSTANCE_ID, + SYSTEM_VARIABLE_ID, +} from "@webstudio-is/sdk"; import { computeExpression, decodeDataVariableName, @@ -56,6 +60,7 @@ test("find available variables", () => { expect( findAvailableVariables({ ...data, startingInstanceId: "boxId" }) ).toEqual([ + expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }), expect.objectContaining({ name: "bodyVariable" }), expect.objectContaining({ name: "boxVariable" }), ]); @@ -72,6 +77,7 @@ test("find masked variables", () => { expect( findAvailableVariables({ ...data, startingInstanceId: "boxId" }) ).toEqual([ + expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }), expect.objectContaining({ scopeInstanceId: "boxId", name: "myVariable" }), ]); }); @@ -90,6 +96,7 @@ test("find global variables", () => { expect( findAvailableVariables({ ...data, startingInstanceId: "boxId" }) ).toEqual([ + expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }), expect.objectContaining({ name: "globalVariable" }), expect.objectContaining({ name: "boxVariable" }), ]); @@ -114,6 +121,7 @@ test("find global variables in slots", () => { expect( findAvailableVariables({ ...data, startingInstanceId: "boxId" }) ).toEqual([ + expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }), expect.objectContaining({ name: "globalVariable" }), expect.objectContaining({ name: "boxVariable" }), ]); diff --git a/apps/builder/app/shared/data-variables.ts b/apps/builder/app/shared/data-variables.ts index e57a2036186b..3fd26d24ee18 100644 --- a/apps/builder/app/shared/data-variables.ts +++ b/apps/builder/app/shared/data-variables.ts @@ -8,9 +8,11 @@ import { type Resources, type WebstudioData, ROOT_INSTANCE_ID, + SYSTEM_VARIABLE_ID, decodeDataVariableId, encodeDataVariableId, findTreeInstanceIdsExcludingSlotDescendants, + systemParameter, transpileExpression, } from "@webstudio-is/sdk"; import { @@ -213,6 +215,8 @@ const findMaskedVariablesByInstanceId = ({ // allow accessing global variables everywhere instanceIdsPath.push(ROOT_INSTANCE_ID); const maskedVariables = new Map(); + // global system variable always present + maskedVariables.set("system", SYSTEM_VARIABLE_ID); // start from the root to descendant // so child variables override parent variables for (const instanceId of instanceIdsPath.reverse()) { @@ -245,6 +249,9 @@ export const findAvailableVariables = ({ if (dataSource) { availableVariables.push(dataSource); } + if (dataSourceId === SYSTEM_VARIABLE_ID) { + availableVariables.push(systemParameter); + } } return availableVariables; }; diff --git a/apps/builder/app/shared/instance-utils.ts b/apps/builder/app/shared/instance-utils.ts index 6e9724866445..600f3e9f711a 100644 --- a/apps/builder/app/shared/instance-utils.ts +++ b/apps/builder/app/shared/instance-utils.ts @@ -80,7 +80,7 @@ import { current, isDraft } from "immer"; * structuredClone can be invoked on draft and throw error * extract current snapshot before cloning */ -const unwrap = (value: Value) => +export const unwrap = (value: Value) => isDraft(value) ? current(value) : value; export const updateWebstudioData = (mutate: (data: WebstudioData) => void) => { @@ -522,7 +522,8 @@ const traverseStyleValue = ( export const extractWebstudioFragment = ( data: Omit, - rootInstanceId: string + rootInstanceId: string, + options: { unsetVariables?: Set } = {} ): WebstudioFragment => { const { assets, @@ -604,8 +605,12 @@ export const extractWebstudioFragment = ( const fragmentDataSources: DataSources = new Map(); const fragmentResourceIds = new Set(); const unsetNameById = new Map(); + const unsetVariables = options.unsetVariables ?? new Set(); for (const dataSource of dataSources.values()) { - if (fragmentInstanceIds.has(dataSource.scopeInstanceId ?? "")) { + if ( + fragmentInstanceIds.has(dataSource.scopeInstanceId ?? "") && + unsetVariables.has(dataSource.id) === false + ) { fragmentDataSources.set(dataSource.id, dataSource); if (dataSource.type === "resource") { fragmentResourceIds.add(dataSource.resourceId); diff --git a/apps/builder/app/shared/nano-states/props.test.tsx b/apps/builder/app/shared/nano-states/props.test.tsx index f20485ef0b45..ba397b15d877 100644 --- a/apps/builder/app/shared/nano-states/props.test.tsx +++ b/apps/builder/app/shared/nano-states/props.test.tsx @@ -6,6 +6,7 @@ import { DataSource, type Instance, ROOT_INSTANCE_ID, + SYSTEM_VARIABLE_ID, collectionComponent, } from "@webstudio-is/sdk"; import { textContentAttribute } from "@webstudio-is/react-sdk"; @@ -27,9 +28,15 @@ import { Variable, ws, } from "@webstudio-is/template"; -import { updateCurrentSystem } from "../system"; +import { $systemDataByPage, updateCurrentSystem } from "../system"; import { registerContainers } from "../sync"; +const initialSystem = { + origin: "https://undefined.wstd.work", + params: {}, + search: {}, +}; + registerContainers(); setEnv("*"); @@ -47,7 +54,7 @@ const setBoxInstance = (id: Instance["id"]) => { const selectPageRoot = ( rootInstanceId: Instance["id"], - systemDataSourceId: DataSource["id"] = "systemId" + systemDataSourceId?: DataSource["id"] ) => { const defaultPages = createDefaultPages({ homePageId: "pageId", @@ -90,7 +97,7 @@ test("collect prop values", () => { ]) ); expect( - $propValuesByInstanceSelector.get().get(JSON.stringify(["box"])) + $propValuesByInstanceSelector.get().get(getInstanceKey(["box"])) ).toEqual( new Map([ ["first", 0], @@ -149,7 +156,7 @@ test("compute expression prop values", () => { ]) ); expect( - $propValuesByInstanceSelector.get().get(JSON.stringify(["box"])) + $propValuesByInstanceSelector.get().get(getInstanceKey(["box"])) ).toEqual( new Map([ ["first", 3], @@ -160,7 +167,7 @@ test("compute expression prop values", () => { $dataSourceVariables.set(new Map([["var1", 4]])); expect( - $propValuesByInstanceSelector.get().get(JSON.stringify(["box"])) + $propValuesByInstanceSelector.get().get(getInstanceKey(["box"])) ).toEqual( new Map([ ["first", 6], @@ -212,13 +219,13 @@ test("generate action prop callbacks", () => { ); const values1 = $propValuesByInstanceSelector .get() - .get(JSON.stringify(["box"])); + .get(getInstanceKey(["box"])); expect(values1?.get("value")).toEqual(1); (values1?.get("onChange") as () => void)(); const values2 = $propValuesByInstanceSelector .get() - .get(JSON.stringify(["box"])); + .get(getInstanceKey(["box"])); expect(values2?.get("value")).toEqual(2); cleanStores($propValuesByInstanceSelector); @@ -255,7 +262,7 @@ test("resolve asset prop values", () => { ]) ); expect( - $propValuesByInstanceSelector.get().get(JSON.stringify(["box"])) + $propValuesByInstanceSelector.get().get(getInstanceKey(["box"])) ).toEqual( new Map([ ["$webstudio$canvasOnly$assetId", "assetId"], @@ -282,7 +289,7 @@ test("resolve page prop values", () => { ]) ); expect( - $propValuesByInstanceSelector.get().get(JSON.stringify(["box"])) + $propValuesByInstanceSelector.get().get(getInstanceKey(["box"])) ).toEqual(new Map([["myPage", "/"]])); cleanStores($propValuesByInstanceSelector); @@ -344,19 +351,19 @@ test("compute expression from collection items", () => { expect($propValuesByInstanceSelector.get()).toEqual( new Map([ [ - JSON.stringify(["list"]), + getInstanceKey(["list"]), new Map([["data", ["orange", "apple", "banana"]]]), ], [ - JSON.stringify(["item", "list[0]", "list"]), + getInstanceKey(["item", "list[0]", "list"]), new Map([["ariaLabel", "orange"]]), ], [ - JSON.stringify(["item", "list[1]", "list"]), + getInstanceKey(["item", "list[1]", "list"]), new Map([["ariaLabel", "apple"]]), ], [ - JSON.stringify(["item", "list[2]", "list"]), + getInstanceKey(["item", "list[2]", "list"]), new Map([["ariaLabel", "banana"]]), ], ]) @@ -402,7 +409,7 @@ test("access parameter value from variables values", () => { expect($propValuesByInstanceSelector.get()).toEqual( new Map([ [ - JSON.stringify(["body"]), + getInstanceKey(["body"]), new Map([["param", "paramValue"]]), ], ]) @@ -442,7 +449,7 @@ test("compute props bound to resource variables", () => { expect($propValuesByInstanceSelector.get()).toEqual( new Map([ [ - JSON.stringify(["body"]), + getInstanceKey(["body"]), new Map([["resource", "my-value"]]), ], ]) @@ -489,14 +496,14 @@ test("compute instance text content when plain text", () => { selectPageRoot("body"); expect($propValuesByInstanceSelector.get()).toEqual( new Map([ - [JSON.stringify(["body"]), new Map()], + [getInstanceKey(["body"]), new Map()], [ - JSON.stringify(["plainBox", "body"]), + getInstanceKey(["plainBox", "body"]), new Map([[textContentAttribute, "plain"]]), ], - [JSON.stringify(["richBox", "body"]), new Map()], + [getInstanceKey(["richBox", "body"]), new Map()], [ - JSON.stringify(["bold", "richBox", "body"]), + getInstanceKey(["bold", "richBox", "body"]), new Map([[textContentAttribute, "bold"]]), ], ]) @@ -538,9 +545,9 @@ test("compute instance text content bound to expression", () => { selectPageRoot("body"); expect($propValuesByInstanceSelector.get()).toEqual( new Map([ - [JSON.stringify(["body"]), new Map()], + [getInstanceKey(["body"]), new Map()], [ - JSON.stringify(["expressionBox", "body"]), + getInstanceKey(["expressionBox", "body"]), new Map([[textContentAttribute, "Hello world"]]), ], ]) @@ -549,51 +556,30 @@ test("compute instance text content bound to expression", () => { cleanStores($propValuesByInstanceSelector); }); -test("use default system values in props", () => { - $instances.set( - toMap([ - { - id: "body", - type: "instance", - component: "Body", - children: [], - }, - ]) - ); - $dataSources.set( - toMap([ - { - id: "systemId", - scopeInstanceId: "body", - name: "system", - type: "parameter", - }, - ]) - ); - $props.set( - toMap([ - { - id: "1", - instanceId: "body", - name: "data-origin", - type: "expression", - value: `$ws$dataSource$systemId.origin`, - }, - ]) +test("use page system values in props", () => { + const systemParameter = new Parameter("system"); + const data = renderData( + <$.Body + ws:id="bodyId" + data-origin={expression`${systemParameter}.origin`} + > ); - selectPageRoot("body"); + expect(data.dataSources.size).toEqual(1); + const [systemParameterId] = data.dataSources.keys(); + $instances.set(data.instances); + $dataSources.set(data.dataSources); + $props.set(data.props); + selectPageRoot("bodyId", systemParameterId); expect($propValuesByInstanceSelector.get()).toEqual( new Map([ [ - JSON.stringify(["body"]), + getInstanceKey(["bodyId"]), new Map([ ["data-origin", "https://undefined.wstd.work"], ]), ], ]) ); - - cleanStores($propValuesByInstanceSelector); }); test("compute props with global variables", () => { @@ -612,16 +598,37 @@ test("compute props with global variables", () => { selectPageRoot("bodyId"); expect($propValuesByInstanceSelector.get()).toEqual( new Map([ - [JSON.stringify(["bodyId"]), new Map()], + [getInstanceKey(["bodyId"]), new Map()], [ - JSON.stringify(["boxId", "bodyId"]), + getInstanceKey(["boxId", "bodyId"]), new Map([["data-value", "root value"]]), ], ]) ); }); -test("compute variable values for root", () => { +test("use global system values in props", () => { + const data = renderData( + <$.Body ws:id="bodyId" data-origin={expression`$ws$system.origin`}> + ); + expect(data.dataSources.size).toEqual(0); + $instances.set(data.instances); + $dataSources.set(data.dataSources); + $props.set(data.props); + selectPageRoot("bodyId"); + expect($propValuesByInstanceSelector.get()).toEqual( + new Map([ + [ + getInstanceKey(["bodyId"]), + new Map([ + ["data-origin", "https://undefined.wstd.work"], + ]), + ], + ]) + ); +}); + +test("compute variable values for page root", () => { const bodyVariable = new Variable("bodyVariable", "initial"); const data = renderData( <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> @@ -632,18 +639,19 @@ test("compute variable values for root", () => { const [dataSourceId] = data.dataSources.keys(); selectPageRoot("bodyId"); $dataSourceVariables.set(new Map([[dataSourceId, "success"]])); - expect($variableValuesByInstanceSelector.get()).toEqual( - new Map([ - [JSON.stringify([ROOT_INSTANCE_ID]), new Map()], - [ - JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), - new Map([[dataSourceId, "success"]]), - ], + expect( + $variableValuesByInstanceSelector + .get() + .get(getInstanceKey(["bodyId", ROOT_INSTANCE_ID])) + ).toEqual( + new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], + [dataSourceId, "success"], ]) ); }); -test("nest variable values from root to current instance", () => { +test("nest variable values from global root to current instance", () => { const bodyVariable = new Variable("bodyVariable", ""); const boxVariable = new Variable("boxVariable", ""); const textVariable = new Variable("textVariable", ""); @@ -668,21 +676,29 @@ test("nest variable values from root to current instance", () => { ); expect($variableValuesByInstanceSelector.get()).toEqual( new Map([ - [JSON.stringify([ROOT_INSTANCE_ID]), new Map()], [ - JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), - new Map([[bodyVariableId, "bodyValue"]]), + getInstanceKey([ROOT_INSTANCE_ID]), + new Map([[SYSTEM_VARIABLE_ID, initialSystem]]), + ], + [ + getInstanceKey(["bodyId", ROOT_INSTANCE_ID]), + new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], + [bodyVariableId, "bodyValue"], + ]), ], [ - JSON.stringify(["boxId", "bodyId", ROOT_INSTANCE_ID]), + getInstanceKey(["boxId", "bodyId", ROOT_INSTANCE_ID]), new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], [bodyVariableId, "bodyValue"], [boxVariableId, "boxValue"], ]), ], [ - JSON.stringify(["textId", "bodyId", ROOT_INSTANCE_ID]), + getInstanceKey(["textId", "bodyId", ROOT_INSTANCE_ID]), new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], [bodyVariableId, "bodyValue"], [textVariableId, "textValue"], ]), @@ -712,60 +728,49 @@ test("compute item values for collection", () => { $instances.set(data.instances); $dataSources.set(data.dataSources); $props.set(data.props); - const [dataVariableId, itemParameterId] = data.dataSources.keys(); + const [_dataVariableId, itemParameterId] = data.dataSources.keys(); selectPageRoot("bodyId"); $dataSourceVariables.set(new Map([])); - expect($variableValuesByInstanceSelector.get()).toEqual( - new Map([ - [JSON.stringify([ROOT_INSTANCE_ID]), new Map()], - [JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), new Map()], - [ - JSON.stringify([ + const values = $variableValuesByInstanceSelector.get(); + expect( + values + .get( + getInstanceKey([ "boxId", "collectionId[0]", "collectionId", "bodyId", ROOT_INSTANCE_ID, - ]), - new Map([ - [dataVariableId, ["apple", "banana", "orange"]], - [itemParameterId, "apple"], - ]), - ], - [ - JSON.stringify([ + ]) + ) + ?.get(itemParameterId) + ).toEqual("apple"); + expect( + values + .get( + getInstanceKey([ "boxId", "collectionId[1]", "collectionId", "bodyId", ROOT_INSTANCE_ID, - ]), - new Map([ - [dataVariableId, ["apple", "banana", "orange"]], - [itemParameterId, "banana"], - ]), - ], - [ - JSON.stringify([ + ]) + ) + ?.get(itemParameterId) + ).toEqual("banana"); + expect( + values + .get( + getInstanceKey([ "boxId", "collectionId[2]", "collectionId", "bodyId", ROOT_INSTANCE_ID, - ]), - new Map([ - [dataVariableId, ["apple", "banana", "orange"]], - [itemParameterId, "orange"], - ]), - ], - [ - JSON.stringify(["collectionId", "bodyId", ROOT_INSTANCE_ID]), - new Map([ - [dataVariableId, ["apple", "banana", "orange"]], - ]), - ], - ]) - ); + ]) + ) + ?.get(itemParameterId) + ).toEqual("orange"); }); test("compute resource variable values", () => { @@ -784,15 +789,12 @@ test("compute resource variable values", () => { const [resourceId] = data.resources.keys(); selectPageRoot("bodyId"); $resourceValues.set(new Map([[resourceId, "my-value"]])); - expect($variableValuesByInstanceSelector.get()).toEqual( - new Map([ - [JSON.stringify([ROOT_INSTANCE_ID]), new Map()], - [ - JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), - new Map([[resourceVariableId, "my-value"]]), - ], - ]) - ); + expect( + $variableValuesByInstanceSelector + .get() + .get(getInstanceKey(["bodyId", ROOT_INSTANCE_ID])) + ?.get(resourceVariableId) + ).toEqual("my-value"); }); test("stop variables lookup outside of slots", () => { @@ -811,39 +813,28 @@ test("stop variables lookup outside of slots", () => { $instances.set(data.instances); $dataSources.set(data.dataSources); $props.set(data.props); - const [bodyVariableId, slotVariableId, boxVariableId] = - data.dataSources.keys(); selectPageRoot("bodyId"); - expect($variableValuesByInstanceSelector.get()).toEqual( - new Map([ - [JSON.stringify([ROOT_INSTANCE_ID]), new Map()], - [ - JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), - new Map([[bodyVariableId, "body"]]), - ], - [ - JSON.stringify(["slotId", "bodyId", ROOT_INSTANCE_ID]), - new Map([ - [bodyVariableId, "body"], - [slotVariableId, "slot"], - ]), - ], - [ - JSON.stringify(["fragmentId", "slotId", "bodyId", ROOT_INSTANCE_ID]), - new Map(), - ], - [ - JSON.stringify([ - "boxId", - "fragmentId", - "slotId", - "bodyId", - ROOT_INSTANCE_ID, - ]), - new Map([[boxVariableId, "box"]]), - ], - ]) - ); + const values = $variableValuesByInstanceSelector.get(); + expect( + values.get(getInstanceKey(["slotId", "bodyId", ROOT_INSTANCE_ID]))?.size + ).toEqual(3); + expect( + values.get( + getInstanceKey(["fragmentId", "slotId", "bodyId", ROOT_INSTANCE_ID]) + )?.size + ).toEqual(1); + expect( + values.get( + getInstanceKey([ + "boxId", + "fragmentId", + "slotId", + "bodyId", + ROOT_INSTANCE_ID, + ]) + )?.size + // global system and box variable + ).toEqual(2); }); test("compute parameter and resource variables without values to make it available in scope", () => { @@ -864,21 +855,14 @@ test("compute parameter and resource variables without values to make it availab $props.set(data.props); const [resourceVariableId, parameterVariableId] = data.dataSources.keys(); selectPageRoot("bodyId"); - expect($variableValuesByInstanceSelector.get()).toEqual( - new Map([ - [JSON.stringify([ROOT_INSTANCE_ID]), new Map()], - [ - JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), - new Map([ - [resourceVariableId, undefined], - [parameterVariableId, undefined], - ]), - ], - ]) - ); + const values = $variableValuesByInstanceSelector + .get() + .get(getInstanceKey(["bodyId", ROOT_INSTANCE_ID])); + expect(values?.get(resourceVariableId)).toEqual(undefined); + expect(values?.get(parameterVariableId)).toEqual(undefined); }); -test("prefill default system variable value", () => { +test("provide page system variable value", () => { const system = new Parameter("system"); const data = renderData( <$.Body ws:id="bodyId" vars={expression`${system}`}> @@ -888,38 +872,65 @@ test("prefill default system variable value", () => { $props.set(data.props); const [systemId] = data.dataSources.keys(); selectPageRoot("bodyId", systemId); + expect( + $variableValuesByInstanceSelector + .get() + .get(getInstanceKey(["bodyId", ROOT_INSTANCE_ID])) + ?.get(systemId) + ).toEqual(initialSystem); + updateCurrentSystem({ + params: { slug: "my-post" }, + }); + expect( + $variableValuesByInstanceSelector + .get() + .get(getInstanceKey(["bodyId", ROOT_INSTANCE_ID])) + ?.get(systemId) + ).toEqual({ + params: { slug: "my-post" }, + search: {}, + origin: "https://undefined.wstd.work", + }); +}); + +test("provide global system variable value", () => { + const data = renderData( + <$.Body ws:id="bodyId" vars={expression`$ws$system`}> + ); + $instances.set(data.instances); + $dataSources.set(data.dataSources); + $props.set(data.props); + selectPageRoot("bodyId"); + $systemDataByPage.set(new Map()); expect($variableValuesByInstanceSelector.get()).toEqual( new Map([ - [JSON.stringify([ROOT_INSTANCE_ID]), new Map()], [ - JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), - new Map([ - [ - systemId, - { params: {}, search: {}, origin: "https://undefined.wstd.work" }, - ], - ]), + getInstanceKey([ROOT_INSTANCE_ID]), + new Map([[SYSTEM_VARIABLE_ID, initialSystem]]), + ], + [ + getInstanceKey(["bodyId", ROOT_INSTANCE_ID]), + new Map([[SYSTEM_VARIABLE_ID, initialSystem]]), ], ]) ); updateCurrentSystem({ params: { slug: "my-post" }, }); + const updatedSystem = { + params: { slug: "my-post" }, + search: {}, + origin: "https://undefined.wstd.work", + }; expect($variableValuesByInstanceSelector.get()).toEqual( new Map([ - [JSON.stringify([ROOT_INSTANCE_ID]), new Map()], [ - JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), - new Map([ - [ - systemId, - { - params: { slug: "my-post" }, - search: {}, - origin: "https://undefined.wstd.work", - }, - ], - ]), + getInstanceKey([ROOT_INSTANCE_ID]), + new Map([[SYSTEM_VARIABLE_ID, updatedSystem]]), + ], + [ + getInstanceKey(["bodyId", ROOT_INSTANCE_ID]), + new Map([[SYSTEM_VARIABLE_ID, updatedSystem]]), ], ]) ); @@ -938,16 +949,26 @@ test("mask variables with the same name in nested scope", () => { $props.set(data.props); const [bodyVariableId, boxVariableId] = data.dataSources.keys(); selectPageRoot("bodyId"); + $systemDataByPage.set(new Map()); expect($variableValuesByInstanceSelector.get()).toEqual( new Map([ - [JSON.stringify([ROOT_INSTANCE_ID]), new Map()], [ - JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), - new Map([[bodyVariableId, "body"]]), + getInstanceKey([ROOT_INSTANCE_ID]), + new Map([[SYSTEM_VARIABLE_ID, initialSystem]]), ], [ - JSON.stringify(["boxId", "bodyId", ROOT_INSTANCE_ID]), - new Map([[boxVariableId, "box"]]), + getInstanceKey(["bodyId", ROOT_INSTANCE_ID]), + new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], + [bodyVariableId, "body"], + ]), + ], + [ + getInstanceKey(["boxId", "bodyId", ROOT_INSTANCE_ID]), + new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], + [boxVariableId, "box"], + ]), ], ]) ); @@ -972,16 +993,23 @@ test("inherit variables from global root", () => { expect($variableValuesByInstanceSelector.get()).toEqual( new Map([ [ - JSON.stringify([ROOT_INSTANCE_ID]), - new Map([[rootVariableId, "root"]]), + getInstanceKey([ROOT_INSTANCE_ID]), + new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], + [rootVariableId, "root"], + ]), ], [ - JSON.stringify(["bodyId", ROOT_INSTANCE_ID]), - new Map([[rootVariableId, "root"]]), + getInstanceKey(["bodyId", ROOT_INSTANCE_ID]), + new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], + [rootVariableId, "root"], + ]), ], [ - JSON.stringify(["boxId", "bodyId", ROOT_INSTANCE_ID]), + getInstanceKey(["boxId", "bodyId", ROOT_INSTANCE_ID]), new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], [rootVariableId, "root"], [boxVariableId, "box"], ]), @@ -1026,6 +1054,7 @@ test("inherit variables from global root inside slots", () => { ) ).toEqual( new Map([ + [SYSTEM_VARIABLE_ID, initialSystem], [rootVariableId, "root"], [boxVariableId, "box"], ]) diff --git a/apps/builder/app/shared/nano-states/props.ts b/apps/builder/app/shared/nano-states/props.ts index a7dbff763b4f..03f812d3dfa5 100644 --- a/apps/builder/app/shared/nano-states/props.ts +++ b/apps/builder/app/shared/nano-states/props.ts @@ -14,6 +14,7 @@ import { collectionComponent, portalComponent, ROOT_INSTANCE_ID, + SYSTEM_VARIABLE_ID, } from "@webstudio-is/sdk"; import { normalizeProps, textContentAttribute } from "@webstudio-is/react-sdk"; import { mapGroupBy } from "~/shared/shim"; @@ -135,6 +136,8 @@ const $unscopedVariableValues = computed( ], (dataSources, dataSourceVariables, resourceValues, page, system) => { const values = new Map(); + // support global system + values.set(SYSTEM_VARIABLE_ID, system); for (const [dataSourceId, dataSource] of dataSources) { if (dataSource.type === "variable") { values.set( @@ -144,7 +147,7 @@ const $unscopedVariableValues = computed( } if (dataSource.type === "parameter") { let value = dataSourceVariables.get(dataSourceId); - // @todo support global system + // support page system if (dataSource.id === page?.systemDataSourceId) { value = system; } @@ -420,6 +423,11 @@ export const $variableValuesByInstanceSelector = computed( variableValues ); const variables = variablesByInstanceId.get(instanceId); + // set global system value + if (instanceId === ROOT_INSTANCE_ID) { + variableNames.set("system", SYSTEM_VARIABLE_ID); + variableValues.set(SYSTEM_VARIABLE_ID, system); + } if (variables) { for (const variable of variables) { // delete previous variable with the same name @@ -433,6 +441,7 @@ export const $variableValuesByInstanceSelector = computed( if (variable.type === "parameter") { const value = dataSourceVariables.get(variable.id); variableValues.set(variable.id, value); + // set page system value if (variable.id === page.systemDataSourceId) { variableValues.set(variable.id, system); } diff --git a/apps/builder/app/shared/page-utils.test.tsx b/apps/builder/app/shared/page-utils.test.tsx index 207ec1963a42..db79ff59fee2 100644 --- a/apps/builder/app/shared/page-utils.test.tsx +++ b/apps/builder/app/shared/page-utils.test.tsx @@ -3,9 +3,7 @@ import type { Project } from "@webstudio-is/project"; import { ROOT_FOLDER_ID, ROOT_INSTANCE_ID, - encodeDataSourceVariable, encodeDataVariableId, - type DataSource, type Instance, type WebstudioData, } from "@webstudio-is/sdk"; @@ -18,6 +16,7 @@ import { insertPageCopyMutable } from "./page-utils"; import { $, expression, + Parameter, renderData, Variable, ws, @@ -30,7 +29,7 @@ const toMap = (list: T[]) => const getWebstudioDataStub = ( data?: Partial ): WebstudioData => ({ - pages: createDefaultPages({ rootInstanceId: "", systemDataSourceId: "" }), + pages: createDefaultPages({ rootInstanceId: "" }), assets: new Map(), dataSources: new Map(), resources: new Map(), @@ -51,14 +50,6 @@ describe("insert page copy", () => { instances: toMap([ { type: "instance", id: "bodyId", component: "Body", children: [] }, ]), - dataSources: toMap([ - { - id: "systemId", - scopeInstanceId: "bodyId", - name: "system", - type: "parameter", - }, - ]), pages: { meta: {}, homePage: { @@ -68,7 +59,6 @@ describe("insert page copy", () => { title: `"Title"`, meta: {}, rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, pages: [], folders: [createRootFolder(["pageId"])], @@ -87,14 +77,6 @@ describe("insert page copy", () => { title: `"Title"`, meta: {}, rootInstanceId: expect.not.stringMatching("bodyId"), - systemDataSourceId: expect.not.stringMatching("systemId"), - }); - expect(data.dataSources.size).toEqual(2); - expect(Array.from(data.dataSources.values())[1]).toEqual({ - id: newPage.systemDataSourceId, - scopeInstanceId: newPage.rootInstanceId, - name: "system", - type: "parameter", }); expect(data.instances.size).toEqual(2); expect(Array.from(data.instances.values())[1]).toEqual({ @@ -110,14 +92,6 @@ describe("insert page copy", () => { instances: toMap([ { type: "instance", id: "bodyId", component: "Body", children: [] }, ]), - dataSources: toMap([ - { - id: "systemId", - scopeInstanceId: "bodyId", - name: "system", - type: "parameter", - }, - ]), pages: { meta: {}, homePage: { @@ -127,7 +101,6 @@ describe("insert page copy", () => { title: `"Home"`, meta: {}, rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, pages: [ { @@ -137,7 +110,6 @@ describe("insert page copy", () => { title: `"Title"`, meta: {}, rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, ], folders: [createRootFolder(["homePageId", "pageId"])], @@ -157,14 +129,6 @@ describe("insert page copy", () => { instances: toMap([ { type: "instance", id: "bodyId", component: "Body", children: [] }, ]), - dataSources: toMap([ - { - id: "systemId", - scopeInstanceId: "bodyId", - name: "system", - type: "parameter", - }, - ]), pages: { meta: {}, homePage: { @@ -174,7 +138,6 @@ describe("insert page copy", () => { title: `"Home"`, meta: {}, rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, pages: [ { @@ -185,7 +148,6 @@ describe("insert page copy", () => { title: `"My Title"`, meta: {}, rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, { id: "page2Id", @@ -195,7 +157,6 @@ describe("insert page copy", () => { title: `"My Title"`, meta: {}, rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, ], folders: [createRootFolder(["homePageId", "page1Id", "page2Id"])], @@ -221,14 +182,6 @@ describe("insert page copy", () => { instances: toMap([ { type: "instance", id: "bodyId", component: "Body", children: [] }, ]), - dataSources: toMap([ - { - id: "systemId", - scopeInstanceId: "bodyId", - name: "system", - type: "parameter", - }, - ]), pages: { meta: {}, homePage: { @@ -238,7 +191,6 @@ describe("insert page copy", () => { title: `"Home"`, meta: {}, rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, pages: [ { @@ -248,7 +200,6 @@ describe("insert page copy", () => { title: `"My Title"`, meta: {}, rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, ], folders: [ @@ -278,41 +229,36 @@ describe("insert page copy", () => { }); test("replace variables in page copy meta", () => { + const bodyVariable = new Variable("bodyVariable", ""); + const dataWithoutPage = renderData( + <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> + ); + const [variableId] = dataWithoutPage.dataSources.keys(); + const variableIdentifier = encodeDataVariableId(variableId); const data = getWebstudioDataStub({ - instances: toMap([ - { type: "instance", id: "bodyId", component: "Body", children: [] }, - ]), - dataSources: toMap([ - { - id: "systemId", - scopeInstanceId: "bodyId", - name: "system", - type: "parameter", - }, - ]), + ...dataWithoutPage, pages: { meta: {}, homePage: { id: "pageId", + rootInstanceId: "bodyId", name: "Name", path: "", - title: `"Title: " + $ws$dataSource$systemId.params.value`, + title: `"Title: " + ${variableIdentifier}`, meta: { - description: `"Description: " + $ws$dataSource$systemId.params.value`, - excludePageFromSearch: `"Exclude: " + $ws$dataSource$systemId.params.value`, - socialImageUrl: `"Image: " + $ws$dataSource$systemId.params.value`, - language: `"Language: " + $ws$dataSource$systemId.params.value`, - status: `"Status: " + $ws$dataSource$systemId.params.value`, - redirect: `"Redirect: " + $ws$dataSource$systemId.params.value`, + description: `"Description: " + ${variableIdentifier}`, + excludePageFromSearch: `"Exclude: " + ${variableIdentifier}`, + socialImageUrl: `"Image: " + ${variableIdentifier}`, + language: `"Language: " + ${variableIdentifier}`, + status: `"Status: " + ${variableIdentifier}`, + redirect: `"Redirect: " + ${variableIdentifier}`, custom: [ { property: "Property", - content: `"Value: " + $ws$dataSource$systemId.params.value`, + content: `"Value: " + ${variableIdentifier}`, }, ], }, - rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, pages: [], folders: [createRootFolder(["pageId"])], @@ -324,30 +270,28 @@ describe("insert page copy", () => { }); expect(data.pages.pages.length).toEqual(1); const newPage = data.pages.pages[0]; - const newSystem = Array.from(data.dataSources.values())[1]; - expect(newSystem.id).not.toEqual("systemId"); - const newSystemName = encodeDataSourceVariable(newSystem.id); + const [_oldVariableId, newVariableId] = data.dataSources.keys(); + const newVariableIdentifier = encodeDataVariableId(newVariableId); expect(newPage).toEqual({ id: expect.not.stringMatching("pageId"), name: "Name (1)", path: "/copy-1", - title: `"Title: " + ${newSystemName}.params.value`, + title: `"Title: " + ${newVariableIdentifier}`, meta: { - description: `"Description: " + ${newSystemName}.params.value`, - excludePageFromSearch: `"Exclude: " + ${newSystemName}.params.value`, - socialImageUrl: `"Image: " + ${newSystemName}.params.value`, - language: `"Language: " + ${newSystemName}.params.value`, - status: `"Status: " + ${newSystemName}.params.value`, - redirect: `"Redirect: " + ${newSystemName}.params.value`, + description: `"Description: " + ${newVariableIdentifier}`, + excludePageFromSearch: `"Exclude: " + ${newVariableIdentifier}`, + socialImageUrl: `"Image: " + ${newVariableIdentifier}`, + language: `"Language: " + ${newVariableIdentifier}`, + status: `"Status: " + ${newVariableIdentifier}`, + redirect: `"Redirect: " + ${newVariableIdentifier}`, custom: [ { property: "Property", - content: `"Value: " + ${newSystemName}.params.value`, + content: `"Value: " + ${newVariableIdentifier}`, }, ], }, rootInstanceId: expect.not.stringMatching("bodyId"), - systemDataSourceId: expect.not.stringMatching("systemId"), }); }); @@ -356,14 +300,6 @@ describe("insert page copy", () => { instances: toMap([ { type: "instance", id: "bodyId", component: "Body", children: [] }, ]), - dataSources: toMap([ - { - id: "systemId", - scopeInstanceId: "bodyId", - name: "system", - type: "parameter", - }, - ]), pages: { meta: {}, homePage: { @@ -373,7 +309,6 @@ describe("insert page copy", () => { title: `"Title"`, meta: {}, rootInstanceId: "bodyId", - systemDataSourceId: "systemId", }, pages: [], folders: [ @@ -417,7 +352,6 @@ describe("insert page copy", () => { const data = { pages: createDefaultPages({ rootInstanceId: "bodyId", - systemDataSourceId: "", }), ...renderData( @@ -451,7 +385,6 @@ describe("insert page copy", () => { const sourceData = { pages: createDefaultPages({ rootInstanceId: "bodyId", - systemDataSourceId: "", }), ...renderData( @@ -467,7 +400,6 @@ describe("insert page copy", () => { const targetData = { pages: createDefaultPages({ rootInstanceId: "anotherBodyId", - systemDataSourceId: "", }), ...renderData( @@ -500,7 +432,6 @@ describe("insert page copy", () => { const sourceData = { pages: createDefaultPages({ rootInstanceId: "bodyId", - systemDataSourceId: "", }), ...renderData( @@ -516,7 +447,6 @@ describe("insert page copy", () => { const targetData = { pages: createDefaultPages({ rootInstanceId: "anotherBodyId", - systemDataSourceId: "", }), // generate different ids in source and data projects ...renderData(<$.Body ws:id="anotherBodyId">, nanoid), @@ -537,4 +467,40 @@ describe("insert page copy", () => { { type: "expression", value: encodeDataVariableId(globalVariableId) }, ]); }); + + test("delete page system in favor of global one", () => { + const pageSystemVariable = new Parameter("system"); + const dataWithoutPages = renderData( + <$.Body ws:id="bodyId" vars={expression`${pageSystemVariable}`}> + <$.Box ws:id="boxId">{expression`${pageSystemVariable}`} + + ); + const [pageSystemVariableId] = dataWithoutPages.dataSources.keys(); + const data = { + pages: createDefaultPages({ + rootInstanceId: "bodyId", + systemDataSourceId: pageSystemVariableId, + }), + ...dataWithoutPages, + }; + data.pages.homePage.title = `${encodeDataVariableId(pageSystemVariableId)}`; + data.pages.homePage.meta.description = `${encodeDataVariableId(pageSystemVariableId)}`; + insertPageCopyMutable({ + source: { data, pageId: data.pages.homePage.id }, + target: { data, folderId: ROOT_FOLDER_ID }, + }); + expect(data.dataSources.size).toEqual(1); + expect(Array.from(data.instances.values())).toEqual([ + expect.objectContaining({ component: "Body", id: "bodyId" }), + expect.objectContaining({ component: "Box", id: "boxId" }), + expect.objectContaining({ component: "Body" }), + expect.objectContaining({ component: "Box" }), + ]); + const newBox = Array.from(data.instances.values()).at(-1); + expect(newBox?.children).toEqual([ + { type: "expression", value: "$ws$system" }, + ]); + expect(data.pages.pages[0].title).toEqual(`$ws$system`); + expect(data.pages.pages[0].meta.description).toEqual(`$ws$system`); + }); }); diff --git a/apps/builder/app/shared/page-utils.ts b/apps/builder/app/shared/page-utils.ts index 2095c1c1e866..142f88be58ed 100644 --- a/apps/builder/app/shared/page-utils.ts +++ b/apps/builder/app/shared/page-utils.ts @@ -15,8 +15,13 @@ import { import { extractWebstudioFragment, insertWebstudioFragmentCopy, + unwrap, } from "./instance-utils"; -import { findAvailableVariables } from "./data-variables"; +import { + findAvailableVariables, + restoreExpressionVariables, + unsetExpressionVariables, +} from "./data-variables"; const deduplicateName = ( pages: Pages, @@ -105,69 +110,78 @@ export const insertPageCopyMutable = ({ startingInstanceId: ROOT_INSTANCE_ID, }), }); + const unsetVariables = new Set(); + const unsetNameById = new Map(); + // replace legacy page system with global variable + if (page.systemDataSourceId) { + unsetVariables.add(page.systemDataSourceId); + unsetNameById.set(page.systemDataSourceId, "system"); + } + const availableVariables = findAvailableVariables({ + ...target.data, + startingInstanceId: ROOT_INSTANCE_ID, + }); + const maskedIdByName = new Map(); + for (const dataSource of availableVariables) { + maskedIdByName.set(dataSource.name, dataSource.id); + } // copy paste page body const { newInstanceIds, newDataSourceIds } = insertWebstudioFragmentCopy({ data: target.data, - fragment: extractWebstudioFragment(source.data, page.rootInstanceId), - availableVariables: findAvailableVariables({ - ...target.data, - startingInstanceId: ROOT_INSTANCE_ID, + fragment: extractWebstudioFragment(source.data, page.rootInstanceId, { + unsetVariables, }), + availableVariables, }); - const newPageId = nanoid(); - const newRootInstanceId = + // unwrap page draft + const newPage = structuredClone(unwrap(page)); + newPage.id = nanoid(); + delete newPage.systemDataSourceId; + newPage.rootInstanceId = newInstanceIds.get(page.rootInstanceId) ?? page.rootInstanceId; - const newSystemDataSourceId = - newDataSourceIds.get(page.systemDataSourceId ?? "") ?? - page.systemDataSourceId; - const newPage: Page = { - ...page, - id: newPageId, - rootInstanceId: newRootInstanceId, - systemDataSourceId: newSystemDataSourceId, - name: deduplicateName(target.data.pages, target.folderId, page.name), - path: deduplicatePath(target.data.pages, target.folderId, page.path), - title: replaceDataSources(page.title, newDataSourceIds), - meta: { - ...page.meta, - description: - page.meta.description === undefined - ? undefined - : replaceDataSources(page.meta.description, newDataSourceIds), - excludePageFromSearch: - page.meta.excludePageFromSearch === undefined - ? undefined - : replaceDataSources( - page.meta.excludePageFromSearch, - newDataSourceIds - ), - socialImageUrl: - page.meta.socialImageUrl === undefined - ? undefined - : replaceDataSources(page.meta.socialImageUrl, newDataSourceIds), - language: - page.meta.language === undefined - ? undefined - : replaceDataSources(page.meta.language, newDataSourceIds), - status: - page.meta.status === undefined - ? undefined - : replaceDataSources(page.meta.status, newDataSourceIds), - redirect: - page.meta.redirect === undefined - ? undefined - : replaceDataSources(page.meta.redirect, newDataSourceIds), - custom: page.meta.custom?.map(({ property, content }) => ({ - property, - content: replaceDataSources(content, newDataSourceIds), - })), - }, + newPage.name = deduplicateName(target.data.pages, target.folderId, page.name); + newPage.path = deduplicatePath(target.data.pages, target.folderId, page.path); + const transformExpression = (expression: string) => { + // rebind expressions with from page system variable to global one + expression = unsetExpressionVariables({ expression, unsetNameById }); + expression = restoreExpressionVariables({ expression, maskedIdByName }); + expression = replaceDataSources(expression, newDataSourceIds); + return expression; }; + newPage.title = transformExpression(newPage.title); + if (newPage.meta.description !== undefined) { + newPage.meta.description = transformExpression(newPage.meta.description); + } + if (newPage.meta.excludePageFromSearch !== undefined) { + newPage.meta.excludePageFromSearch = transformExpression( + newPage.meta.excludePageFromSearch + ); + } + if (newPage.meta.socialImageUrl !== undefined) { + newPage.meta.socialImageUrl = transformExpression( + newPage.meta.socialImageUrl + ); + } + if (newPage.meta.language !== undefined) { + newPage.meta.language = transformExpression(newPage.meta.language); + } + if (newPage.meta.status !== undefined) { + newPage.meta.status = transformExpression(newPage.meta.status); + } + if (newPage.meta.redirect !== undefined) { + newPage.meta.redirect = transformExpression(newPage.meta.redirect); + } + if (newPage.meta.custom) { + newPage.meta.custom = newPage.meta.custom.map(({ property, content }) => ({ + property, + content: transformExpression(content), + })); + } target.data.pages.pages.push(newPage); for (const folder of target.data.pages.folders) { if (folder.id === target.folderId) { - folder.children.push(newPageId); + folder.children.push(newPage.id); } } - return newPageId; + return newPage.id; }; diff --git a/apps/builder/app/shared/webstudio-data-migrator.test.ts b/apps/builder/app/shared/webstudio-data-migrator.test.ts index e71ac6fb4886..b604efbc2251 100644 --- a/apps/builder/app/shared/webstudio-data-migrator.test.ts +++ b/apps/builder/app/shared/webstudio-data-migrator.test.ts @@ -7,7 +7,6 @@ import { migrateWebstudioDataMutable } from "./webstudio-data-migrator"; const emptyData: WebstudioData = { pages: createDefaultPages({ rootInstanceId: "rootInstanceId", - systemDataSourceId: "systemDataSourceId", }), assets: new Map(), dataSources: new Map(), diff --git a/apps/builder/package.json b/apps/builder/package.json index 03bb155c1c24..01ed76d8cbed 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -28,7 +28,7 @@ "@codemirror/language": "^6.10.8", "@codemirror/lint": "^6.8.4", "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.36.2", + "@codemirror/view": "^6.36.3", "@floating-ui/dom": "^1.6.13", "@fontsource-variable/inter": "^5.0.20", "@fontsource-variable/manrope": "^5.0.20", diff --git a/packages/project-build/src/db/build.ts b/packages/project-build/src/db/build.ts index b04a64119705..a6ef58c3ea8b 100644 --- a/packages/project-build/src/db/build.ts +++ b/packages/project-build/src/db/build.ts @@ -263,19 +263,7 @@ export const createBuild = async ( ): Promise => { const newInstances = createNewPageInstances(); const [rootInstanceId] = newInstances[0]; - const systemDataSource: DataSource = { - id: nanoid(), - scopeInstanceId: rootInstanceId, - name: "system", - type: "parameter", - }; - - const defaultPages = Pages.parse( - createDefaultPages({ - rootInstanceId, - systemDataSourceId: systemDataSource.id, - }) - ); + const defaultPages = createDefaultPages({ rootInstanceId }); const newBuild = await context.postgrest.client.from("Build").insert({ id: crypto.randomUUID(), @@ -283,9 +271,6 @@ export const createBuild = async ( pages: serializePages(defaultPages), breakpoints: serializeData(new Map(createInitialBreakpoints())), instances: serializeData(new Map(newInstances)), - dataSources: serializeData( - new Map([[systemDataSource.id, systemDataSource]]) - ), }); if (newBuild.error) { throw newBuild.error; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e6202e9e46d..531aa26c67ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,8 +155,8 @@ importers: specifier: ^6.5.2 version: 6.5.2 '@codemirror/view': - specifier: ^6.36.2 - version: 6.36.2 + specifier: ^6.36.3 + version: 6.36.3 '@floating-ui/dom': specifier: ^1.6.13 version: 1.6.13 @@ -2497,8 +2497,8 @@ packages: '@codemirror/state@6.5.2': resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} - '@codemirror/view@6.36.2': - resolution: {integrity: sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==} + '@codemirror/view@6.36.3': + resolution: {integrity: sha512-N2bilM47QWC8Hnx0rMdDxO2x2ImJ1FvZWXubwKgjeoOrWwEiFrtpA7SFHcuZ+o2Ze2VzbkgbzWVj4+V18LVkeg==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -9900,14 +9900,14 @@ snapshots: dependencies: '@codemirror/language': 6.10.8 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.2 + '@codemirror/view': 6.36.3 '@lezer/common': 1.2.3 '@codemirror/commands@6.8.0': dependencies: '@codemirror/language': 6.10.8 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.2 + '@codemirror/view': 6.36.3 '@lezer/common': 1.2.3 '@codemirror/lang-css@6.3.1': @@ -9925,7 +9925,7 @@ snapshots: '@codemirror/lang-javascript': 6.2.3 '@codemirror/language': 6.10.8 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.2 + '@codemirror/view': 6.36.3 '@lezer/common': 1.2.3 '@lezer/css': 1.1.10 '@lezer/html': 1.3.10 @@ -9936,7 +9936,7 @@ snapshots: '@codemirror/language': 6.10.8 '@codemirror/lint': 6.8.4 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.2 + '@codemirror/view': 6.36.3 '@lezer/common': 1.2.3 '@lezer/javascript': 1.4.19 @@ -9946,14 +9946,14 @@ snapshots: '@codemirror/lang-html': 6.4.9 '@codemirror/language': 6.10.8 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.2 + '@codemirror/view': 6.36.3 '@lezer/common': 1.2.3 '@lezer/markdown': 1.3.1 '@codemirror/language@6.10.8': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.2 + '@codemirror/view': 6.36.3 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 @@ -9962,14 +9962,14 @@ snapshots: '@codemirror/lint@6.8.4': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.2 + '@codemirror/view': 6.36.3 crelt: 1.0.6 '@codemirror/state@6.5.2': dependencies: '@marijn/find-cluster-break': 1.0.2 - '@codemirror/view@6.36.2': + '@codemirror/view@6.36.3': dependencies: '@codemirror/state': 6.5.2 style-mod: 4.1.2