Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: Release 05-03-2025 #4949

Closed
wants to merge 9 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import {
$resources,
$areResourcesLoading,
invalidateResource,
getComputedResource,
getComputedResourceRequest,
$userPlanFeatures,
$instances,
$props,
Expand Down Expand Up @@ -776,7 +776,7 @@ export const VariablePopoverTrigger = ({
prefix={<CopyIcon />}
color="ghost"
onClick={() => {
const resourceRequest = getComputedResource(
const resourceRequest = getComputedResourceRequest(
variable.resourceId
);
if (resourceRequest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,32 +318,36 @@ export const StyleSourceControl = ({
disabled={disabled}
aria-current={selected && state === undefined}
role="button"
onClick={onSelect}
hasError={error !== undefined}
>
<Flex grow css={{ padding: theme.spacing[2] }}>
<StyleSourceButton
disabled={disabled || isEditing}
isEditing={isEditing}
tabIndex={-1}
onClick={onSelect}
>
<Flex align="center" justify="center" gap="1">
{source === "local" ? (
{source === "local" ? (
<Flex justify="center" align="center">
<Box
// We need this so that the small local button has a bigger clickable surface
css={{ position: "absolute", inset: 0 }}
/>
<LocalStyleIcon showDot={hasStyles} />
) : (
<>
<EditableText
isEditing={isEditing}
onChangeEditing={onChangeEditing}
onChangeValue={onChangeValue}
value={label}
/>
{hasStyles === false && isEditing === false && (
<LocalStyleIcon showDot={hasStyles} />
)}
</>
)}
</Flex>
</Flex>
) : (
<Flex align="center" justify="center" gap="1">
<EditableText
isEditing={isEditing}
onChangeEditing={onChangeEditing}
onChangeValue={onChangeValue}
value={label}
/>
{hasStyles === false && isEditing === false && (
<LocalStyleIcon showDot={hasStyles} />
)}
</Flex>
)}
</StyleSourceButton>
</Flex>
{stateLabel !== undefined && (
Expand Down
31 changes: 25 additions & 6 deletions apps/builder/app/builder/shared/css-editor/css-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ export const CssEditor = ({
},
}));

const advancedProperties = Array.from(styleMap.keys()) as Array<CssProperty>;
const advancedProperties = Array.from(styleMap.keys());

const currentProperties =
searchProperties ??
Expand Down Expand Up @@ -394,7 +394,7 @@ export const CssEditor = ({
keys: ["property", "value"],
}).map(({ property }) => property);

setSearchProperties(matched as CssProperty[]);
setSearchProperties(matched);
};

const afterChangingStyles = () => {
Expand All @@ -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 && (
Expand All @@ -420,8 +439,8 @@ export const CssEditor = ({
)}
<CssEditorContextMenu
onPaste={handleInsertStyles}
onDeleteProperty={onDeleteProperty}
onDeleteAllDeclarations={onDeleteAllDeclarations}
onDeleteProperty={handleDeleteProperty}
onDeleteAllDeclarations={handleDeleteAllDeclarations}
styleMap={styleMap}
properties={
searchProperties ?? [...recentProperties, ...currentProperties]
Expand All @@ -446,7 +465,7 @@ export const CssEditor = ({
}
}}
onReset={afterChangingStyles}
onDeleteProperty={onDeleteProperty}
onDeleteProperty={handleDeleteProperty}
onSetProperty={onSetProperty}
/>
);
Expand Down Expand Up @@ -493,7 +512,7 @@ export const CssEditor = ({
<LazyRender key={property}>
<AdvancedDeclarationLonghand
property={property}
onDeleteProperty={onDeleteProperty}
onDeleteProperty={handleDeleteProperty}
onSetProperty={onSetProperty}
/>
</LazyRender>
Expand Down
4 changes: 2 additions & 2 deletions apps/builder/app/canvas/shared/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand Down Expand Up @@ -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),
Expand Down
7 changes: 6 additions & 1 deletion apps/builder/app/shared/awareness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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;
};

Expand Down
4 changes: 4 additions & 0 deletions apps/builder/app/shared/instance-utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
8 changes: 7 additions & 1 deletion apps/builder/app/shared/instance-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ export const insertWebstudioFragmentAt = (
insertable.parentSelector,
data.instances
);
if (instancePath === undefined) {
return;
}
const { newInstanceIds } = insertWebstudioFragmentCopy({
data,
fragment,
Expand Down Expand Up @@ -432,8 +435,11 @@ export const reparentInstance = (

export const deleteInstanceMutable = (
data: Omit<WebstudioData, "pages">,
instancePath: InstancePath
instancePath: undefined | InstancePath
) => {
if (instancePath === undefined) {
return false;
}
const {
instances,
props,
Expand Down
80 changes: 46 additions & 34 deletions apps/builder/app/shared/nano-states/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, unknown>();
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;
Expand Down Expand Up @@ -549,7 +546,7 @@ export const $variableValuesByInstanceSelector = computed(
}
);

const computeResource = (
const computeResourceRequest = (
resource: Resource,
values: Map<DataSource["id"], unknown>
): ResourceRequest => {
Expand All @@ -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;
}
);

Expand All @@ -603,19 +617,19 @@ const cacheByKeys = new Map<string, unknown>();

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;
}
Expand All @@ -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);
}
Expand All @@ -646,7 +660,7 @@ export const subscribeResources = () => {
frameId = requestAnimationFrame(async () => {
const matched = new Map<Resource["id"], ResourceRequest>();
const missing = new Map<Resource["id"], ResourceRequest>();
for (const request of computedResources) {
for (const request of computedResourceRequests) {
const cacheKey = JSON.stringify(request);
if (cacheByKeys.has(cacheKey)) {
matched.set(request.id, request);
Expand All @@ -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<string, unknown>();
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);
Expand Down
13 changes: 13 additions & 0 deletions apps/builder/app/shared/pages/use-switch-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
});
});
};

/**
Expand Down
Loading
Loading