Move breakpoints logic to designer (#782)

We have stores synchronization and can move the logic from canvas to
designer.

Here refactored this way breakpoints, got rid from direct atom changes
without store and removed breakpoints pub sub events.
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2023-01-13 19:32:23 +03:00
1 parent f9f64934db
commit 00da9c8e86
9 files changed
+125 -136

No files matched your search

+2 -6
View File
@@ -28,14 +28,11 @@ import { useTrackSelectedElement } from "./shared/use-track-selected-element";
import { WrapperComponentDev } from "./features/wrapper-component";
import { useSync } from "./shared/sync";
import { useManageProps } from "./shared/props";
import {
useManageBreakpoints,
useInitializeBreakpoints,
} from "./shared/breakpoints";
import {
rootInstanceContainer,
useBreakpoints,
useRootInstance,
useSetBreakpoints,
useSetPresetStyles,
useSetRootInstance,
useSetStyles,
@@ -135,7 +132,6 @@ type DesignModeProps = {
};
const DesignMode = ({ treeId, buildId }: DesignModeProps) => {
useManageBreakpoints();
useManageDesignModeStyles();
useManageProps({ treeId });
usePublishSelectedInstanceData();
@@ -166,9 +162,9 @@ export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
throw new Error("Tree is null");
}
const isDesignerReady = useSubscribeDesignerReady();
useInitializeBreakpoints(data.breakpoints);
useInitializeDesignTokens(data.designTokens);
const assets = useAssets(data.assets);
useSetBreakpoints(data.breakpoints);
useAllUserProps(data.props);
useSetPresetStyles(data.tree.presetStyles);
useSetStyles(data.tree.styles);
@@ -1,65 +0,0 @@
import store from "immerhin";
import type { Breakpoint } from "@webstudio-is/css-data";
import { useSubscribe } from "~/shared/pubsub";
import {
breakpointsContainer,
rootInstanceContainer,
stylesContainer,
useBreakpoints,
} from "~/shared/nano-states";
import { addMediaRules } from "./styles";
import { useSyncInitializeOnce } from "~/shared/hook-utils";
import { filterMutable } from "~/shared/array-utils";
export const useInitializeBreakpoints = (breakpoints: Array<Breakpoint>) => {
const [, setCurrentBreakpoints] = useBreakpoints();
useSyncInitializeOnce(() => {
setCurrentBreakpoints(breakpoints);
addMediaRules(breakpoints);
});
};
const useBreakpointChange = () => {
useSubscribe("breakpointChange", (breakpoint) => {
store.createTransaction([breakpointsContainer], (breakpoints) => {
const foundBreakpoint = breakpoints.find(
({ id }) => id === breakpoint.id
);
if (foundBreakpoint) {
foundBreakpoint.label = breakpoint.label;
foundBreakpoint.minWidth = breakpoint.minWidth;
} else {
// Its a new breakpoint
breakpoints.push(breakpoint);
}
addMediaRules(breakpoints);
});
});
};
const useBreakpointDelete = () => {
useSubscribe("breakpointDelete", (breakpoint) => {
store.createTransaction(
[breakpointsContainer, rootInstanceContainer, stylesContainer],
(breakpoints, rootInstance, styles) => {
if (rootInstance === undefined) {
return;
}
const index = breakpoints.findIndex(({ id }) => id === breakpoint.id);
if (index !== -1) {
breakpoints.splice(index, 1);
}
// delete breakpoint styles
filterMutable(styles, (style) => style.breakpointId !== breakpoint.id);
}
);
});
};
export const useManageBreakpoints = () => {
useBreakpointChange();
useBreakpointDelete();
};
+8 -7
View File
@@ -17,7 +17,6 @@ import {
} from "@webstudio-is/react-sdk";
import {
validStaticValueTypes,
type Breakpoint,
type ValidStaticStyleValue,
type StyleValue,
type CssRule,
@@ -80,6 +79,14 @@ const tokensCssEngine = createCssEngine({ name: "tokens" });
const presetStylesEngine = createCssEngine({ name: "presetStyles" });
export const GlobalStyles = ({ assets }: { assets: Array<Asset> }) => {
const [breakpoints] = useBreakpoints();
useIsomorphicLayoutEffect(() => {
for (const breakpoint of breakpoints) {
cssEngine.addMediaRule(breakpoint.id, breakpoint);
}
}, [breakpoints]);
useIsomorphicLayoutEffect(() => {
for (const style of helperStyles) {
helpersCssEngine.addPlaintextRule(style);
@@ -159,12 +166,6 @@ const toVarStyleWithFallback = (instanceId: string, style: Style): Style => {
return dynamicStyle;
};
export const addMediaRules = (breakpoints: Array<Breakpoint>) => {
for (const breakpoint of breakpoints) {
cssEngine.addMediaRule(breakpoint.id, breakpoint);
}
};
const wrappedRulesMap = new Map<string, StyleRule | PlaintextRule>();
const addRule = (id: string, cssRule: CssRule) => {
@@ -1,11 +1,12 @@
import { useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { type Breakpoint } from "@webstudio-is/css-data";
import { type Publish } from "~/shared/pubsub";
import ObjectId from "bson-objectid";
import store from "immerhin";
import type { Breakpoint } from "@webstudio-is/css-data";
import { Button, TextField, Flex, Text } from "@webstudio-is/design-system";
import { PlusIcon, TrashIcon } from "@webstudio-is/icons";
import ObjectId from "bson-objectid";
import { useBreakpoints } from "~/shared/nano-states";
import { breakpointsContainer, useBreakpoints } from "~/shared/nano-states";
import { replaceByOrAppendMutable } from "~/shared/array-utils";
type BreakpointEditorItemProps = {
breakpoint: Breakpoint;
@@ -87,16 +88,23 @@ const BreakpointEditorItem = ({
};
type BreakpointsEditorProps = {
breakpoints: Array<Breakpoint>;
publish: Publish;
onDelete: (breakpoint: Breakpoint) => void;
};
export const BreakpointsEditor = ({
publish,
onDelete,
}: BreakpointsEditorProps) => {
const [breakpoints, setBreakpoints] = useBreakpoints();
export const BreakpointsEditor = ({ onDelete }: BreakpointsEditorProps) => {
const [breakpoints] = useBreakpoints();
const [addedBreakpoints, setAddedBreakpoints] = useState<Breakpoint[]>([]);
const storedBreakpoints = new Set<string>();
for (const breakpoint of breakpoints) {
storedBreakpoints.add(breakpoint.id);
}
// filter out new breakpoints which are already store
// instead of deleting from state to avoid the case
// when both states do not have such breakpoint
// and focused input remounts
const newBreakpoints = addedBreakpoints.filter(
(breakpoint) => storedBreakpoints.has(breakpoint.id) === false
);
return (
<Flex gap="2" direction="column">
<Flex
@@ -113,8 +121,8 @@ export const BreakpointsEditor = ({
<Button
variant="ghost"
onClick={() => {
setBreakpoints([
...breakpoints,
setAddedBreakpoints((prev) => [
...prev,
{
id: ObjectId().toString(),
label: "",
@@ -125,20 +133,19 @@ export const BreakpointsEditor = ({
prefix={<PlusIcon />}
/>
</Flex>
{breakpoints.map((breakpoint) => {
{[...breakpoints, ...newBreakpoints].map((breakpoint) => {
return (
<BreakpointEditorItem
key={breakpoint.id}
breakpoint={breakpoint}
onChange={(updatedBreakpoint) => {
publish({ type: "breakpointChange", payload: updatedBreakpoint });
const nextBreakpoints = breakpoints.map((breakpoint) => {
if (breakpoint.id === updatedBreakpoint.id) {
return updatedBreakpoint;
}
return breakpoint;
store.createTransaction([breakpointsContainer], (breakpoints) => {
replaceByOrAppendMutable(
breakpoints,
updatedBreakpoint,
({ id }) => id === updatedBreakpoint.id
);
});
setBreakpoints(nextBreakpoints);
}}
onDelete={onDelete}
/>
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import store from "immerhin";
import { type Breakpoint } from "@webstudio-is/css-data";
import { type Publish, useSubscribe } from "~/shared/pubsub";
import { useSubscribe } from "~/shared/pubsub";
import {
Text,
DropdownMenu,
@@ -23,16 +24,13 @@ import {
useSubscribeZoomFromShortcut,
} from "./use-subscribe-shortcuts";
import { ConfirmationDialog } from "./confirmation-dialog";
import { useBreakpoints } from "~/shared/nano-states";
import {
breakpointsContainer,
stylesContainer,
useBreakpoints,
} from "~/shared/nano-states";
import { utils } from "@webstudio-is/project";
declare module "~/shared/pubsub" {
export interface PubsubMap {
breakpointDelete: Breakpoint;
breakpointChange: Breakpoint;
loadBreakpoints: Array<Breakpoint>;
}
}
import { removeByMutable } from "~/shared/array-utils";
type BreakpointSelectorItemProps = {
breakpoint: Breakpoint;
@@ -56,18 +54,14 @@ const menuItemCss = {
minWidth: 180,
};
type BreakpointsProps = {
publish: Publish;
};
export const Breakpoints = ({ publish }: BreakpointsProps) => {
export const Breakpoints = () => {
const [view, setView] = useState<
"selector" | "editor" | "confirmation" | undefined
>();
const [breakpointToDelete, setBreakpointToDelete] = useState<
Breakpoint | undefined
>();
const [breakpoints, setBreakpoints] = useBreakpoints();
const [breakpoints] = useBreakpoints();
const [selectedBreakpoint, setSelectedBreakpoint] = useSelectedBreakpoint();
const [breakpointPreview, setBreakpointPreview] =
useState(selectedBreakpoint);
@@ -94,17 +88,20 @@ export const Breakpoints = ({ publish }: BreakpointsProps) => {
if (breakpointToDelete === undefined) {
return;
}
const nextBreakpoints = [...breakpoints];
const index = breakpoints.indexOf(breakpointToDelete);
nextBreakpoints.splice(index, 1);
setBreakpoints(nextBreakpoints);
const [updatedBreakpoints] = store.createTransaction(
[breakpointsContainer, stylesContainer],
(breakpoints, styles) => {
removeByMutable(breakpoints, ({ id }) => id === breakpointToDelete.id);
removeByMutable(
styles,
(style) => style.breakpointId === breakpointToDelete.id
);
}
);
if (breakpointToDelete === selectedBreakpoint) {
setSelectedBreakpoint(utils.breakpoints.sort(nextBreakpoints)[0]);
setSelectedBreakpoint(utils.breakpoints.sort(updatedBreakpoints)[0]);
}
publish({
type: "breakpointDelete",
payload: breakpointToDelete,
});
setBreakpointToDelete(undefined);
setView("editor");
};
@@ -140,8 +137,6 @@ export const Breakpoints = ({ publish }: BreakpointsProps) => {
{view === "editor" && (
<>
<BreakpointsEditor
breakpoints={breakpoints}
publish={publish}
onDelete={(breakpoint) => {
setBreakpointToDelete(breakpoint);
setView("confirmation");
@@ -41,7 +41,7 @@ export const Topbar = ({ css, project, publish, previewUrl }: TopbarProps) => {
}}
>
<Menu publish={publish} />
<Breakpoints publish={publish} />
<Breakpoints />
<Flex
align="center"
css={{
+39 -4
View File
@@ -1,7 +1,7 @@
import { expect, test } from "@jest/globals";
import { filterMutable } from "./array-utils";
import { removeByMutable, replaceByOrAppendMutable } from "./array-utils";
test("filterMutable", () => {
test("removeByMutable", () => {
const array = [
{ param: 1 },
{ param: 2 },
@@ -10,7 +10,7 @@ test("filterMutable", () => {
{ param: 5 },
{ param: 6 },
];
filterMutable(array, (item) => item.param % 2 === 0);
removeByMutable(array, (item) => item.param % 2 === 1);
expect(array).toMatchInlineSnapshot(`
[
{
@@ -24,7 +24,7 @@ test("filterMutable", () => {
},
]
`);
filterMutable(array, (item) => item.param === 4);
removeByMutable(array, (item) => item.param !== 4);
expect(array).toMatchInlineSnapshot(`
[
{
@@ -33,3 +33,38 @@ test("filterMutable", () => {
]
`);
});
test("replaceByOrAppendMutable", () => {
const array = [{ param: 1 }, { param: 2 }, { param: 3 }];
replaceByOrAppendMutable(array, { param: 6 }, (item) => item.param === 2);
expect(array).toMatchInlineSnapshot(`
[
{
"param": 1,
},
{
"param": 6,
},
{
"param": 3,
},
]
`);
replaceByOrAppendMutable(array, { param: 8 }, (item) => item.param === 8);
expect(array).toMatchInlineSnapshot(`
[
{
"param": 1,
},
{
"param": 6,
},
{
"param": 3,
},
{
"param": 8,
},
]
`);
});
+19 -4
View File
@@ -1,11 +1,26 @@
export const filterMutable = <T>(
array: T[],
predicate: (value: T) => boolean
type Predicate<Item> = (value: Item) => boolean;
export const removeByMutable = <Item>(
array: Item[],
predicate: Predicate<Item>
) => {
// reversed order to splice without breaking index
for (let index = array.length - 1; index >= 0; index -= 1) {
if (predicate(array[index]) === false) {
if (predicate(array[index])) {
array.splice(index, 1);
}
}
};
export const replaceByOrAppendMutable = <Item>(
array: Item[],
item: Item,
predicate: Predicate<Item>
) => {
const matchedIndex = array.findIndex(predicate);
if (matchedIndex === -1) {
array.push(item);
} else {
array[matchedIndex] = item;
}
};
@@ -38,8 +38,13 @@ export const useSetStyles = (styles: Styles) => {
});
};
export const breakpointsContainer = atom<Array<Breakpoint>>([]);
export const breakpointsContainer = atom<Breakpoint[]>([]);
export const useBreakpoints = () => useValue(breakpointsContainer);
export const useSetBreakpoints = (breakpoints: Breakpoint[]) => {
useSyncInitializeOnce(() => {
breakpointsContainer.set(breakpoints);
});
};
const isPreviewModeContainer = atom<boolean>(false);
export const useIsPreviewMode = () => useValue(isPreviewModeContainer);