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">$.Body>),
+ })
+ ).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