mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 21:15:18 +09:00
Merge pull request #20 from webstudio-is/props-transactions
Props update/delete using transactions
This commit is contained in:
19 files changed
+371
-101
No files matched your search
@@ -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 { useManageProps } from "./shared/props";
|
||||
|
||||
const useElementsTree = () => {
|
||||
const [rootInstance] = useRootInstance();
|
||||
@@ -81,6 +82,7 @@ type DesignModeProps = {
|
||||
const DesignMode = ({ treeId }: DesignModeProps) => {
|
||||
useDragDropHandlers();
|
||||
useUpdateStyle();
|
||||
useManageProps();
|
||||
usePublishSelectedInstance({ treeId });
|
||||
useInsertInstance();
|
||||
useReparentInstance();
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
type UserPropsUpdates,
|
||||
useSubscribe,
|
||||
allUserPropsContainer,
|
||||
DeleteProp,
|
||||
} from "@webstudio-is/sdk";
|
||||
import { createTransaction, register } from "~/lib/sync-engine";
|
||||
import {
|
||||
updateAllUserPropsMutable,
|
||||
deletePropMutable,
|
||||
} from "~/shared/props-utils";
|
||||
|
||||
register("props", allUserPropsContainer);
|
||||
|
||||
export const useManageProps = () => {
|
||||
useSubscribe<"updateProps", UserPropsUpdates>(
|
||||
"updateProps",
|
||||
(userPropsUpdates) => {
|
||||
createTransaction([allUserPropsContainer], (allUserProps) => {
|
||||
updateAllUserPropsMutable(allUserProps, userPropsUpdates);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
useSubscribe<"deleteProp", DeleteProp>("deleteProp", (deleteProp) => {
|
||||
createTransaction([allUserPropsContainer], (allUserProps) => {
|
||||
deletePropMutable(allUserProps, deleteProp);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -69,7 +69,7 @@ export const usePropsLogic = ({
|
||||
publish<"deleteProp", DeleteProp>({
|
||||
type: "deleteProp",
|
||||
payload: {
|
||||
propsId: selectedInstanceData.props.id,
|
||||
instanceId: selectedInstanceData.id,
|
||||
propId: id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
type DeleteProp,
|
||||
type UserPropsUpdates,
|
||||
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";
|
||||
@@ -13,33 +9,19 @@ 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 }) => {
|
||||
useSubscribe<"syncChanges", Array<SyncItem>>(
|
||||
"syncChanges",
|
||||
(transactions) => {
|
||||
enqueue(() =>
|
||||
fetch(`/rest/props/delete-prop`, {
|
||||
fetch(`/rest/patch`, {
|
||||
method: "post",
|
||||
body: JSON.stringify({ propsId, propId }),
|
||||
body: JSON.stringify({
|
||||
transactions,
|
||||
treeId: project.devTreeId,
|
||||
projectId: project.id,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
useSubscribe<"syncChanges", Array<SyncItem>>("syncChanges", (queue) => {
|
||||
enqueue(() =>
|
||||
fetch(`/rest/patch/${project.devTreeId}`, {
|
||||
method: "post",
|
||||
body: JSON.stringify(queue),
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -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<SyncItem> = await request.json();
|
||||
type PatchData = {
|
||||
transactions: Array<SyncItem>;
|
||||
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" };
|
||||
@@ -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" };
|
||||
};
|
||||
@@ -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" };
|
||||
};
|
||||
@@ -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 (
|
||||
@@ -68,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,
|
||||
@@ -130,3 +112,35 @@ export const clone = async ({
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const patch = async (
|
||||
{ treeId }: { treeId: Tree["id"]; projectId: Project["id"] },
|
||||
patches: Array<Patch>
|
||||
) => {
|
||||
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<AllUserProps>(
|
||||
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,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -84,7 +84,10 @@ export const clone = async (id: string) => {
|
||||
return await create(tree.root);
|
||||
};
|
||||
|
||||
export const patchRoot = async (treeId: Tree["id"], patches: Array<Patch>) => {
|
||||
export const patchRoot = async (
|
||||
{ treeId }: { treeId: Tree["id"] },
|
||||
patches: Array<Patch>
|
||||
) => {
|
||||
const tree = await loadById(treeId);
|
||||
const root = applyPatches(tree.root, patches);
|
||||
await prisma.tree.update({
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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 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",
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./update-props";
|
||||
export * from "./delete-prop";
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 {},
|
||||
}
|
||||
`;
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user