Represent styles as map (#1008)

Ref https://github.com/webstudio-is/webstudio-designer/issues/696

Here patches by styles indexes are replaced with patch by key to prevent
conflicts while collaborating.

The unusual part here is map key whiich is combined from a few fields:
"styleSourceId:breakpointId:property"

This format is enforced with typescript to always use utility
getStyleDeclKey to access specific style.
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2023-02-13 16:08:19 +03:00
1 parent 2e43ba1b39
commit 85f5f832aa
18 files changed
+191 -142

No files matched your search

@@ -1,6 +1,6 @@
import { useEffect } from "react";
import { useStore } from "@nanostores/react";
import type { Instance, Prop, Styles } from "@webstudio-is/project-build";
import type { Instance, Prop, StyleDecl } from "@webstudio-is/project-build";
import { getBrowserStyle } from "@webstudio-is/react-sdk";
import { publish, subscribe } from "~/shared/pubsub";
import {
@@ -57,7 +57,7 @@ export const SelectedInstanceConnector = ({
}: {
instanceElementRef: { current: undefined | HTMLElement };
instance: Instance;
instanceStyles: Styles;
instanceStyles: StyleDecl[];
instanceProps: undefined | Prop[];
}) => {
const rootInstance = useStore(rootInstanceContainer);
+2 -2
View File
@@ -7,7 +7,7 @@ import {
selectedInstanceIdStore,
useBreakpoints,
} from "~/shared/nano-states";
import type { Styles } from "@webstudio-is/project-build";
import type { StyleDecl } from "@webstudio-is/project-build";
import {
getComponentMeta,
getComponentNames,
@@ -159,7 +159,7 @@ export const useCssRules = ({
instanceStyles,
}: {
instanceId: string;
instanceStyles: Styles;
instanceStyles: StyleDecl[];
}) => {
const [breakpoints] = useBreakpoints();
@@ -69,11 +69,13 @@ export const Breakpoints = () => {
const [updatedBreakpoints] = store.createTransaction(
[breakpointsContainer, stylesStore],
(breakpoints, styles) => {
removeByMutable(breakpoints, ({ id }) => id === breakpointToDelete.id);
removeByMutable(
styles,
(style) => style.breakpointId === breakpointToDelete.id
);
const breakpointId = breakpointToDelete.id;
removeByMutable(breakpoints, ({ id }) => id === breakpointId);
for (const [styleDeclKey, styleDecl] of styles) {
if (styleDecl.breakpointId === breakpointId) {
styles.delete(styleDeclKey);
}
}
}
);
if (breakpointToDelete === selectedBreakpoint) {
@@ -1,5 +1,5 @@
import type { Breakpoint } from "@webstudio-is/css-data";
import type { Instance, Styles } from "@webstudio-is/project-build";
import type { Instance, StyleDecl } from "@webstudio-is/project-build";
import {
getCascadedBreakpointIds,
getCascadedInfo,
@@ -36,7 +36,7 @@ const cascadedBreakpointIds = getCascadedBreakpointIds(
selectedBreakpointId
);
const cascadingStylesByInstanceId = new Map<Instance["id"], Styles>();
const cascadingStylesByInstanceId = new Map<Instance["id"], StyleDecl[]>();
cascadingStylesByInstanceId.set(selectedInstanceId, [
{
breakpointId: "1",
@@ -92,7 +92,7 @@ const rootInstance: Instance = {
],
};
const inheritingStylesByInstanceId = new Map<Instance["id"], Styles>();
const inheritingStylesByInstanceId = new Map<Instance["id"], StyleDecl[]>();
inheritingStylesByInstanceId.set("1", [
// should be inherited even from another breakpoint
{
@@ -10,7 +10,7 @@ import { properties } from "@webstudio-is/css-data";
import { utils } from "@webstudio-is/project";
import type {
Instance,
Styles,
StyleDecl,
StyleSource as StyleSourceType,
} from "@webstudio-is/project-build";
import {
@@ -81,7 +81,7 @@ for (const [property, value] of Object.entries(properties)) {
}
const getSelectedStyle = (
stylesByStyleSourceId: Map<StyleSourceType["id"], Styles>,
stylesByStyleSourceId: Map<StyleSourceType["id"], StyleDecl[]>,
breakpointId: string,
styleSourceId: string
) => {
@@ -121,7 +121,7 @@ export const getCascadedBreakpointIds = (
* affecting current view
*/
export const getCascadedInfo = (
stylesByInstanceId: Map<Instance["id"], Styles>,
stylesByInstanceId: Map<Instance["id"], StyleDecl[]>,
instanceId: string,
cascadedBreakpointIds: string[]
) => {
@@ -149,7 +149,7 @@ export const getCascadedInfo = (
*/
export const getInheritedInfo = (
rootInstance: Instance,
stylesByInstanceId: Map<Instance["id"], Styles>,
stylesByInstanceId: Map<Instance["id"], StyleDecl[]>,
instanceId: string,
cascadedBreakpointIds: string[],
selectedBreakpointId: string
@@ -1,7 +1,7 @@
import { useCallback } from "react";
import store from "immerhin";
import warnOnce from "warn-once";
import type { Instance } from "@webstudio-is/project-build";
import { getStyleDeclKey, type Instance } from "@webstudio-is/project-build";
import type { StyleUpdates } from "@webstudio-is/project";
import type { StyleProperty, StyleValue } from "@webstudio-is/css-data";
import { type Publish } from "~/shared/pubsub";
@@ -12,10 +12,6 @@ import {
stylesStore,
} from "~/shared/nano-states";
import { useSelectedBreakpoint } from "~/designer/shared/nano-states";
import {
removeByMutable,
replaceByOrAppendMutable,
} from "~/shared/array-utils";
// @todo: must be removed, now it's only for compatibility with existing code
import { parseCssValue } from "./parse-css-value";
import { useStyleInfo } from "./style-info";
@@ -101,29 +97,22 @@ export const useStyleData = ({ selectedInstance, publish }: UseStyleData) => {
for (const update of updates) {
if (update.operation === "set") {
replaceByOrAppendMutable(
styles,
{
breakpointId,
styleSourceId,
property: update.property,
value: update.value,
},
(item) =>
item.styleSourceId === styleSourceId &&
item.breakpointId === breakpointId &&
item.property === update.property
);
const styleDecl = {
breakpointId,
styleSourceId,
property: update.property,
value: update.value,
};
styles.set(getStyleDeclKey(styleDecl), styleDecl);
}
if (update.operation === "delete") {
removeByMutable(
styles,
(item) =>
item.styleSourceId === styleSourceId &&
item.breakpointId === breakpointId &&
item.property === update.property
);
const styleDeclKey = getStyleDeclKey({
breakpointId,
styleSourceId,
property: update.property,
});
styles.delete(styleDeclKey);
}
}
}
@@ -2,9 +2,10 @@ import store from "immerhin";
import { z } from "zod";
import { useEffect } from "react";
import {
getStyleDeclKey,
Instance,
Prop,
Styles,
StyleDecl,
StyleSource,
StyleSourceSelection,
} from "@webstudio-is/project-build";
@@ -37,7 +38,7 @@ const InstanceData = z.object({
props: z.array(Prop),
styleSourceSelections: z.array(StyleSourceSelection),
styleSources: z.array(StyleSource),
styles: Styles,
styles: z.array(StyleDecl),
});
type InstanceData = z.infer<typeof InstanceData>;
@@ -118,7 +119,9 @@ const pasteInstance = (data: InstanceData) => {
for (const styleSource of data.styleSources) {
styleSources.set(styleSource.id, styleSource);
}
styles.push(...data.styles);
for (const styleDecl of data.styles) {
styles.set(getStyleDeclKey(styleDecl), styleDecl);
}
}
);
selectedInstanceIdStore.set(data.instance.id);
@@ -58,7 +58,7 @@ export const generateCssText = async (
}
const styleRules = getStyleRules(
canvasData.build?.styles,
new Map(canvasData.build?.styles),
new Map(canvasData.tree?.styleSourceSelections)
);
for (const { breakpointId, instanceId, style } of styleRules) {
+6 -4
View File
@@ -1,5 +1,5 @@
import store from "immerhin";
import { Instance } from "@webstudio-is/project-build";
import type { Instance } from "@webstudio-is/project-build";
import {
rootInstanceContainer,
propsStore,
@@ -86,9 +86,11 @@ export const deleteInstance = (targetInstanceId: Instance["id"]) => {
for (const styleSourceId of subtreeLocalStyleSourceIds) {
styleSources.delete(styleSourceId);
}
removeByMutable(styles, (styleDecl) =>
subtreeLocalStyleSourceIds.has(styleDecl.styleSourceId)
);
for (const [styleDeclKey, styleDecl] of styles) {
if (subtreeLocalStyleSourceIds.has(styleDecl.styleSourceId)) {
styles.delete(styleDeclKey);
}
}
selectedInstanceIdStore.set(parentInstance.id);
}
@@ -8,6 +8,8 @@ import {
Instance,
Prop,
Props,
StyleDecl,
StyleDeclKey,
Styles,
StyleSource,
StyleSources,
@@ -86,11 +88,11 @@ export const useInstanceProps = (instanceId: undefined | Instance["id"]) => {
return instanceProps;
};
export const stylesStore = atom<Styles>([]);
export const stylesStore = atom<Styles>(new Map());
export const useSetStyles = (styles: Styles) => {
export const useSetStyles = (styles: [StyleDeclKey, StyleDecl][]) => {
useSyncInitializeOnce(() => {
stylesStore.set(styles);
stylesStore.set(new Map(styles));
});
};
export const useInstanceStyles = (instanceId: undefined | Instance["id"]) => {
@@ -165,8 +167,8 @@ export const selectedStyleSourceIdStore = atom<undefined | StyleSource["id"]>(
export const stylesIndexStore = computed(
[stylesStore, styleSourceSelectionsStore],
(styles, styleSourceSelections) => {
const stylesByStyleSourceId = new Map<StyleSource["id"], Styles>();
for (const styleDecl of styles) {
const stylesByStyleSourceId = new Map<StyleSource["id"], StyleDecl[]>();
for (const styleDecl of styles.values()) {
const { styleSourceId } = styleDecl;
let styleSourceStyles = stylesByStyleSourceId.get(styleSourceId);
if (styleSourceStyles === undefined) {
@@ -176,9 +178,9 @@ export const stylesIndexStore = computed(
styleSourceStyles.push(styleDecl);
}
const stylesByInstanceId = new Map<Instance["id"], Styles>();
const stylesByInstanceId = new Map<Instance["id"], StyleDecl[]>();
for (const { instanceId, values } of styleSourceSelections.values()) {
const instanceStyles: Styles = [];
const instanceStyles: StyleDecl[] = [];
for (const styleSourceId of values) {
const styleSourceStyles = stylesByStyleSourceId.get(styleSourceId);
if (styleSourceStyles) {
+16 -12
View File
@@ -3,6 +3,7 @@ import type {
Instance,
Prop,
StyleDecl,
Styles,
StyleSource,
StyleSourceSelection,
} from "@webstudio-is/project-build";
@@ -73,7 +74,10 @@ const createStyleSourceSelection = (
};
};
const createStyleDecl = (styleSourceId: string): StyleDecl => {
const createStyleDecl = (
styleSourceId: string,
breakpointId: string
): StyleDecl => {
return {
styleSourceId,
breakpointId: "breakpointId",
@@ -389,21 +393,21 @@ test("clone style source selections with applied instance ids and style source i
});
test("clone styles with appled new style source ids", () => {
const styles = [
createStyleDecl("styleSource1"),
createStyleDecl("styleSource2"),
createStyleDecl("styleSource1"),
createStyleDecl("styleSource3"),
createStyleDecl("styleSource1"),
createStyleDecl("styleSource3"),
];
const styles: Styles = new Map([
[`styleSource1:bp1:width`, createStyleDecl("styleSource1", "bp1")],
[`styleSource2:bp2:width`, createStyleDecl("styleSource2", "bp2")],
[`styleSource1:bp3:width`, createStyleDecl("styleSource1", "bp3")],
[`styleSource3:bp4:width`, createStyleDecl("styleSource3", "bp4")],
[`styleSource1:bp5:width`, createStyleDecl("styleSource1", "bp5")],
[`styleSource3:bp6:width`, createStyleDecl("styleSource3", "bp6")],
]);
const clonedStyleSourceIds = new Map<StyleSource["id"], StyleSource["id"]>();
clonedStyleSourceIds.set("styleSource2", "newStyleSource2");
clonedStyleSourceIds.set("styleSource3", "newStyleSource3");
expect(cloneStyles(styles, clonedStyleSourceIds)).toEqual([
createStyleDecl("newStyleSource2"),
createStyleDecl("newStyleSource3"),
createStyleDecl("newStyleSource3"),
createStyleDecl("newStyleSource2", "bp2"),
createStyleDecl("newStyleSource3", "bp4"),
createStyleDecl("newStyleSource3", "bp6"),
]);
});
+3 -2
View File
@@ -4,6 +4,7 @@ import type {
Instance,
Prop,
Props,
StyleDecl,
Styles,
StyleSource,
StyleSources,
@@ -277,8 +278,8 @@ export const cloneStyles = (
styles: Styles,
clonedStyleSourceIds: Map<Instance["id"], Instance["id"]>
) => {
const clonedStyles: Styles = [];
for (const styleDecl of styles) {
const clonedStyles: StyleDecl[] = [];
for (const styleDecl of styles.values()) {
const styleSourceId = clonedStyleSourceIds.get(styleDecl.styleSourceId);
if (styleSourceId === undefined) {
continue;
+10 -1
View File
@@ -34,6 +34,15 @@ export const StyleDecl = z.object({
export type StyleDecl = z.infer<typeof StyleDecl>;
export const Styles = z.array(StyleDecl);
export type StyleDeclKey =
`${StyleDecl["styleSourceId"]}:${StyleDecl["breakpointId"]}:${StyleDecl["property"]}`;
export const getStyleDeclKey = (
styleDecl: Omit<StyleDecl, "value">
): StyleDeclKey => {
return `${styleDecl.styleSourceId}:${styleDecl.breakpointId}:${styleDecl.property}`;
};
export const Styles = z.map(z.string() as z.ZodType<StyleDeclKey>, StyleDecl);
export type Styles = z.infer<typeof Styles>;
+3 -3
View File
@@ -23,7 +23,7 @@ const parseBuild = async (build: DbBuild): Promise<Build> => {
createdAt: build.createdAt.toISOString(),
pages,
breakpoints: Breakpoints.parse(JSON.parse(build.breakpoints)),
styles: await parseStyles(build.projectId, build.styles),
styles: Array.from(await parseStyles(build.projectId, build.styles)),
styleSources: Array.from(parseStyleSources(build.styleSources)),
};
};
@@ -321,9 +321,9 @@ export async function create(
breakpoints: JSON.stringify(
props.sourceBuild?.breakpoints ?? db.breakpoints.createValues()
),
styles: serializeStyles(props.sourceBuild?.styles ?? []),
styles: serializeStyles(new Map(props.sourceBuild?.styles)),
styleSources: serializeStyleSources(
new Map(props.sourceBuild?.styleSources ?? [])
new Map(props.sourceBuild?.styleSources)
),
isDev: props.env === "dev",
isProd: props.env === "prod",
+29 -18
View File
@@ -3,7 +3,13 @@ import { type Patch, applyPatches } from "immer";
import { prisma } from "@webstudio-is/prisma-client";
import type { Asset } from "@webstudio-is/asset-uploader";
import { formatAsset } from "@webstudio-is/asset-uploader/server";
import { StoredStyles, Styles } from "@webstudio-is/project-build";
import {
type StoredStyleDecl,
type StyleDecl,
getStyleDeclKey,
StoredStyles,
Styles,
} from "@webstudio-is/project-build";
import type { Build, Project } from "../shared/schema";
import {
authorizeProject,
@@ -11,7 +17,7 @@ import {
} from "@webstudio-is/trpc-interface/server";
const parseValue = (
styleValue: StoredStyles[number]["value"],
styleValue: StoredStyleDecl["value"],
assetsMap: Map<string, Asset>
) => {
if (styleValue.type === "image") {
@@ -67,14 +73,16 @@ export const parseStyles = async (
assetsMap.set(asset.id, formatAsset(asset));
}
const styles: Styles = storedStyles.map((styleDecl) => {
return {
styleSourceId: styleDecl.styleSourceId,
breakpointId: styleDecl.breakpointId,
property: styleDecl.property,
value: parseValue(styleDecl.value, assetsMap),
const styles: Styles = new Map();
for (const storedStyleDecl of storedStyles) {
const styleDecl = {
styleSourceId: storedStyleDecl.styleSourceId,
breakpointId: storedStyleDecl.breakpointId,
property: storedStyleDecl.property,
value: parseValue(storedStyleDecl.value, assetsMap),
};
});
styles.set(getStyleDeclKey(styleDecl), styleDecl);
}
return styles;
};
@@ -82,7 +90,7 @@ export const parseStyles = async (
/**
* prepare value to store in db
*/
const serializeValue = (styleValue: Styles[number]["value"]) => {
const serializeValue = (styleValue: StyleDecl["value"]) => {
if (styleValue.type === "image") {
return {
type: "image" as const,
@@ -97,14 +105,17 @@ const serializeValue = (styleValue: Styles[number]["value"]) => {
};
export const serializeStyles = (styles: Styles) => {
const storedStyles: StoredStyles = styles.map((styleDecl) => {
return {
breakpointId: styleDecl.breakpointId,
styleSourceId: styleDecl.styleSourceId,
property: styleDecl.property,
value: serializeValue(styleDecl.value),
};
});
const storedStyles: StoredStyles = Array.from(
styles.values(),
(styleDecl) => {
return {
breakpointId: styleDecl.breakpointId,
styleSourceId: styleDecl.styleSourceId,
property: styleDecl.property,
value: serializeValue(styleDecl.value),
};
}
);
return JSON.stringify(storedStyles);
};
+6 -2
View File
@@ -1,6 +1,10 @@
import { z, type ZodType } from "zod";
import type { Breakpoints } from "@webstudio-is/css-data";
import type { Styles, StyleSource } from "@webstudio-is/project-build";
import type {
StyleDecl,
StyleDeclKey,
StyleSource,
} from "@webstudio-is/project-build";
import type { Data } from "@webstudio-is/react-sdk";
const MIN_TITLE_LENGTH = 2;
@@ -99,7 +103,7 @@ export type Build = {
isProd: boolean;
pages: Pages;
breakpoints: Breakpoints;
styles: Styles;
styles: [StyleDeclKey, StyleDecl][];
styleSources: [StyleSource["id"], StyleSource][];
};
@@ -6,50 +6,71 @@ import type {
import { getStyleRules } from "./style-rules";
test("compute styles from different style sources", () => {
const styles: Styles = [
{
breakpointId: "a",
styleSourceId: "styleSource1",
property: "width",
value: { type: "unit", value: 10, unit: "px" },
},
{
breakpointId: "a",
styleSourceId: "styleSource2",
property: "display",
value: { type: "keyword", value: "block" },
},
{
breakpointId: "a",
styleSourceId: "styleSource4",
property: "color",
value: { type: "keyword", value: "green" },
},
{
breakpointId: "a",
styleSourceId: "styleSource4",
property: "width",
value: { type: "keyword", value: "min-content" },
},
{
breakpointId: "a",
styleSourceId: "styleSource3",
property: "color",
value: { type: "keyword", value: "red" },
},
{
breakpointId: "b",
styleSourceId: "styleSource5",
property: "color",
value: { type: "keyword", value: "orange" },
},
{
breakpointId: "a",
styleSourceId: "styleSource6",
property: "color",
value: { type: "keyword", value: "blue" },
},
];
const styles: Styles = new Map([
[
"styleSource1:a:width",
{
breakpointId: "a",
styleSourceId: "styleSource1",
property: "width",
value: { type: "unit", value: 10, unit: "px" },
},
],
[
"styleSource2:a:display",
{
breakpointId: "a",
styleSourceId: "styleSource2",
property: "display",
value: { type: "keyword", value: "block" },
},
],
[
"styleSource4:a:color",
{
breakpointId: "a",
styleSourceId: "styleSource4",
property: "color",
value: { type: "keyword", value: "green" },
},
],
[
"styleSource4:a:width",
{
breakpointId: "a",
styleSourceId: "styleSource4",
property: "width",
value: { type: "keyword", value: "min-content" },
},
],
[
"styleSource3:a:color",
{
breakpointId: "a",
styleSourceId: "styleSource3",
property: "color",
value: { type: "keyword", value: "red" },
},
],
[
"styleSource5:b:color",
{
breakpointId: "b",
styleSourceId: "styleSource5",
property: "color",
value: { type: "keyword", value: "orange" },
},
],
[
"styleSource6:a:color",
{
breakpointId: "a",
styleSourceId: "styleSource6",
property: "color",
value: { type: "keyword", value: "blue" },
},
],
]);
const styleSourceSelections: StyleSourceSelections = new Map([
[
"instance1",
@@ -1,5 +1,6 @@
import type { Breakpoint, Style } from "@webstudio-is/css-data";
import type {
StyleDecl,
Styles,
StyleSource,
StyleSourceSelections,
@@ -16,14 +17,14 @@ type StyleRule = {
* and group by instance and breakpoint
*/
export const getStyleRules = (
styles?: Styles,
styleSourceSelections?: StyleSourceSelections
styles: Styles,
styleSourceSelections: StyleSourceSelections
) => {
if (styles === undefined || styleSourceSelections === undefined) {
return [];
}
const stylesByStyleSourceId = new Map<StyleSource["id"], Styles>();
for (const styleDecl of styles) {
const stylesByStyleSourceId = new Map<StyleSource["id"], StyleDecl[]>();
for (const styleDecl of styles.values()) {
const { styleSourceId } = styleDecl;
let styleSourceStyles = stylesByStyleSourceId.get(styleSourceId);
// instance can be undefined when style is from other tree