This commit is contained in:
Rizky 2024-02-12 17:06:14 +07:00
parent 4711bafef1
commit 51d0f5accd
13 changed files with 397 additions and 62 deletions

View File

@ -146,24 +146,25 @@ model org_user {
} }
model page { model page {
id String @id(map: "page_id") @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id(map: "page_id") @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String name String
url String url String
content_tree Json content_tree Json
id_site String @db.Uuid id_site String @db.Uuid
created_at DateTime? @default(now()) @db.Timestamp(6) created_at DateTime? @default(now()) @db.Timestamp(6)
js_compiled String? js_compiled String?
js String? js String?
updated_at DateTime? @default(now()) @db.Timestamp(6) updated_at DateTime? @default(now()) @db.Timestamp(6)
id_folder String? @db.Uuid id_folder String? @db.Uuid
is_deleted Boolean @default(false) is_deleted Boolean @default(false)
id_layout String? @db.Uuid id_layout String? @db.Uuid
is_default_layout Boolean @default(false) is_default_layout Boolean @default(false)
code_assign code_assign[] code_assign code_assign[]
page_folder page_folder? @relation(fields: [id_folder], references: [id], onDelete: NoAction, onUpdate: NoAction) page_folder page_folder? @relation(fields: [id_folder], references: [id], onDelete: NoAction, onUpdate: NoAction)
page page? @relation("pageTopage", fields: [id_layout], references: [id], onDelete: NoAction, onUpdate: NoAction) page page? @relation("pageTopage", fields: [id_layout], references: [id], onDelete: NoAction, onUpdate: NoAction)
other_page page[] @relation("pageTopage") other_page page[] @relation("pageTopage")
site site @relation(fields: [id_site], references: [id], onDelete: NoAction, onUpdate: NoAction) site site @relation(fields: [id_site], references: [id], onDelete: NoAction, onUpdate: NoAction)
page_history page_history[]
} }
model page_folder { model page_folder {
@ -307,3 +308,11 @@ model code_assign {
component_group component_group? @relation(fields: [id_component_group], references: [id], onDelete: NoAction, onUpdate: NoAction) component_group component_group? @relation(fields: [id_component_group], references: [id], onDelete: NoAction, onUpdate: NoAction)
page page? @relation(fields: [id_page], references: [id], onDelete: NoAction, onUpdate: NoAction) page page? @relation(fields: [id_page], references: [id], onDelete: NoAction, onUpdate: NoAction)
} }
model page_history {
id String @id(map: "page_history_id") @default(dbgenerated("gen_random_uuid()")) @db.Uuid
id_page String @db.Uuid
content_tree Bytes
ts String
page page @relation(fields: [id_page], references: [id], onDelete: NoAction, onUpdate: NoAction)
}

View File

@ -11,6 +11,7 @@ import { SyncConnection } from "../type";
import { parseJs } from "../editor/parser/parse-js"; import { parseJs } from "../editor/parser/parse-js";
import { snapshot } from "../entity/snapshot"; import { snapshot } from "../entity/snapshot";
import { validate } from "uuid"; import { validate } from "uuid";
import { gzipAsync } from "utils/diff";
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const timeout = { const timeout = {

View File

@ -3,6 +3,9 @@ import { SAction } from "../actions";
import { docs } from "../entity/docs"; import { docs } from "../entity/docs";
import { gunzipAsync } from "../entity/zlib"; import { gunzipAsync } from "../entity/zlib";
import { SyncConnection } from "../type"; import { SyncConnection } from "../type";
import { gzipAsync } from "utils/diff";
const history = {} as Record<string, string>;
export const yjs_diff_local: SAction["yjs"]["diff_local"] = async function ( export const yjs_diff_local: SAction["yjs"]["diff_local"] = async function (
this: SyncConnection, this: SyncConnection,
@ -25,6 +28,32 @@ export const yjs_diff_local: SAction["yjs"]["diff_local"] = async function (
if (root) { if (root) {
if (mode === "page") { if (mode === "page") {
if (validate(id) && id) { if (validate(id) && id) {
let mode = "create" as "create" | "update";
const cur = Math.round(Date.now() / 5000) + "";
if (history[id] && history[id] === cur) {
mode = "update";
}
history[id] = cur;
if (mode === "create") {
await _db.page_history.create({
data: {
id_page: id,
content_tree: await gzipAsync(JSON.stringify(root.toJSON())),
ts: history[id],
},
});
} else {
await _db.page_history.updateMany({
data: {
content_tree: await gzipAsync(JSON.stringify(root.toJSON())),
},
where: {
id_page: id,
ts: history[id],
},
});
}
await _db.page.update({ await _db.page.update({
where: { id }, where: { id },
data: { data: {

View File

@ -16,6 +16,7 @@ import { EdPopComp } from "./panel/popup/comp/comp-popup";
import { EdPopPage } from "./panel/popup/page/page-popup"; import { EdPopPage } from "./panel/popup/page/page-popup";
import { EdPopScript } from "./panel/popup/script/pop-script"; import { EdPopScript } from "./panel/popup/script/pop-script";
import { EdPopSite } from "./panel/popup/site/site-popup"; import { EdPopSite } from "./panel/popup/site/site-popup";
import { EdPageHistoryMain } from "./panel/main/main-history";
export const EdBase = () => { export const EdBase = () => {
const p = useGlobal(EDGlobal, "EDITOR"); const p = useGlobal(EDGlobal, "EDITOR");
@ -51,22 +52,27 @@ export const EdBase = () => {
)} )}
<div className="flex flex-1 flex-col items-stretch"> <div className="flex flex-1 flex-col items-stretch">
<EdMid /> <EdMid />
<div
className={cx( {p.page.history.id ? (
"flex flex-1 items-stretch", <EdPageHistoryMain />
p.mode === "mobile" ? mobileCSS : "bg-white" ) : (
)} <div
> className={cx(
{p.status !== "ready" ? ( "flex flex-1 items-stretch",
<Loading note={`page-${p.status}`} /> p.mode === "mobile" ? mobileCSS : "bg-white"
) : ( )}
<> >
<EdMain /> {p.status !== "ready" ? (
<EdPane type="right" min_size={240} /> <Loading note={`page-${p.status}`} />
<EdRight /> ) : (
</> <>
)} <EdMain />
</div> <EdPane type="right" min_size={240} />
<EdRight />
</>
)}
</div>
)}
</div> </div>
</div> </div>
<> <>

View File

@ -7,11 +7,17 @@ import { EdApi } from "./panel/header/left/api";
import { EdSiteJS } from "./panel/header/left/js"; import { EdSiteJS } from "./panel/header/left/js";
import { EdSitePicker } from "./panel/header/left/site-picker"; import { EdSitePicker } from "./panel/header/left/site-picker";
import { EdTreeBody } from "./panel/tree/body"; import { EdTreeBody } from "./panel/tree/body";
import { EdPageHistoryBtn } from "./panel/tree/history-btn";
import { EdPageHistoryList } from "./panel/tree/history-list";
import { EdTreeSearch } from "./panel/tree/search"; import { EdTreeSearch } from "./panel/tree/search";
import { treeRebuild } from "./logic/tree/build";
export const EdLeft = () => { export const EdLeft = () => {
const p = useGlobal(EDGlobal, "EDITOR"); const p = useGlobal(EDGlobal, "EDITOR");
const local = useLocal({ tree: null as any, timeout: null as any }); const local = useLocal({
tree: null as any,
timeout: null as any,
});
if (!local.tree) { if (!local.tree) {
clearTimeout(local.timeout); clearTimeout(local.timeout);
@ -40,27 +46,46 @@ export const EdLeft = () => {
</div> </div>
</div> </div>
<EdTreeSearch /> <div className="flex flex-row items-stretch border-b">
<EdPageHistoryBtn
show={p.page.history.show}
onShow={async (show) => {
p.page.history.id = "";
p.page.history.show = show;
if (!show) {
await treeRebuild(p);
}
p.render();
local.render();
}}
/>
{!p.page.history.show && <EdTreeSearch />}
</div>
<div <div
className="tree-body flex relative flex-1 overflow-y-auto overflow-x-hidden" className="tree-body flex relative flex-1 overflow-y-auto overflow-x-hidden"
ref={(ref) => { ref={(ref) => {
if (ref) local.tree = ref; if (ref) local.tree = ref;
}} }}
> >
<div className="absolute inset-0 flex flex-col"> {p.page.history.show ? (
{local.tree && ( <EdPageHistoryList />
<DndProvider ) : (
backend={HTML5Backend} <div className="absolute inset-0 flex flex-col">
options={getBackendOptions({ {local.tree && (
html5: { <DndProvider
rootElement: local.tree, backend={HTML5Backend}
}, options={getBackendOptions({
})} html5: {
> rootElement: local.tree,
<EdTreeBody /> },
</DndProvider> })}
)} >
</div> <EdTreeBody />
</DndProvider>
)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,6 +2,8 @@ import { getCompMeta } from "../comp/comp-meta";
import { IMeta, PG, active } from "../ed-global"; import { IMeta, PG, active } from "../ed-global";
export const isMetaActive = (p: PG, meta: IMeta) => { export const isMetaActive = (p: PG, meta: IMeta) => {
if (!meta.item) return false;
let is_active: boolean = active.item_id === meta.item.id; let is_active: boolean = active.item_id === meta.item.id;
if (active.comp_id) { if (active.comp_id) {
if (meta.parent?.comp_id === active.comp_id) { if (meta.parent?.comp_id === active.comp_id) {

View File

@ -168,6 +168,10 @@ export const EDGlobal = {
init_local_effect: {} as Record<string, boolean>, init_local_effect: {} as Record<string, boolean>,
}, },
page: { page: {
history: {
id: "",
show: false
},
root_id: "root", root_id: "root",
cur: EmptyPage, cur: EmptyPage,
doc: null as null | DPage, doc: null as null | DPage,

View File

@ -0,0 +1,152 @@
import { useGlobal, useLocal } from "web-utils";
import { EDGlobal } from "../../logic/ed-global";
import { FC, useEffect } from "react";
import { decompress } from "wasm-gzip";
import { Vi } from "../../../vi/vi";
import { genMeta } from "../../../vi/meta/meta";
import { IRoot } from "../../../../utils/types/root";
import { IContent } from "../../../../utils/types/general";
import { initLoadComp } from "../../../vi/meta/comp/init-comp-load";
import { loadCompSnapshot } from "../../logic/comp/load";
import { IItem } from "../../../../utils/types/item";
import { mainStyle } from "./main";
import { Loading } from "../../../../utils/ui/loading";
import { treeRebuild } from "../../logic/tree/build";
const decoder = new TextDecoder();
export const EdPageHistoryMain: FC<{}> = ({}) => {
const p = useGlobal(EDGlobal, "EDITOR");
const local = useLocal({
loading: true,
root: null as any,
meta: {} as any,
entry: [] as any,
width: 0,
height: 0,
});
useEffect(() => {
local.loading = true;
local.render();
_db.page_history
.findFirst({
where: { id: p.page.history.id },
select: {
content_tree: true,
},
})
.then(async (e) => {
if (e) {
const zip = new Uint8Array((e.content_tree as any).data);
const root = JSON.parse(decoder.decode(decompress(zip))) as IRoot;
local.root = root;
await initLoadComp(
{
comps: p.comp.loaded,
meta: local.meta,
mode: "page",
},
root as unknown as IItem,
{
async load(comp_ids) {
if (!p.sync) return;
const ids = comp_ids.filter((id) => !p.comp.loaded[id]);
const comps = await p.sync.comp.load(ids, true);
let result = Object.entries(comps);
for (const [id_comp, comp] of result) {
if (comp && comp.snapshot && !p.comp.list[id_comp]) {
if (!p.comp.loaded[id_comp]) {
await loadCompSnapshot(p, id_comp, comp.snapshot);
}
}
}
},
}
);
local.meta = {};
local.entry = [];
for (const item of root.childs) {
local.entry.push(item.id);
genMeta(
{
note: "cache-rebuild",
comps: p.comp.loaded,
meta: local.meta,
mode: "page",
},
{ item: item as IContent }
);
}
local.loading = false;
local.render();
p.render();
}
});
}, [p.page.history.id]);
return (
<div className="flex flex-1 flex-col items-stretch">
<div className="border-b p-1 text-sm flex">
<div
className="border px-2 cursor-pointer hover:bg-blue-200 border border-blue-700 hover:bg-blue-700 hover:text-white transition-all "
onClick={async () => {
if (confirm("Are you sure ?")) {
p.page.history.id = "";
p.page.history.show = false;
p.page.doc?.transact(() => {
const root = p.page.doc?.getMap("map").get("root");
if (root) {
syncronize(root as any, local.root);
}
});
p.render();
}
}}
>
Revert to this version
</div>
</div>
<div
className={cx(
"flex flex-1 relative overflow-auto",
p.mode === "mobile" ? "flex-col items-center" : ""
)}
ref={(el) => {
if (el) {
const bound = el.getBoundingClientRect();
if (local.width !== bound.width || local.height !== bound.height) {
local.width = bound.width;
local.height = bound.height;
local.render();
}
}
}}
>
{local.loading ? (
<Loading backdrop={true} />
) : (
<div className={mainStyle(p, local.meta)}>
<Vi
meta={local.meta}
mode={p.mode}
api_url={p.site.config.api_url}
site_id={p.site.id}
page_id={p.page.cur.id}
entry={local.entry}
api={p.script.api}
db={p.script.db}
script={{ init_local_effect: p.script.init_local_effect }}
/>
</div>
)}
</div>
</div>
);
};

View File

@ -107,7 +107,7 @@ export const EdMain = () => {
return null; return null;
}; };
const mainStyle = (p: PG, meta?: IMeta) => { export const mainStyle = (p: PG, meta?: IMeta) => {
let is_active = meta ? isMetaActive(p, meta) : false; let is_active = meta ? isMetaActive(p, meta) : false;
const scale = parseInt(p.ui.zoom.replace("%", "")) / 100; const scale = parseInt(p.ui.zoom.replace("%", "")) / 100;

View File

@ -87,9 +87,9 @@ export const EdPopComp = () => {
className={cx( className={cx(
"border cursor-pointer -mb-[1px] px-2 hover:text-blue-500 hover:border-blue-500 hover:border-b-transparent select-none", "border cursor-pointer -mb-[1px] px-2 hover:text-blue-500 hover:border-blue-500 hover:border-b-transparent select-none",
local.tab === e && local.tab === e &&
"bg-white border-b-transparent", "bg-white border-b-transparent",
local.tab !== e && local.tab !== e &&
"text-slate-400 border-b-slate-200 border-transparent bg-transparent" "text-slate-400 border-b-slate-200 border-transparent bg-transparent"
)} )}
onClick={() => { onClick={() => {
local.tab = e; local.tab = e;
@ -101,7 +101,7 @@ export const EdPopComp = () => {
); );
})} })}
</div> </div>
<div className="flex flex-1 mr-1 justify-end"> <div className="flex flex-1 mr-1">
<input <input
type="search" type="search"
placeholder="Search" placeholder="Search"
@ -128,19 +128,23 @@ export const EdPopComp = () => {
background: #efefff; background: #efefff;
} }
`, `,
compPicker.search ? css` compPicker.search
> .tree-root { ? css`
display: flex; > .tree-root {
flex-direction: row; display: flex;
flex-wrap: wrap; flex-direction: row;
position: relative; flex-wrap: wrap;
}` : css` position: relative;
> .tree-root > .listitem > .container { }
display: flex; `
flex-direction: row; : css`
flex-wrap: wrap; > .tree-root > .listitem > .container {
position: relative; display: flex;
}` flex-direction: row;
flex-wrap: wrap;
position: relative;
}
`
)} )}
> >
{compPicker.ref && compPicker.status === "ready" && ( {compPicker.ref && compPicker.status === "ready" && (

View File

@ -0,0 +1,41 @@
import { FC } from "react";
export const EdPageHistoryBtn: FC<{
show: boolean;
onShow: (show: boolean) => void;
}> = ({ show, onShow }) => {
return (
<>
<div
className={cx(
"border-r min-w-[25px] px-2 flex items-center justify-center cursor-pointer select-none",
show && "bg-blue-700 text-white flex-1",
!show && "hover:bg-blue-100"
)}
onClick={() => {
onShow(!show);
}}
>
<div
className={css`
svg {
width: 12px;
height: 12px;
}
`}
dangerouslySetInnerHTML={{
__html: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-history"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>`,
}}
></div>
{show && (
<>
<div className="text-xs ml-1 flex-1">History</div>
<div className="ml-3 border-l border-l-blue-100/20 pl-2">
&times;
</div>
</>
)}
</div>
</>
);
};

View File

@ -0,0 +1,63 @@
import { useGlobal, useLocal } from "web-utils";
import { EDGlobal } from "../../logic/ed-global";
import { Loading } from "../../../../utils/ui/loading";
import { format, formatDistance } from "date-fns";
export const EdPageHistoryList = () => {
const p = useGlobal(EDGlobal, "EDITOR");
const local = useLocal(
{ loading: true, list: [] as Awaited<ReturnType<typeof queryList>> },
async () => {
console.log("query list");
local.list = await queryList(p.page.cur.id);
local.loading = false;
local.render();
}
);
return (
<>
{local.loading ? (
<Loading backdrop={false} />
) : (
<div className="flex flex-1 flex-col items-stretch">
{local.list.map((e) => {
return (
<div
className={cx(
"flex justify-between items-center text-sm px-2 py-1 cursor-pointer hover:bg-blue-100 border-b transition-all select-none",
e.id === p.page.history.id &&
"border-r-4 bg-blue-50 border-r-blue-700"
)}
key={e.id}
onClick={() => {
p.page.history.id = e.id;
p.render();
local.render();
}}
>
<div className="flex-1">
{format(parseInt(e.ts) * 5000, "yyyy-MM-dd HH:mm:ss")}
</div>
<div className="text-right text-[11px]">
{formatDistance(Date.now(), parseInt(e.ts) * 5000) + " ago"}
</div>
</div>
);
})}
</div>
)}
</>
);
};
const queryList = async (page_id: string) => {
return await _db.page_history.findMany({
where: { id_page: page_id },
select: {
id: true,
ts: true,
},
orderBy: { ts: "desc" },
});
};

View File

@ -22,7 +22,6 @@ export const EdTreeSearch = () => {
return ( return (
<div <div
className="flex flex-col items-stretch border-b"
onMouseOver={() => { onMouseOver={() => {
if (local.focus) { if (local.focus) {
local.hover = true; local.hover = true;