This commit is contained in:
Rizky 2023-10-24 14:40:17 +07:00
parent 587b3611ca
commit 0a39588932
19 changed files with 291 additions and 60 deletions

View File

@ -142,6 +142,7 @@ export const _ = {
id: newcomp.id,
name: "",
props: {},
ref_ids: {},
},
} as IItem
);

View File

@ -5,32 +5,34 @@ export const SyncActionDefinition = {
"load": "2"
},
"comp": {
"list": "3",
"group": "4",
"load": "5"
"new": "3",
"list": "4",
"group": "5",
"load": "6"
},
"page": {
"list": "6",
"load": "7"
"list": "7",
"load": "8"
},
"yjs": {
"um": "8",
"sv_local": "9",
"diff_local": "10",
"sv_remote": "11"
"um": "9",
"sv_local": "10",
"diff_local": "11",
"sv_remote": "12"
}
};
export const SyncActionPaths = {
"0": "site.list",
"1": "site.group",
"2": "site.load",
"3": "comp.list",
"4": "comp.group",
"5": "comp.load",
"6": "page.list",
"7": "page.load",
"8": "yjs.um",
"9": "yjs.sv_local",
"10": "yjs.diff_local",
"11": "yjs.sv_remote"
"3": "comp.new",
"4": "comp.list",
"5": "comp.group",
"6": "comp.load",
"7": "page.list",
"8": "page.load",
"9": "yjs.um",
"10": "yjs.sv_local",
"11": "yjs.diff_local",
"12": "yjs.sv_remote"
};

View File

@ -4,6 +4,7 @@ import {
EPage,
ESite,
} from "../../../web/src/render/ed/logic/ed-global";
import { IItem } from "../../../web/src/utils/types/item";
/*
WARNING:
@ -22,8 +23,10 @@ export const SyncActions = {
load: async (id: string) => ({}) as ESite | void,
},
comp: {
new: async (arg: { group_id: string; item: IItem }) => {},
list: () => ({}) as Record<string, Exclude<component, "content_tree">>,
group: () => ({}) as Record<string, string[]>,
group: async (id_site: string) =>
({}) as Record<string, { id: string; name: string; comps: string[] }>,
load: async (id: string) => ({}) as EComp | void,
},
page: {

View File

@ -0,0 +1,31 @@
import { SAction } from "../actions";
import { SyncConnection } from "../type";
export const comp_group: SAction["comp"]["group"] = async function (
this: SyncConnection,
id_site
) {
const result: Awaited<ReturnType<SAction["comp"]["group"]>> = {};
const groups = await db.component_group.findMany({
where: {
component_site: {
some: {
id_site,
},
},
},
select: {
name: true,
id: true,
},
});
for (const g of groups) {
result[g.id] = {
id: g.id,
name: g.name,
comps: [],
};
}
return result;
};

View File

@ -1,21 +1,65 @@
import { syncronize } from "y-pojo";
import { docs } from "../entity/docs";
import { SAction } from "../actions";
import { conns } from "../entity/conn";
import { Y, docs } from "../entity/docs";
import { snapshot } from "../entity/snapshot";
import { user } from "../entity/user";
import { gzipAsync } from "../entity/zlib";
import { SyncConnection } from "../type";
import { sendWS } from "../sync-handler";
import { SyncConnection, SyncType } from "../type";
export const comp_load = async function (this: SyncConnection, id: string) {
export const comp_load: SAction["comp"]["load"] = async function (
this: SyncConnection,
id: string
) {
let snap = snapshot.get("comp", id);
let ydoc = docs.comp[id];
const conf = this.conf;
if (!conf) return undefined;
if (!snap || !ydoc) {
const createUndoManager = async (root: Y.Map<any>) => {
const um = new Y.UndoManager(root, {
ignoreRemoteMapChanges: true,
});
return um;
};
const attachOnUpdate = async (doc: Y.Doc, um: Y.UndoManager) => {
snapshot.set("comp", id, "id_doc", um.doc.clientID);
doc.on("update", async (update: Uint8Array, origin: any) => {
const bin = Y.encodeStateAsUpdate(doc);
snapshot.set("comp", id, "bin", bin);
const sv_local = await gzipAsync(update);
user.active.findAll({ comp_id: id }).map((e) => {
if (origin !== um) {
if (e.client_id === origin) return;
}
const ws = conns.get(e.client_id)?.ws;
if (ws)
sendWS(ws, {
type: SyncType.Event,
event: "remote_svlocal",
data: { type: "comp", sv_local, id },
});
});
});
};
const defaultActive = {
select: "" as "" | "comp" | "item" | "section" | "text",
};
if (!snap && !ydoc) {
const comp = await db.component.findFirst({ where: { id } });
if (comp) {
const doc = new Y.Doc();
let root = doc.getMap("map");
syncronize(root, { id, item: comp.content_tree });
syncronize(root, { id, root: comp.content_tree });
const um = new Y.UndoManager(root, { ignoreRemoteMapChanges: true });
const um = await createUndoManager(root);
docs.comp[id] = {
doc: doc as any,
id,
@ -23,13 +67,24 @@ export const comp_load = async function (this: SyncConnection, id: string) {
};
const bin = Y.encodeStateAsUpdate(doc);
await attachOnUpdate(doc, um);
snapshot.update({
bin,
id,
type: "comp",
name: comp.name,
url: "",
ts: Date.now(),
id_doc: doc.clientID,
});
user.active.add({
...defaultActive,
client_id: this.client_id,
user_id: this.user_id,
site_id: conf.site_id,
page_id: conf.page_id,
comp_id: comp.id,
});
return {
@ -40,22 +95,43 @@ export const comp_load = async function (this: SyncConnection, id: string) {
}
} else if (snap && !ydoc) {
const doc = new Y.Doc();
snapshot.set("comp", id, "id_doc", doc.clientID);
Y.applyUpdate(doc, snap.bin);
let root = doc.getMap("map");
const um = new Y.UndoManager(root, { ignoreRemoteMapChanges: true });
docs.page[id] = {
const um = await createUndoManager(root);
await attachOnUpdate(doc, um);
docs.comp[id] = {
doc: doc as any,
id,
um,
};
user.active.add({
...defaultActive,
client_id: this.client_id,
user_id: this.user_id,
site_id: conf.site_id,
page_id: conf.page_id,
comp_id: id,
});
return {
id: id,
name: snap.name,
snapshot: await gzipAsync(snap.bin),
};
} else if (snap && ydoc) {
user.active.add({
...defaultActive,
client_id: this.client_id,
user_id: this.user_id,
site_id: conf.site_id,
page_id: conf.page_id,
comp_id: id,
});
return {
id: snap.id,
name: snap.name,

View File

@ -0,0 +1,6 @@
import { SAction } from "../actions";
import { SyncConnection } from "../type";
export const comp_new: SAction["comp"]["new"] = async function (
this: SyncConnection
) {};

View File

@ -2,6 +2,7 @@ export * from "./site_load";
export * from "./site_group";
export * from "./page_load";
export * from "./comp_load";
export * from "./comp_group";
export * from "./yjs_um";
export * from "./yjs_sv_local";
export * from "./yjs_diff_local";

View File

@ -15,7 +15,10 @@ export const page_load: SAction["page"]["load"] = async function (
let snap = snapshot.get("page", id);
let ydoc = docs.page[id];
if (this.conf) this.conf.page_id = id;
const conf = this.conf;
if (!conf) return undefined;
conf.page_id = id;
const createUndoManager = async (root: Y.Map<any>) => {
const um = new Y.UndoManager(root, {

View File

@ -1,6 +1,5 @@
import { SAction } from "../actions";
import { Y, docs } from "../entity/docs";
import { snapshot } from "../entity/snapshot";
import { gunzipAsync } from "../entity/zlib";
import { SyncConnection } from "../type";

View File

@ -2,19 +2,47 @@ import { dir } from "dir";
import { RootDatabase, open } from "lmdb";
import { g } from "utils/global";
const emptySnapshot = {
type: "" as "" | "comp" | "page" | "site",
type EmptySnapshot = {
type: "";
id: string;
bin: Uint8Array;
id_doc: number;
name: string;
ts: number;
};
type CompSnapshot = {
type: "comp";
id: string;
bin: Uint8Array;
id_doc: number;
name: string;
ts: number;
};
type PageSnapshot = {
type: "page";
id: string;
bin: Uint8Array;
id_doc: number;
name: string;
ts: number;
url: string;
id_site: string;
};
type DocSnapshotMap = {
page: PageSnapshot;
comp: CompSnapshot;
"": EmptySnapshot;
};
export type DocSnapshot = EmptySnapshot | CompSnapshot | PageSnapshot;
const emptySnapshot: DocSnapshot = {
type: "",
id: "",
bin: new Uint8Array(),
id_doc: 0,
name: "",
ts: Date.now(),
};
export type DocSnapshot = typeof emptySnapshot & {
type: "page";
url: string;
id_site: string;
};
export const snapshot = {
_db: null as null | RootDatabase<DocSnapshot>,
@ -41,8 +69,8 @@ export const snapshot = {
}
return res as DocSnapshot;
},
get(type: string, id: string) {
return this.db.get(`${type}-${id}`);
get<K extends DocSnapshot["type"]>(type: K, id: string) {
return this.db.get(`${type}-${id}`) as DocSnapshotMap[K] | null;
},
async update(data: DocSnapshot) {
const id = `${data.type}-${data.id}`;
@ -50,7 +78,7 @@ export const snapshot = {
return true;
},
async set<T extends keyof DocSnapshot>(
type: string,
type: keyof DocSnapshotMap,
id: string,
key: T,
value: DocSnapshot[T]

View File

@ -15,6 +15,7 @@ export const user = {
user_id: string;
site_id: string;
page_id: string;
comp_id?: string;
select: "" | "comp" | "item" | "section" | "text";
},
"client_id"

View File

@ -6,6 +6,7 @@ import { edRoute } from "./logic/ed-route";
import { EdMain } from "./panel/main/main";
import { EdTree } from "./panel/tree/tree";
import { edUndoManager } from "./logic/ed-undo";
import { EdPopCompGroup } from "./panel/popup/comp-group";
export const EdBase = () => {
const p = useGlobal(EDGlobal, "EDITOR");
@ -35,6 +36,10 @@ export const EdBase = () => {
<EdTree />
<EdMain />
</div>
<>
<EdPopCompGroup />
</>
</div>
);
};

View File

@ -5,6 +5,7 @@ import { IItem, MItem } from "../../../utils/types/item";
import { DComp, DPage, IRoot } from "../../../utils/types/root";
import { ISection } from "../../../utils/types/section";
import { IText, MText } from "../../../utils/types/text";
import { SAction } from "../../../../../srv/ws/sync/actions";
const EmptySite = {
id: "",
@ -83,11 +84,12 @@ export const EDGlobal = {
doc: null as null | DComp,
item: null as null | IItem,
list: {} as Record<string, { cur: EComp; doc: DComp }>,
group: {} as Record<string, Awaited<ReturnType<SAction["comp"]["group"]>>>,
},
ui: {
tree: {
search: "",
searchMode: {
search_mode: {
Name: true,
JS: false,
HTML: false,
@ -96,13 +98,11 @@ export const EDGlobal = {
open: {} as Record<string, string[]>,
},
popup: {
comp: null as null | true | ((comp_id: string) => void | Promise<void>),
compGroup: null as
| null
| true
| {
event: React.MouseEvent<HTMLElement, MouseEvent>;
pick: (group_id: string) => void | Promise<void>;
comp: null as null | ((comp_id: string) => void | Promise<void>),
comp_group: null as null | {
mouse_event: React.MouseEvent<HTMLElement, MouseEvent>;
on_pick?: (group_id: string) => void | Promise<void>;
on_close?: () => void | Promise<void>;
},
},
},

View File

@ -0,0 +1,60 @@
import { useGlobal } from "web-utils";
import { Menu, MenuItem } from "../../../../utils/ui/context-menu";
import { EDGlobal } from "../../logic/ed-global";
import { useEffect } from "react";
import { Loading } from "../../../../utils/ui/loading";
export const EdPopCompGroup = () => {
const p = useGlobal(EDGlobal, "EDITOR");
useEffect(() => {
(async () => {
if (!p.comp.group[p.site.id]) {
p.comp.group[p.site.id] = await p.sync.comp.group(p.site.id);
}
console.log(p.comp.group[p.site.id]);
p.render();
})();
}, []);
if (!p.ui.popup.comp_group) return null;
const pop = p.ui.popup.comp_group;
const group = p.comp.group[p.site.id];
return (
<Menu
mouseEvent={pop.mouse_event}
onClose={() => {
p.ui.popup.comp_group = null;
p.render();
if (pop.on_close) pop.on_close();
}}
>
<MenuItem
disabled
label={
!group ? (
<div className="bg-white relative w-[150px] h-[20px]">
<div className="absolute inset-0 -mx-[10px] -my-[2px]">
<Loading />
</div>
</div>
) : (
<div className="text-slate-500">Choose Component Group:</div>
)
}
/>
{Object.values(group || {})
.filter((g) => g.name !== "__TRASH__")
.sort((a, b) => (a.name > b.name ? 1 : -1))
.map((g) => (
<MenuItem
onClick={() => {
p.ui.popup.comp_group?.on_pick?.(g.id);
}}
label={<div className="pl-2">{g.name}</div>}
key={g.id}
/>
))}
</Menu>
);
};

View File

@ -1,8 +1,19 @@
import { IItem } from "../../../../../../../utils/types/item";
import { PG } from "../../../../../logic/ed-global";
export const edActionNewComp = (p: PG, item: IItem) => {
export const edActionNewComp = (
p: PG,
item: IItem,
e: React.MouseEvent<HTMLElement, MouseEvent>
) => {
const mitem = p.page.meta[item.id].mitem;
if (mitem) {
p.ui.popup.comp_group = {
mouse_event: e,
async on_pick(group_id) {
await p.sync.comp.new({ group_id, item });
},
};
p.render();
}
};

View File

@ -57,7 +57,10 @@ export const EdTreeCtxMenu = ({
if (!item) {
return (
<Menu mouseEvent={event} onClose={onClose}>
<MenuItem label={<div className="text-gray-400">Unavailable</div>} />
<MenuItem
disabled
label={<div className="text-slate-500">Unavailable</div>}
/>
</Menu>
);
}
@ -79,7 +82,7 @@ export const EdTreeCtxMenu = ({
{type === "item" && !comp?.id && (
<MenuItem
label="Create Component"
onClick={() => edActionNewComp(p, item)}
onClick={(e) => edActionNewComp(p, item, e)}
/>
)}
{!item.hidden && (

View File

@ -12,7 +12,8 @@ export const indentHook = (
let shouldOpen = open[p.page.cur.id] || [];
let meta = p.page.meta[active.item_id];
const cur = p.page.meta[active.item_id];
let meta = p.page.meta[cur.parent_item.id];
while (meta) {
if (meta.item.id) shouldOpen.push(meta.item.id);
meta = p.page.meta[meta.parent_item.id];

View File

@ -66,7 +66,7 @@ export const EdTreeSearch = () => {
{(local.focus || local.hover || p.ui.tree.search) && (
<div className="p-1 bg-white text-xs border-t flex space-x-1 justify-between">
<div className="flex space-x-1">
{Object.entries(p.ui.tree.searchMode).map(([name, active]) => {
{Object.entries(p.ui.tree.search_mode).map(([name, active]) => {
return (
<div
className={cx(
@ -74,7 +74,7 @@ export const EdTreeSearch = () => {
active ? "bg-blue-500 text-white" : "hover:bg-blue-100"
)}
onClick={() => {
(p.ui.tree.searchMode as any)[name] = !active;
(p.ui.tree.search_mode as any)[name] = !active;
local.render();
local.sref?.focus();
}}
@ -94,7 +94,7 @@ export const EdTreeSearch = () => {
export const doTreeSearch = (p: PG) => {
let tree: Record<string, { idx: number; node: NodeModel<EdMeta> }> = {};
if (p.ui.tree.searchMode.Name) {
if (p.ui.tree.search_mode.Name) {
const [idxs, info] = uf.search(
p.page.tree.map((e) => e.text),
p.ui.tree.search
@ -162,7 +162,7 @@ export const doTreeSearch = (p: PG) => {
if (item) {
const js = item.adv?.js;
if (js) {
if (p.ui.tree.searchMode.JS) {
if (p.ui.tree.search_mode.JS) {
if ((js as string).toLowerCase().includes(search)) {
tree[item.id] = { idx: i++, node: { ...row, parent: "root" } };
}
@ -170,7 +170,7 @@ export const doTreeSearch = (p: PG) => {
}
const css = item.adv?.css;
if (css) {
if (p.ui.tree.searchMode.CSS) {
if (p.ui.tree.search_mode.CSS) {
if (css.toString().toLowerCase().includes(search)) {
tree[item.id] = { idx: i++, node: { ...row, parent: "root" } };
}
@ -179,7 +179,7 @@ export const doTreeSearch = (p: PG) => {
const html = item.adv?.html;
if (html) {
if (p.ui.tree.searchMode.HTML) {
if (p.ui.tree.search_mode.HTML) {
if (html.toString().toLowerCase().includes(search)) {
tree[item.id] = { idx: i++, node: { ...row, parent: "root" } };
}

View File

@ -52,7 +52,7 @@ export const MenuItem = forwardRef<
interface Props {
label?: string;
nested?: boolean;
mouseEvent: React.MouseEvent<HTMLDivElement, MouseEvent>;
mouseEvent: React.MouseEvent<HTMLElement, MouseEvent>;
onClose: () => void;
}