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, id: newcomp.id,
name: "", name: "",
props: {}, props: {},
ref_ids: {},
}, },
} as IItem } as IItem
); );

View File

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

View File

@ -4,6 +4,7 @@ import {
EPage, EPage,
ESite, ESite,
} from "../../../web/src/render/ed/logic/ed-global"; } from "../../../web/src/render/ed/logic/ed-global";
import { IItem } from "../../../web/src/utils/types/item";
/* /*
WARNING: WARNING:
@ -22,8 +23,10 @@ export const SyncActions = {
load: async (id: string) => ({}) as ESite | void, load: async (id: string) => ({}) as ESite | void,
}, },
comp: { comp: {
new: async (arg: { group_id: string; item: IItem }) => {},
list: () => ({}) as Record<string, Exclude<component, "content_tree">>, 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, load: async (id: string) => ({}) as EComp | void,
}, },
page: { 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 { 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 { snapshot } from "../entity/snapshot";
import { user } from "../entity/user";
import { gzipAsync } from "../entity/zlib"; 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 snap = snapshot.get("comp", id);
let ydoc = docs.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 } }); const comp = await db.component.findFirst({ where: { id } });
if (comp) { if (comp) {
const doc = new Y.Doc(); const doc = new Y.Doc();
let root = doc.getMap("map"); 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] = { docs.comp[id] = {
doc: doc as any, doc: doc as any,
id, id,
@ -23,13 +67,24 @@ export const comp_load = async function (this: SyncConnection, id: string) {
}; };
const bin = Y.encodeStateAsUpdate(doc); const bin = Y.encodeStateAsUpdate(doc);
await attachOnUpdate(doc, um);
snapshot.update({ snapshot.update({
bin, bin,
id, id,
type: "comp", type: "comp",
name: comp.name, name: comp.name,
url: "",
ts: Date.now(), 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 { return {
@ -40,22 +95,43 @@ export const comp_load = async function (this: SyncConnection, id: string) {
} }
} else if (snap && !ydoc) { } else if (snap && !ydoc) {
const doc = new Y.Doc(); const doc = new Y.Doc();
snapshot.set("comp", id, "id_doc", doc.clientID);
Y.applyUpdate(doc, snap.bin); Y.applyUpdate(doc, snap.bin);
let root = doc.getMap("map"); let root = doc.getMap("map");
const um = new Y.UndoManager(root, { ignoreRemoteMapChanges: true }); const um = await createUndoManager(root);
docs.page[id] = { await attachOnUpdate(doc, um);
docs.comp[id] = {
doc: doc as any, doc: doc as any,
id, id,
um, 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 { return {
id: id, id: id,
name: snap.name, name: snap.name,
snapshot: await gzipAsync(snap.bin), snapshot: await gzipAsync(snap.bin),
}; };
} else if (snap && ydoc) { } 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 { return {
id: snap.id, id: snap.id,
name: snap.name, 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 "./site_group";
export * from "./page_load"; export * from "./page_load";
export * from "./comp_load"; export * from "./comp_load";
export * from "./comp_group";
export * from "./yjs_um"; export * from "./yjs_um";
export * from "./yjs_sv_local"; export * from "./yjs_sv_local";
export * from "./yjs_diff_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 snap = snapshot.get("page", id);
let ydoc = docs.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 createUndoManager = async (root: Y.Map<any>) => {
const um = new Y.UndoManager(root, { const um = new Y.UndoManager(root, {

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import { IItem, MItem } from "../../../utils/types/item";
import { DComp, DPage, IRoot } from "../../../utils/types/root"; import { DComp, DPage, IRoot } from "../../../utils/types/root";
import { ISection } from "../../../utils/types/section"; import { ISection } from "../../../utils/types/section";
import { IText, MText } from "../../../utils/types/text"; import { IText, MText } from "../../../utils/types/text";
import { SAction } from "../../../../../srv/ws/sync/actions";
const EmptySite = { const EmptySite = {
id: "", id: "",
@ -83,11 +84,12 @@ export const EDGlobal = {
doc: null as null | DComp, doc: null as null | DComp,
item: null as null | IItem, item: null as null | IItem,
list: {} as Record<string, { cur: EComp; doc: DComp }>, list: {} as Record<string, { cur: EComp; doc: DComp }>,
group: {} as Record<string, Awaited<ReturnType<SAction["comp"]["group"]>>>,
}, },
ui: { ui: {
tree: { tree: {
search: "", search: "",
searchMode: { search_mode: {
Name: true, Name: true,
JS: false, JS: false,
HTML: false, HTML: false,
@ -96,14 +98,12 @@ export const EDGlobal = {
open: {} as Record<string, string[]>, open: {} as Record<string, string[]>,
}, },
popup: { popup: {
comp: null as null | true | ((comp_id: string) => void | Promise<void>), comp: null as null | ((comp_id: string) => void | Promise<void>),
compGroup: null as comp_group: null as null | {
| null mouse_event: React.MouseEvent<HTMLElement, MouseEvent>;
| true on_pick?: (group_id: string) => void | Promise<void>;
| { on_close?: () => void | Promise<void>;
event: React.MouseEvent<HTMLElement, MouseEvent>; },
pick: (group_id: string) => 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 { IItem } from "../../../../../../../utils/types/item";
import { PG } from "../../../../../logic/ed-global"; 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; const mitem = p.page.meta[item.id].mitem;
if (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) { if (!item) {
return ( return (
<Menu mouseEvent={event} onClose={onClose}> <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> </Menu>
); );
} }
@ -79,7 +82,7 @@ export const EdTreeCtxMenu = ({
{type === "item" && !comp?.id && ( {type === "item" && !comp?.id && (
<MenuItem <MenuItem
label="Create Component" label="Create Component"
onClick={() => edActionNewComp(p, item)} onClick={(e) => edActionNewComp(p, item, e)}
/> />
)} )}
{!item.hidden && ( {!item.hidden && (

View File

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

View File

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