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/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..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 @@ -318,7 +318,6 @@ export const StyleSourceControl = ({ disabled={disabled} aria-current={selected && state === undefined} role="button" - onClick={onSelect} hasError={error !== undefined} > @@ -326,24 +325,29 @@ export const StyleSourceControl = ({ disabled={disabled || isEditing} isEditing={isEditing} tabIndex={-1} + onClick={onSelect} > - - {source === "local" ? ( + {source === "local" ? ( + + - ) : ( - <> - - {hasStyles === false && isEditing === false && ( - - )} - - )} - + + ) : ( + + + {hasStyles === false && isEditing === false && ( + + )} + + )} {stateLabel !== undefined && ( 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 = ({ 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), 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/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/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); + } + } + }); + }); }; /** diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index df4679e1b999..0a51fd24c46c 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -497,8 +497,10 @@ 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") { - rootInstanceId = bodyInstance.children[0].value; + // @todo test empty xml + 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 = ""; 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