From 0ab602cb6cdd4a6d462f2167e1de41769f0fc5df Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 5 Mar 2025 20:13:12 +0000 Subject: [PATCH 1/9] build: Release 05-03-2025 From 52f1d0b1193133b35fd1790b71a00fe35a61fa6c Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 5 Mar 2025 20:47:04 +0000 Subject: [PATCH 2/9] fix: When deleting property during search in advanced panel - remove it from results (#4950) ## Description 1. What is this PR about (link the issue and add a short description) ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file --- .../builder/shared/css-editor/css-editor.tsx | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/apps/builder/app/builder/shared/css-editor/css-editor.tsx b/apps/builder/app/builder/shared/css-editor/css-editor.tsx index 1900fa0e6885..77bf4fc60433 100644 --- a/apps/builder/app/builder/shared/css-editor/css-editor.tsx +++ b/apps/builder/app/builder/shared/css-editor/css-editor.tsx @@ -340,7 +340,7 @@ export const CssEditor = ({ }, })); - const advancedProperties = Array.from(styleMap.keys()) as Array; + const advancedProperties = Array.from(styleMap.keys()); const currentProperties = searchProperties ?? @@ -394,7 +394,7 @@ export const CssEditor = ({ keys: ["property", "value"], }).map(({ property }) => property); - setSearchProperties(matched as CssProperty[]); + setSearchProperties(matched); }; const afterChangingStyles = () => { @@ -407,6 +407,25 @@ export const CssEditor = ({ }); }; + const handleDeleteProperty: DeleteProperty = (property, options = {}) => { + onDeleteProperty(property, options); + if (options.isEphemeral === true) { + return; + } + setSearchProperties( + searchProperties?.filter((searchProperty) => searchProperty !== property) + ); + }; + + const handleDeleteAllDeclarations = (styleMap: CssStyleMap) => { + setSearchProperties( + searchProperties?.filter( + (searchProperty) => styleMap.has(searchProperty) === false + ) + ); + onDeleteAllDeclarations(styleMap); + }; + return ( <> {showSearch && ( @@ -420,8 +439,8 @@ export const CssEditor = ({ )} ); @@ -493,7 +512,7 @@ export const CssEditor = ({ From c7ecd5794b6aa334dbbd913720efecc24ecf5ed9 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 6 Mar 2025 10:36:33 +0000 Subject: [PATCH 3/9] fix: Fix token state selector (#4955) ## Description Select the state, see it stays selected ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file --- .../style-panel/style-source/style-source-control.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/builder/app/builder/features/style-panel/style-source/style-source-control.tsx b/apps/builder/app/builder/features/style-panel/style-source/style-source-control.tsx index c81226b8b0f9..f854eef89b49 100644 --- a/apps/builder/app/builder/features/style-panel/style-source/style-source-control.tsx +++ b/apps/builder/app/builder/features/style-panel/style-source/style-source-control.tsx @@ -215,6 +215,12 @@ const StyleSourceButton = styled("button", { }, false: {}, }, + bleed: { + true: { + position: "absolute", + inset: 0, + }, + }, }, }); @@ -318,7 +324,6 @@ export const StyleSourceControl = ({ disabled={disabled} aria-current={selected && state === undefined} role="button" - onClick={onSelect} hasError={error !== undefined} > @@ -326,6 +331,8 @@ export const StyleSourceControl = ({ disabled={disabled || isEditing} isEditing={isEditing} tabIndex={-1} + onClick={onSelect} + bleed={source === "local"} > {source === "local" ? ( From 355e0e4406bbad22bc6a71412d8ade1875e22b16 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 6 Mar 2025 11:32:31 +0000 Subject: [PATCH 4/9] fix: Refix bigger clickable surface for local style source (#4956) ## Description 1. What is this PR about (link the issue and add a short description) ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file --- .../style-source/style-source-control.tsx | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/style-source/style-source-control.tsx b/apps/builder/app/builder/features/style-panel/style-source/style-source-control.tsx index f854eef89b49..7bc5ce2770a1 100644 --- a/apps/builder/app/builder/features/style-panel/style-source/style-source-control.tsx +++ b/apps/builder/app/builder/features/style-panel/style-source/style-source-control.tsx @@ -215,12 +215,6 @@ const StyleSourceButton = styled("button", { }, false: {}, }, - bleed: { - true: { - position: "absolute", - inset: 0, - }, - }, }, }); @@ -332,25 +326,28 @@ export const StyleSourceControl = ({ isEditing={isEditing} tabIndex={-1} onClick={onSelect} - bleed={source === "local"} > - - {source === "local" ? ( + {source === "local" ? ( + + - ) : ( - <> - - {hasStyles === false && isEditing === false && ( - - )} - - )} - + + ) : ( + + + {hasStyles === false && isEditing === false && ( + + )} + + )} {stateLabel !== undefined && ( From c9748770a14c897d229a08a35000b34afb018098 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 7 Mar 2025 23:50:20 +0700 Subject: [PATCH 5/9] fix: handle empty xml (#4965) XML body did not have any children and the logic had runtime error. --- packages/cli/src/prebuild.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index df4679e1b999..7831edb9b2e7 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -497,7 +497,12 @@ export const prebuild = async (options: { if (documentType === "xml") { // treat first body child as root const bodyInstance = instances.get(rootInstanceId); - if (bodyInstance?.children?.[0].type === "id") { + // @todo test empty xml + if ( + bodyInstance && + bodyInstance.children.length > 0 && + bodyInstance.children[0].type === "id" + ) { rootInstanceId = bodyInstance.children[0].value; } // remove all unexpected components From 26cfc99da2689f614d81728ed709307565f127ea Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sat, 8 Mar 2025 01:21:12 +0700 Subject: [PATCH 6/9] fix: render empty page when xml is empty (#4966) --- packages/cli/src/prebuild.ts | 9 ++--- .../src/component-generator.test.tsx | 22 ++++++++++++ packages/react-sdk/src/component-generator.ts | 35 ++++++++++--------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index 7831edb9b2e7..0a51fd24c46c 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -498,12 +498,9 @@ export const prebuild = async (options: { // treat first body child as root const bodyInstance = instances.get(rootInstanceId); // @todo test empty xml - if ( - bodyInstance && - bodyInstance.children.length > 0 && - bodyInstance.children[0].type === "id" - ) { - rootInstanceId = bodyInstance.children[0].value; + const firstChild = bodyInstance?.children.at(0); + if (firstChild?.type === "id") { + rootInstanceId = firstChild.value; } // remove all unexpected components for (const instance of instances.values()) { diff --git a/packages/react-sdk/src/component-generator.test.tsx b/packages/react-sdk/src/component-generator.test.tsx index 21ed5a1e8c8c..dee11a410090 100644 --- a/packages/react-sdk/src/component-generator.test.tsx +++ b/packages/react-sdk/src/component-generator.test.tsx @@ -1250,3 +1250,25 @@ test("ignore ws:block-template when generate index attribute", () => { ) ); }); + +test("render empty component when no instances found", () => { + expect( + generateWebstudioComponent({ + classesMap: new Map(), + scope: createScope(), + name: "Page", + rootInstanceId: "", + parameters: [], + metas: new Map(), + ...renderData(<$.Body ws:id="bodyId">), + }) + ).toEqual( + validateJSX( + clear(` + const Page = () => { + return <> + } + `) + ) + ); +}); diff --git a/packages/react-sdk/src/component-generator.ts b/packages/react-sdk/src/component-generator.ts index 02003573b1c6..dfa91b73fb3c 100644 --- a/packages/react-sdk/src/component-generator.ts +++ b/packages/react-sdk/src/component-generator.ts @@ -407,34 +407,35 @@ export const generateWebstudioComponent = ({ metas: Map; }) => { const instance = instances.get(rootInstanceId); - if (instance === undefined) { - return ""; - } const indexesWithinAncestors = getIndexesWithinAncestors(metas, instances, [ rootInstanceId, ]); const usedDataSources: DataSources = new Map(); - const generatedJsx = generateJsxElement({ - context: "expression", - scope, - instance, - props, - dataSources, - usedDataSources, - indexesWithinAncestors, - classesMap, - children: generateJsxChildren({ + let generatedJsx = "<>\n"; + // instance can be missing when generate xml + if (instance) { + generatedJsx = generateJsxElement({ + context: "expression", scope, - children: instance.children, - instances, + instance, props, dataSources, usedDataSources, indexesWithinAncestors, classesMap, - }), - }); + children: generateJsxChildren({ + scope, + children: instance.children, + instances, + props, + dataSources, + usedDataSources, + indexesWithinAncestors, + classesMap, + }), + }); + } let generatedProps = ""; let generatedParameters = ""; From 7020b09e8b070a6fb7267cfba13c89142009ee59 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sat, 8 Mar 2025 18:23:47 +0700 Subject: [PATCH 7/9] fix: update display from none (#4969) Fixes https://github.com/webstudio-is/webstudio/issues/4963 Looks like accidentally wrong method was used. --- apps/builder/app/canvas/shared/styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/canvas/shared/styles.ts b/apps/builder/app/canvas/shared/styles.ts index 21152ec7c733..8c3bbbe8012e 100644 --- a/apps/builder/app/canvas/shared/styles.ts +++ b/apps/builder/app/canvas/shared/styles.ts @@ -39,7 +39,7 @@ import { canvasApi } from "~/shared/canvas-api"; import { $selectedInstance, $selectedPage } from "~/shared/awareness"; import { findAllEditableInstanceSelector } from "~/shared/instance-utils"; import type { InstanceSelector } from "~/shared/tree-utils"; -import { getVisibleElementsByInstanceSelector } from "~/shared/dom-utils"; +import { getAllElementsByInstanceSelector } from "~/shared/dom-utils"; import { createComputedStyleDeclStore } from "~/builder/features/style-panel/shared/model"; const userSheet = createRegularStyleSheet({ name: "user-styles" }); @@ -641,7 +641,7 @@ const subscribeEphemeralStyle = () => { // We need to apply the custom property to the selected element as well. // Otherwise, variables defined on it will not be visible on documentElement. - const elements = getVisibleElementsByInstanceSelector(instanceSelector); + const elements = getAllElementsByInstanceSelector(instanceSelector); for (const element of elements) { element.style.setProperty( getEphemeralProperty(styleDecl), From 3bdc00ba6acbe00dda0d2d346650bfb19fcea387 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Thu, 13 Mar 2025 22:15:19 +0700 Subject: [PATCH 8/9] fix: load resources when switch pages (#4994) Fixes https://github.com/webstudio-is/webstudio/issues/4992 --- .../settings-panel/variable-popover.tsx | 4 +- apps/builder/app/shared/nano-states/props.ts | 80 +++++++++++-------- packages/sdk-components-animation/private-src | 2 +- 3 files changed, 49 insertions(+), 37 deletions(-) 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 f7f0cf1195c9..5df809cec14a 100644 --- a/apps/builder/app/builder/features/settings-panel/variable-popover.tsx +++ b/apps/builder/app/builder/features/settings-panel/variable-popover.tsx @@ -55,7 +55,7 @@ import { $resources, $areResourcesLoading, invalidateResource, - getComputedResource, + getComputedResourceRequest, $userPlanFeatures, $instances, $props, @@ -776,7 +776,7 @@ export const VariablePopoverTrigger = ({ prefix={} color="ghost" onClick={() => { - const resourceRequest = getComputedResource( + const resourceRequest = getComputedResourceRequest( variable.resourceId ); if (resourceRequest) { diff --git a/apps/builder/app/shared/nano-states/props.ts b/apps/builder/app/shared/nano-states/props.ts index f8efa10e4448..bd9f7a8a11a4 100644 --- a/apps/builder/app/shared/nano-states/props.ts +++ b/apps/builder/app/shared/nano-states/props.ts @@ -15,6 +15,7 @@ import { portalComponent, ROOT_INSTANCE_ID, SYSTEM_VARIABLE_ID, + findTreeInstanceIds, } from "@webstudio-is/sdk"; import { normalizeProps, textContentAttribute } from "@webstudio-is/react-sdk"; import { mapGroupBy } from "~/shared/shim"; @@ -168,23 +169,19 @@ const $unscopedVariableValues = computed( * circular updates */ const $loaderVariableValues = computed( - [$dataSources, $dataSourceVariables, $selectedPage, $currentSystem], - (dataSources, dataSourceVariables, selectedPage, system) => { + [$dataSources, $selectedPage, $currentSystem], + (dataSources, selectedPage, system) => { const values = new Map(); values.set(SYSTEM_VARIABLE_ID, system); for (const [dataSourceId, dataSource] of dataSources) { if (dataSource.type === "variable") { - values.set( - dataSourceId, - dataSourceVariables.get(dataSourceId) ?? dataSource.value.value - ); + values.set(dataSourceId, dataSource.value.value); } - if (dataSource.type === "parameter") { - let value = dataSourceVariables.get(dataSourceId); - if (dataSource.id === selectedPage?.systemDataSourceId) { - value = system; - } - values.set(dataSourceId, value); + if ( + dataSource.type === "parameter" || + dataSource.id === selectedPage?.systemDataSourceId + ) { + values.set(dataSourceId, system); } } return values; @@ -549,7 +546,7 @@ export const $variableValuesByInstanceSelector = computed( } ); -const computeResource = ( +const computeResourceRequest = ( resource: Resource, values: Map ): ResourceRequest => { @@ -569,14 +566,31 @@ const computeResource = ( return request; }; -const $computedResources = computed( - [$resources, $loaderVariableValues], - (resources, values) => { - const computedResources: ResourceRequest[] = []; - for (const resource of resources.values()) { - computedResources.push(computeResource(resource, values)); +const $computedResourceRequests = computed( + [$selectedPage, $instances, $dataSources, $resources, $loaderVariableValues], + (page, instances, dataSources, resources, values) => { + const computedResourceRequests: ResourceRequest[] = []; + if (page === undefined) { + return computedResourceRequests; + } + const instanceIds = findTreeInstanceIds(instances, page.rootInstanceId); + instanceIds.add(ROOT_INSTANCE_ID); + // load only resources bound to variables on current page + // action resources should not be loaded automatically + for (const dataSource of dataSources.values()) { + if ( + instanceIds.has(dataSource.scopeInstanceId ?? "") && + dataSource.type === "resource" + ) { + const resource = resources.get(dataSource.resourceId); + if (resource) { + computedResourceRequests.push( + computeResourceRequest(resource, values) + ); + } + } } - return computedResources; + return computedResourceRequests; } ); @@ -603,19 +617,19 @@ const cacheByKeys = new Map(); const $invalidator = atom(0); -export const getComputedResource = (resourceId: Resource["id"]) => { +export const getComputedResourceRequest = (resourceId: Resource["id"]) => { const resources = $resources.get(); const resource = resources.get(resourceId); if (resource === undefined) { return; } const values = $loaderVariableValues.get(); - return computeResource(resource, values); + return computeResourceRequest(resource, values); }; // bump index of resource to invaldate cache entry export const invalidateResource = (resourceId: Resource["id"]) => { - const request = getComputedResource(resourceId); + const request = getComputedResourceRequest(resourceId); if (request === undefined) { return; } @@ -634,10 +648,10 @@ export const subscribeResources = () => { let frameId: undefined | number; // subscribe changing resources or global invalidation return computed( - [$computedResources, $invalidator], - (computedResources, invalidator) => - [computedResources, invalidator] as const - ).subscribe(([computedResources]) => { + [$computedResourceRequests, $invalidator], + (computedResourceRequests, invalidator) => + [computedResourceRequests, invalidator] as const + ).subscribe(([computedResourceRequests]) => { if (frameId) { cancelAnimationFrame(frameId); } @@ -646,7 +660,7 @@ export const subscribeResources = () => { frameId = requestAnimationFrame(async () => { const matched = new Map(); const missing = new Map(); - for (const request of computedResources) { + for (const request of computedResourceRequests) { const cacheKey = JSON.stringify(request); if (cacheByKeys.has(cacheKey)) { matched.set(request.id, request); @@ -661,14 +675,12 @@ export const subscribeResources = () => { cacheByKeys.set(cacheKey, undefined); } - const missingValues = Array.from(missing.values()); - if (missingValues.length === 0) { - return; + let result = new Map(); + if (missing.size > 0) { + result = await loadResources(Array.from(missing.values())); } - - const result = await loadResources(missingValues); const newResourceValues = new Map(); - for (const request of computedResources) { + for (const request of computedResourceRequests) { const cacheKey = JSON.stringify(request); // read from cache or store in cache const response = result.get(request.id) ?? cacheByKeys.get(cacheKey); diff --git a/packages/sdk-components-animation/private-src b/packages/sdk-components-animation/private-src index c1053aa05f2d..0d0d2790759a 160000 --- a/packages/sdk-components-animation/private-src +++ b/packages/sdk-components-animation/private-src @@ -1 +1 @@ -Subproject commit c1053aa05f2d5986e3c75a3061281dc8e282ce6f +Subproject commit 0d0d2790759ac01df3c9104d4e0e34f4a09ddab7 From 846dbd385f8a941a4d7b888705f2dc6be79c92bf Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Thu, 13 Mar 2025 22:32:59 +0700 Subject: [PATCH 9/9] fix: navigate to home page when undo page creation (#4995) Fixes https://github.com/webstudio-is/webstudio/issues/4993 --- apps/builder/app/shared/awareness.ts | 7 ++++++- apps/builder/app/shared/instance-utils.test.tsx | 4 ++++ apps/builder/app/shared/instance-utils.ts | 8 +++++++- apps/builder/app/shared/pages/use-switch-page.ts | 13 +++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/shared/awareness.ts b/apps/builder/app/shared/awareness.ts index ea5fee5cd11b..4dd991c45d8c 100644 --- a/apps/builder/app/shared/awareness.ts +++ b/apps/builder/app/shared/awareness.ts @@ -99,7 +99,7 @@ export const getInstancePath = ( instances: Instances, virtualInstances?: Instances, temporaryInstances?: Instances -): InstancePath => { +): undefined | InstancePath => { const instancePath: InstancePath = []; for (let index = 0; index < instanceSelector.length; index += 1) { const instanceId = instanceSelector[index]; @@ -116,6 +116,11 @@ export const getInstancePath = ( instanceSelector: instanceSelector.slice(index), }); } + // all consuming code expect at least one instance to be selected + // though it is possible to get empty array when undo created page + if (instancePath.length === 0) { + return undefined; + } return instancePath; }; diff --git a/apps/builder/app/shared/instance-utils.test.tsx b/apps/builder/app/shared/instance-utils.test.tsx index 8313d804c9ac..8059bc6a0cc2 100644 --- a/apps/builder/app/shared/instance-utils.test.tsx +++ b/apps/builder/app/shared/instance-utils.test.tsx @@ -1505,3 +1505,7 @@ describe("find closest insertable", () => { expect(findClosestInsertable(newListItemFragment)).toEqual(undefined); }); }); + +test("get undefined instead of instance path when no instances found", () => { + expect(getInstancePath(["boxId"], new Map())).toEqual(undefined); +}); diff --git a/apps/builder/app/shared/instance-utils.ts b/apps/builder/app/shared/instance-utils.ts index 37a5404e2e98..45ae284a78cc 100644 --- a/apps/builder/app/shared/instance-utils.ts +++ b/apps/builder/app/shared/instance-utils.ts @@ -315,6 +315,9 @@ export const insertWebstudioFragmentAt = ( insertable.parentSelector, data.instances ); + if (instancePath === undefined) { + return; + } const { newInstanceIds } = insertWebstudioFragmentCopy({ data, fragment, @@ -432,8 +435,11 @@ export const reparentInstance = ( export const deleteInstanceMutable = ( data: Omit, - instancePath: InstancePath + instancePath: undefined | InstancePath ) => { + if (instancePath === undefined) { + return false; + } const { instances, props, diff --git a/apps/builder/app/shared/pages/use-switch-page.ts b/apps/builder/app/shared/pages/use-switch-page.ts index d0591b996205..96dc7e007444 100644 --- a/apps/builder/app/shared/pages/use-switch-page.ts +++ b/apps/builder/app/shared/pages/use-switch-page.ts @@ -109,6 +109,19 @@ export const useSyncPageUrl = () => { }) ); }, [builderMode, navigate, page, pageHash]); + + useEffect(() => { + return $selectedPage.subscribe((page) => { + // switch to home page when current one does not exist + // possible when undo creating page + if (page === undefined) { + const pages = $pages.get(); + if (pages) { + selectPage(pages.homePage.id); + } + } + }); + }); }; /**