From 17203aa47c521b650da70afc898815bb40085c75 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 1 Apr 2022 17:03:33 +0100 Subject: [PATCH 1/2] update props using transaction --- app/canvas/canvas.tsx | 2 + app/canvas/shared/props.ts | 20 +++++ app/designer/features/sync/use-sync.ts | 38 ++++----- .../rest/{patch.$treeId.ts => patch.ts} | 21 +++-- app/routes/rest/props.update.ts | 9 --- app/shared/db/props.server.ts | 34 ++++++++ app/shared/db/tree.server.ts | 5 +- .../__snapshots__/update-props.test.ts.snap | 80 +++++++++++++++++++ app/shared/props-utils/index.ts | 1 + app/shared/props-utils/update-props.test.ts | 80 +++++++++++++++++++ app/shared/props-utils/update-props.ts | 25 ++++++ .../reparent-instance.test.ts.snap | 24 ------ 12 files changed, 278 insertions(+), 61 deletions(-) create mode 100644 app/canvas/shared/props.ts rename app/routes/rest/{patch.$treeId.ts => patch.ts} (51%) delete mode 100644 app/routes/rest/props.update.ts create mode 100644 app/shared/props-utils/__snapshots__/update-props.test.ts.snap create mode 100644 app/shared/props-utils/index.ts create mode 100644 app/shared/props-utils/update-props.test.ts create mode 100644 app/shared/props-utils/update-props.ts delete mode 100644 app/shared/tree-utils/__snapshots__/reparent-instance.test.ts.snap diff --git a/app/canvas/canvas.tsx b/app/canvas/canvas.tsx index de60845781..de615a52d5 100644 --- a/app/canvas/canvas.tsx +++ b/app/canvas/canvas.tsx @@ -32,6 +32,7 @@ import { WrapperComponentDev } from "./features/wrapper-component"; import { rootInstanceContainer, useRootInstance } from "./shared/nano-values"; import { usePeriodicSync } from "./shared/use-periodic-sync"; import { createTransaction } from "~/lib/sync-engine"; +import { useUpdateProps } from "./shared/props"; const useElementsTree = () => { const [rootInstance] = useRootInstance(); @@ -81,6 +82,7 @@ type DesignModeProps = { const DesignMode = ({ treeId }: DesignModeProps) => { useDragDropHandlers(); useUpdateStyle(); + useUpdateProps(); usePublishSelectedInstance({ treeId }); useInsertInstance(); useReparentInstance(); diff --git a/app/canvas/shared/props.ts b/app/canvas/shared/props.ts new file mode 100644 index 0000000000..9a1d7d2792 --- /dev/null +++ b/app/canvas/shared/props.ts @@ -0,0 +1,20 @@ +import { + type UserPropsUpdates, + useSubscribe, + allUserPropsContainer, +} from "@webstudio-is/sdk"; +import { createTransaction, register } from "~/lib/sync-engine"; +import { updateAllUserPropsMutable } from "~/shared/props-utils"; + +register("props", allUserPropsContainer); + +export const useUpdateProps = () => { + useSubscribe<"updateProps", UserPropsUpdates>( + "updateProps", + (userPropsUpdates) => { + createTransaction([allUserPropsContainer], (allUserProps) => { + updateAllUserPropsMutable(allUserProps, userPropsUpdates); + }); + } + ); +}; diff --git a/app/designer/features/sync/use-sync.ts b/app/designer/features/sync/use-sync.ts index 8f3a227159..9b6868f751 100644 --- a/app/designer/features/sync/use-sync.ts +++ b/app/designer/features/sync/use-sync.ts @@ -1,8 +1,4 @@ -import { - type DeleteProp, - type UserPropsUpdates, - type Project, -} from "@webstudio-is/sdk"; +import { type DeleteProp, type Project } from "@webstudio-is/sdk"; import type { Config } from "~/config"; import { useSubscribe } from "~/designer/features/canvas-iframe"; import { enqueue } from "./queue"; @@ -13,15 +9,6 @@ import { type SyncItem } from "~/lib/sync-engine"; // and backend fetches and updates big objects, so if we send quickly, // we end up overwriting things export const useSync = ({ project }: { config: Config; project: Project }) => { - useSubscribe<"updateProps", UserPropsUpdates>("updateProps", (update) => { - enqueue(() => - fetch(`/rest/props/update`, { - method: "post", - body: JSON.stringify(update), - }) - ); - }); - useSubscribe<"deleteProp", DeleteProp>( "deleteProp", ({ propsId, propId }) => { @@ -34,12 +21,19 @@ export const useSync = ({ project }: { config: Config; project: Project }) => { } ); - useSubscribe<"syncChanges", Array>("syncChanges", (queue) => { - enqueue(() => - fetch(`/rest/patch/${project.devTreeId}`, { - method: "post", - body: JSON.stringify(queue), - }) - ); - }); + useSubscribe<"syncChanges", Array>( + "syncChanges", + (transactions) => { + enqueue(() => + fetch(`/rest/patch`, { + method: "post", + body: JSON.stringify({ + transactions, + treeId: project.devTreeId, + projectId: project.id, + }), + }) + ); + } + ); }; diff --git a/app/routes/rest/patch.$treeId.ts b/app/routes/rest/patch.ts similarity index 51% rename from app/routes/rest/patch.$treeId.ts rename to app/routes/rest/patch.ts index b14b3aec23..cb668389ba 100644 --- a/app/routes/rest/patch.$treeId.ts +++ b/app/routes/rest/patch.ts @@ -1,14 +1,25 @@ import { type ActionFunction } from "remix"; import * as db from "~/shared/db"; import { type SyncItem } from "~/lib/sync-engine"; +import { type Project, type Tree } from "@webstudio-is/sdk"; -const updaters = { root: db.tree.patchRoot }; +const updaters = { + root: db.tree.patchRoot, + props: db.props.patch, +}; type UpdaterKey = keyof typeof updaters; -export const action: ActionFunction = async ({ request, params }) => { - if (params.treeId === undefined) return { errors: "Tree id required" }; - const transactions: Array = await request.json(); +type PatchData = { + transactions: Array; + treeId: Tree["id"]; + projectId: Project["id"]; +}; + +export const action: ActionFunction = async ({ request }) => { + const { treeId, projectId, transactions }: PatchData = await request.json(); + if (treeId === undefined) return { errors: "Tree id required" }; + if (projectId === undefined) return { errors: "Project id required" }; // @todo parallelize the updates // currently not possible because we fetch the entire tree // and parallelized updates will cause unpredictable side effects @@ -18,7 +29,7 @@ export const action: ActionFunction = async ({ request, params }) => { if (namespace in updaters === false) { return { errors: `Unknown namespace "${namespace}"` }; } - await updaters[namespace as UpdaterKey](params.treeId, patches); + await updaters[namespace as UpdaterKey]({ treeId, projectId }, patches); } } return { status: "ok" }; diff --git a/app/routes/rest/props.update.ts b/app/routes/rest/props.update.ts deleted file mode 100644 index a10d9132b9..0000000000 --- a/app/routes/rest/props.update.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type ActionFunction } from "remix"; -import * as db from "~/shared/db"; - -export const action: ActionFunction = async ({ request }) => { - // @todo types - const update = await request.json(); - await db.props.update(update); - return { status: "ok" }; -}; diff --git a/app/shared/db/props.server.ts b/app/shared/db/props.server.ts index 5d463330f3..475d61987d 100644 --- a/app/shared/db/props.server.ts +++ b/app/shared/db/props.server.ts @@ -5,7 +5,9 @@ import { type Project, type InstanceProps, type Tree, + type AllUserProps, } from "@webstudio-is/sdk"; +import { applyPatches, type Patch } from "immer"; import { prisma, PrismaClientKnownRequestError } from "./prisma.server"; export const loadByProject = async ( @@ -130,3 +132,35 @@ export const clone = async ({ data, }); }; + +export const patch = async ( + { treeId }: { treeId: Tree["id"]; projectId: Project["id"] }, + patches: Array +) => { + const allProps = await loadByTreeId(treeId); + + // We should get rid of this by applying patches on the client to the original + // model instead of the instanceId map. + // The map is handy for accessing props, but probably should be just cached interface, not the main data structure. + const allPropsMapByInstanceId = allProps.reduce((acc, prop) => { + acc[prop.instanceId] = prop; + return acc; + }, {} as AllUserProps); + const nextProps = applyPatches( + allPropsMapByInstanceId, + patches + ); + + await Promise.all( + Object.values(nextProps).map( + async ({ id, instanceId, treeId, props }) => + await prisma.instanceProps.upsert({ + where: { id: id }, + create: { id: id, instanceId, treeId, props }, + update: { + props, + }, + }) + ) + ); +}; diff --git a/app/shared/db/tree.server.ts b/app/shared/db/tree.server.ts index b517efd583..3e0c6244b1 100644 --- a/app/shared/db/tree.server.ts +++ b/app/shared/db/tree.server.ts @@ -84,7 +84,10 @@ export const clone = async (id: string) => { return await create(tree.root); }; -export const patchRoot = async (treeId: Tree["id"], patches: Array) => { +export const patchRoot = async ( + { treeId }: { treeId: Tree["id"] }, + patches: Array +) => { const tree = await loadById(treeId); const root = applyPatches(tree.root, patches); await prisma.tree.update({ diff --git a/app/shared/props-utils/__snapshots__/update-props.test.ts.snap b/app/shared/props-utils/__snapshots__/update-props.test.ts.snap new file mode 100644 index 0000000000..2c0a5a7ab4 --- /dev/null +++ b/app/shared/props-utils/__snapshots__/update-props.test.ts.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Update props with added prop 1`] = ` +Object { + "instanceId": Object { + "id": "id", + "instanceId": "instanceId", + "props": Array [ + Object { + "id": "propId", + "prop": "a", + "value": "1", + }, + ], + "treeId": "treeId", + }, +} +`; + +exports[`Update props with deleted props 1`] = ` +Object { + "instanceId": Object { + "id": "id", + "instanceId": "instanceId", + "props": Array [ + Object { + "id": "propId", + "prop": "a", + "value": "1", + }, + ], + "treeId": "treeId", + }, +} +`; + +exports[`Update props with empty updates 1`] = ` +Object { + "instanceId": Object { + "id": "id", + "instanceId": "instanceId", + "props": Array [], + "treeId": "treeId", + }, +} +`; + +exports[`Update props with renamed prop name 1`] = ` +Object { + "instanceId": Object { + "id": "id", + "instanceId": "instanceId", + "props": Array [ + Object { + "id": "propId", + "prop": "b", + "value": "1", + }, + ], + "treeId": "treeId", + }, +} +`; + +exports[`Update props with updated prop value 1`] = ` +Object { + "instanceId": Object { + "id": "id", + "instanceId": "instanceId", + "props": Array [ + Object { + "id": "propId", + "prop": "a", + "value": "2", + }, + ], + "treeId": "treeId", + }, +} +`; diff --git a/app/shared/props-utils/index.ts b/app/shared/props-utils/index.ts new file mode 100644 index 0000000000..234a81bdb5 --- /dev/null +++ b/app/shared/props-utils/index.ts @@ -0,0 +1 @@ +export * from "./update-props"; diff --git a/app/shared/props-utils/update-props.test.ts b/app/shared/props-utils/update-props.test.ts new file mode 100644 index 0000000000..c17b2a80af --- /dev/null +++ b/app/shared/props-utils/update-props.test.ts @@ -0,0 +1,80 @@ +import { type AllUserProps, type UserPropsUpdates } from "@webstudio-is/sdk"; +import { updateAllUserPropsMutable } from "./update-props"; + +describe("Update props", () => { + test("with empty updates", () => { + const propsMap: AllUserProps = { + instanceId: { + id: "id", + instanceId: "instanceId", + treeId: "treeId", + props: [], + }, + }; + const update: UserPropsUpdates = { + propsId: "id", + instanceId: "instanceId", + treeId: "treeId", + updates: [], + }; + updateAllUserPropsMutable(propsMap, update); + expect(propsMap).toMatchSnapshot(); + }); + + test("with added prop", () => { + const propsMap: AllUserProps = { + instanceId: { + id: "id", + instanceId: "instanceId", + treeId: "treeId", + props: [], + }, + }; + const update: UserPropsUpdates = { + propsId: "id", + instanceId: "instanceId", + treeId: "treeId", + updates: [{ id: "propId", prop: "a", value: "1" }], + }; + updateAllUserPropsMutable(propsMap, update); + expect(propsMap).toMatchSnapshot(); + }); + + test("with updated prop value", () => { + const propsMap: AllUserProps = { + instanceId: { + id: "id", + instanceId: "instanceId", + treeId: "treeId", + props: [{ id: "propId", prop: "a", value: "1" }], + }, + }; + const update: UserPropsUpdates = { + propsId: "id", + instanceId: "instanceId", + treeId: "treeId", + updates: [{ id: "propId", prop: "a", value: "2" }], + }; + updateAllUserPropsMutable(propsMap, update); + expect(propsMap).toMatchSnapshot(); + }); + + test("with renamed prop name", () => { + const propsMap: AllUserProps = { + instanceId: { + id: "id", + instanceId: "instanceId", + treeId: "treeId", + props: [{ id: "propId", prop: "a", value: "1" }], + }, + }; + const update: UserPropsUpdates = { + propsId: "id", + instanceId: "instanceId", + treeId: "treeId", + updates: [{ id: "propId", prop: "b", value: "1" }], + }; + updateAllUserPropsMutable(propsMap, update); + expect(propsMap).toMatchSnapshot(); + }); +}); diff --git a/app/shared/props-utils/update-props.ts b/app/shared/props-utils/update-props.ts new file mode 100644 index 0000000000..63c6ef3d7c --- /dev/null +++ b/app/shared/props-utils/update-props.ts @@ -0,0 +1,25 @@ +import { type AllUserProps, type UserPropsUpdates } from "@webstudio-is/sdk"; + +export const updateAllUserPropsMutable = ( + allUserProps: AllUserProps, + { instanceId, propsId, treeId, updates }: UserPropsUpdates +) => { + if (instanceId in allUserProps === false) { + allUserProps[instanceId] = { + id: propsId, + instanceId, + treeId, + props: [], + }; + } + const instanceProps = allUserProps[instanceId]; + for (const update of updates) { + const prop = instanceProps.props.find(({ id }) => id === update.id); + if (prop === undefined) { + instanceProps.props.push(update); + } else { + prop.prop = update.prop; + prop.value = update.value; + } + } +}; diff --git a/app/shared/tree-utils/__snapshots__/reparent-instance.test.ts.snap b/app/shared/tree-utils/__snapshots__/reparent-instance.test.ts.snap deleted file mode 100644 index 7a84f997a5..0000000000 --- a/app/shared/tree-utils/__snapshots__/reparent-instance.test.ts.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Reparent instance div 1 > div 2, div 3 => div 1 > div 2 > div 3 1`] = ` -Object { - "children": Array [ - Object { - "children": Array [ - Object { - "children": Array [], - "component": "Box", - "id": "3", - "style": Object {}, - }, - ], - "component": "Box", - "id": "2", - "style": Object {}, - }, - ], - "component": "Box", - "id": "1", - "style": Object {}, -} -`; From a9bcc164deeedf8f629a41ddfe100deac003e68a Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 1 Apr 2022 22:16:15 +0100 Subject: [PATCH 2/2] delete props using transaction --- app/canvas/canvas.tsx | 4 +- app/canvas/shared/props.ts | 14 +++++- .../settings-panel/use-props-logic.ts | 2 +- app/designer/features/sync/use-sync.ts | 14 +----- app/routes/rest/props.delete-prop.ts | 8 ---- app/shared/db/props.server.ts | 20 -------- .../__snapshots__/delete-prop.test.ts.snap | 40 ++++++++++++++++ .../__snapshots__/update-props.test.ts.snap | 17 ------- app/shared/props-utils/delete-prop.test.ts | 46 +++++++++++++++++++ app/shared/props-utils/delete-prop.ts | 13 ++++++ app/shared/props-utils/index.ts | 1 + package.json | 2 +- yarn.lock | 8 ++-- 13 files changed, 121 insertions(+), 68 deletions(-) delete mode 100644 app/routes/rest/props.delete-prop.ts create mode 100644 app/shared/props-utils/__snapshots__/delete-prop.test.ts.snap create mode 100644 app/shared/props-utils/delete-prop.test.ts create mode 100644 app/shared/props-utils/delete-prop.ts diff --git a/app/canvas/canvas.tsx b/app/canvas/canvas.tsx index de615a52d5..f58828b181 100644 --- a/app/canvas/canvas.tsx +++ b/app/canvas/canvas.tsx @@ -32,7 +32,7 @@ import { WrapperComponentDev } from "./features/wrapper-component"; import { rootInstanceContainer, useRootInstance } from "./shared/nano-values"; import { usePeriodicSync } from "./shared/use-periodic-sync"; import { createTransaction } from "~/lib/sync-engine"; -import { useUpdateProps } from "./shared/props"; +import { useManageProps } from "./shared/props"; const useElementsTree = () => { const [rootInstance] = useRootInstance(); @@ -82,7 +82,7 @@ type DesignModeProps = { const DesignMode = ({ treeId }: DesignModeProps) => { useDragDropHandlers(); useUpdateStyle(); - useUpdateProps(); + useManageProps(); usePublishSelectedInstance({ treeId }); useInsertInstance(); useReparentInstance(); diff --git a/app/canvas/shared/props.ts b/app/canvas/shared/props.ts index 9a1d7d2792..58cbd65460 100644 --- a/app/canvas/shared/props.ts +++ b/app/canvas/shared/props.ts @@ -2,13 +2,17 @@ import { type UserPropsUpdates, useSubscribe, allUserPropsContainer, + DeleteProp, } from "@webstudio-is/sdk"; import { createTransaction, register } from "~/lib/sync-engine"; -import { updateAllUserPropsMutable } from "~/shared/props-utils"; +import { + updateAllUserPropsMutable, + deletePropMutable, +} from "~/shared/props-utils"; register("props", allUserPropsContainer); -export const useUpdateProps = () => { +export const useManageProps = () => { useSubscribe<"updateProps", UserPropsUpdates>( "updateProps", (userPropsUpdates) => { @@ -17,4 +21,10 @@ export const useUpdateProps = () => { }); } ); + + useSubscribe<"deleteProp", DeleteProp>("deleteProp", (deleteProp) => { + createTransaction([allUserPropsContainer], (allUserProps) => { + deletePropMutable(allUserProps, deleteProp); + }); + }); }; diff --git a/app/designer/features/settings-panel/use-props-logic.ts b/app/designer/features/settings-panel/use-props-logic.ts index c37c103863..7321ec1a3a 100644 --- a/app/designer/features/settings-panel/use-props-logic.ts +++ b/app/designer/features/settings-panel/use-props-logic.ts @@ -69,7 +69,7 @@ export const usePropsLogic = ({ publish<"deleteProp", DeleteProp>({ type: "deleteProp", payload: { - propsId: selectedInstanceData.props.id, + instanceId: selectedInstanceData.id, propId: id, }, }); diff --git a/app/designer/features/sync/use-sync.ts b/app/designer/features/sync/use-sync.ts index 9b6868f751..cac3c8a049 100644 --- a/app/designer/features/sync/use-sync.ts +++ b/app/designer/features/sync/use-sync.ts @@ -1,4 +1,4 @@ -import { type DeleteProp, type Project } from "@webstudio-is/sdk"; +import { type Project } from "@webstudio-is/sdk"; import type { Config } from "~/config"; import { useSubscribe } from "~/designer/features/canvas-iframe"; import { enqueue } from "./queue"; @@ -9,18 +9,6 @@ import { type SyncItem } from "~/lib/sync-engine"; // and backend fetches and updates big objects, so if we send quickly, // we end up overwriting things export const useSync = ({ project }: { config: Config; project: Project }) => { - useSubscribe<"deleteProp", DeleteProp>( - "deleteProp", - ({ propsId, propId }) => { - enqueue(() => - fetch(`/rest/props/delete-prop`, { - method: "post", - body: JSON.stringify({ propsId, propId }), - }) - ); - } - ); - useSubscribe<"syncChanges", Array>( "syncChanges", (transactions) => { diff --git a/app/routes/rest/props.delete-prop.ts b/app/routes/rest/props.delete-prop.ts deleted file mode 100644 index 3a7995b4b9..0000000000 --- a/app/routes/rest/props.delete-prop.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type ActionFunction } from "remix"; -import * as db from "~/shared/db"; - -export const action: ActionFunction = async ({ request }) => { - const data = await request.json(); - await db.props.deleteProp(data); - return { status: "ok" }; -}; diff --git a/app/shared/db/props.server.ts b/app/shared/db/props.server.ts index 475d61987d..cbbe311f59 100644 --- a/app/shared/db/props.server.ts +++ b/app/shared/db/props.server.ts @@ -70,26 +70,6 @@ export const update = async ({ }); }; -export const deleteProp = async ({ - propsId, - propId, -}: { - propsId: InstanceProps["id"]; - propId: UserProp["id"]; -}) => { - // @todo update in one command, remove queueing logic on the ui - // as of prisma client v3.10.0 updating composite types like in this doc didn't work - // https://www.prisma.io/docs/concepts/components/prisma-client/composite-types - const props = (await loadById(propsId))?.props ?? []; - const index = props.findIndex((prop) => prop.id == propId); - if (index === -1) return; - props.splice(index, 1); - await prisma.instanceProps.update({ - where: { id: propsId }, - data: { props }, - }); -}; - export const deleteProps = async ({ instanceId, treeId, diff --git a/app/shared/props-utils/__snapshots__/delete-prop.test.ts.snap b/app/shared/props-utils/__snapshots__/delete-prop.test.ts.snap new file mode 100644 index 0000000000..1debedd303 --- /dev/null +++ b/app/shared/props-utils/__snapshots__/delete-prop.test.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Delete prop found and deleted 1`] = ` +Object { + "instanceId": Object { + "id": "id", + "instanceId": "instanceId", + "props": Array [ + Object { + "id": "propId1", + "prop": "a", + "value": "1", + }, + ], + "treeId": "treeId", + }, +} +`; + +exports[`Delete prop not found and not deleted 1`] = ` +Object { + "instanceId": Object { + "id": "id", + "instanceId": "instanceId", + "props": Array [ + Object { + "id": "propId", + "prop": "b", + "value": "1", + }, + Object { + "id": "propId1", + "prop": "a", + "value": "1", + }, + ], + "treeId": "treeId", + }, +} +`; diff --git a/app/shared/props-utils/__snapshots__/update-props.test.ts.snap b/app/shared/props-utils/__snapshots__/update-props.test.ts.snap index 2c0a5a7ab4..2712a36a36 100644 --- a/app/shared/props-utils/__snapshots__/update-props.test.ts.snap +++ b/app/shared/props-utils/__snapshots__/update-props.test.ts.snap @@ -17,23 +17,6 @@ Object { } `; -exports[`Update props with deleted props 1`] = ` -Object { - "instanceId": Object { - "id": "id", - "instanceId": "instanceId", - "props": Array [ - Object { - "id": "propId", - "prop": "a", - "value": "1", - }, - ], - "treeId": "treeId", - }, -} -`; - exports[`Update props with empty updates 1`] = ` Object { "instanceId": Object { diff --git a/app/shared/props-utils/delete-prop.test.ts b/app/shared/props-utils/delete-prop.test.ts new file mode 100644 index 0000000000..98ad18490a --- /dev/null +++ b/app/shared/props-utils/delete-prop.test.ts @@ -0,0 +1,46 @@ +import { type AllUserProps } from "@webstudio-is/sdk"; +import { deletePropMutable } from "./delete-prop"; + +describe("Delete prop", () => { + test("found and deleted", () => { + const propsMap: AllUserProps = { + instanceId: { + id: "id", + instanceId: "instanceId", + treeId: "treeId", + props: [ + { id: "propId", prop: "b", value: "1" }, + { id: "propId1", prop: "a", value: "1" }, + ], + }, + }; + + const isDeleted = deletePropMutable(propsMap, { + instanceId: "instanceId", + propId: "propId", + }); + expect(propsMap).toMatchSnapshot(); + expect(isDeleted).toBeTruthy(); + }); + + test("not found and not deleted", () => { + const propsMap: AllUserProps = { + instanceId: { + id: "id", + instanceId: "instanceId", + treeId: "treeId", + props: [ + { id: "propId", prop: "b", value: "1" }, + { id: "propId1", prop: "a", value: "1" }, + ], + }, + }; + + const isDeleted = deletePropMutable(propsMap, { + instanceId: "instanceId", + propId: "notFound", + }); + expect(propsMap).toMatchSnapshot(); + expect(isDeleted).toBeFalsy(); + }); +}); diff --git a/app/shared/props-utils/delete-prop.ts b/app/shared/props-utils/delete-prop.ts new file mode 100644 index 0000000000..947bd79f21 --- /dev/null +++ b/app/shared/props-utils/delete-prop.ts @@ -0,0 +1,13 @@ +import { type AllUserProps, type DeleteProp } from "@webstudio-is/sdk"; + +export const deletePropMutable = ( + allProps: AllUserProps, + deleteProp: DeleteProp +) => { + const prop = allProps[deleteProp.instanceId]; + if (prop === undefined) return false; + const index = prop.props.findIndex(({ id }) => id === deleteProp.propId); + if (index === -1) return false; + prop.props.splice(index, 1); + return true; +}; diff --git a/app/shared/props-utils/index.ts b/app/shared/props-utils/index.ts index 234a81bdb5..e76c58df03 100644 --- a/app/shared/props-utils/index.ts +++ b/app/shared/props-utils/index.ts @@ -1 +1,2 @@ export * from "./update-props"; +export * from "./delete-prop"; diff --git a/package.json b/package.json index 15be975160..52552c8b42 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@remix-run/vercel": "^1.2.3", "@stitches/react": "^1.2.6", "@stitches/stringify": "^1.2.6", - "@webstudio-is/sdk": "^0.1.20", + "@webstudio-is/sdk": "^0.1.21", "bson-objectid": "^2.0.2", "camelcase": "^6.3.0", "hyphenate-style-name": "^1.0.4", diff --git a/yarn.lock b/yarn.lock index 7683e2e17e..623e804fb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1820,10 +1820,10 @@ dependencies: web-streams-polyfill "^3.1.1" -"@webstudio-is/sdk@^0.1.20": - version "0.1.20" - resolved "https://registry.yarnpkg.com/@webstudio-is/sdk/-/sdk-0.1.20.tgz#134fcc2eb166a0c0b648fac4fb008c98666f88c6" - integrity sha512-WI379pIabj6WU05qaP9MeUFE+9uCQu8uoEAXkxQ9mjkFpQUNY8lIn3ncsvmxZvyoKCaFIZ2UBAs2Pb7Q6fwSHQ== +"@webstudio-is/sdk@^0.1.21": + version "0.1.21" + resolved "https://registry.yarnpkg.com/@webstudio-is/sdk/-/sdk-0.1.21.tgz#245f48b4be4a72524338c9017afecba0ff8bfaef" + integrity sha512-pafxgtw6YwxOUKh1cyazurFA565xBjqJMpb6h6AaBNErbwJtTzTb7QFVz04skBdEMOnuhCACvPqp+tJU2XEP+w== dependencies: "@prisma/client" "^3.10.0" "@stitches/react" "^1.2.7"