diff --git a/app/srv/ws/sync/actions/site_load.ts b/app/srv/ws/sync/actions/site_load.ts index 8bc0ffe7..670edfdd 100644 --- a/app/srv/ws/sync/actions/site_load.ts +++ b/app/srv/ws/sync/actions/site_load.ts @@ -12,9 +12,14 @@ export const site_load: SAction["site"]["load"] = async function ( if (site) { if (this.conf) this.conf.site_id = site.id; + const config = + typeof site.config === "object" && site.config + ? { api_url: (site.config as any).api_url || "" } + : { api_url: "" }; + return { id: site.id, - config: site.config as ESite["config"], + config: config as ESite["config"], domain: site.domain, js: site.js || "", js_compiled: site.js_compiled || "", diff --git a/app/web/src/render/ed/ed-left.tsx b/app/web/src/render/ed/ed-left.tsx index 37617c5b..be18d332 100644 --- a/app/web/src/render/ed/ed-left.tsx +++ b/app/web/src/render/ed/ed-left.tsx @@ -5,7 +5,7 @@ import { EDGlobal } from "./logic/ed-global"; import { EdTreeBody } from "./panel/tree/body"; import { EdTreeSearch } from "./panel/tree/search"; import { EdSitePicker } from "./panel/header/left/site"; -import { EdApiConfig } from "./panel/header/left/api"; +import { EdApi } from "./panel/header/left/api"; import { EdExport } from "./panel/header/left/export"; import { EdNpm } from "./panel/header/left/npm"; import { EdSiteJS } from "./panel/header/left/js"; @@ -31,7 +31,7 @@ export const EdLeft = () => {
- +
diff --git a/app/web/src/render/ed/panel/header/left/api.tsx b/app/web/src/render/ed/panel/header/left/api.tsx index 5e642712..ea0e1bf1 100644 --- a/app/web/src/render/ed/panel/header/left/api.tsx +++ b/app/web/src/render/ed/panel/header/left/api.tsx @@ -1,9 +1,15 @@ +import { EdApiServer } from "../../popup/api/api-server"; import { TopBtn } from "../top-btn"; -export const EdApiConfig = () => { +export const EdApi = () => { return ( - - API + } + placement="right" + > +
API
); }; diff --git a/app/web/src/render/ed/panel/header/top-btn.tsx b/app/web/src/render/ed/panel/header/top-btn.tsx index 84afdfb1..5564b48a 100644 --- a/app/web/src/render/ed/panel/header/top-btn.tsx +++ b/app/web/src/render/ed/panel/header/top-btn.tsx @@ -1,5 +1,6 @@ import { ReactElement, ReactNode } from "react"; import { Popover } from "../../../../utils/ui/popover"; +import { Placement } from "@floating-ui/react"; export const TopBtn = ({ children, @@ -9,6 +10,7 @@ export const TopBtn = ({ onClick, style = "normal", popover, + placement, }: { children: ReactNode; className?: string; @@ -17,6 +19,7 @@ export const TopBtn = ({ onClick?: React.MouseEventHandler; style?: "slim" | "normal"; popover?: ReactElement; + placement?: Placement; }) => { const result = (
+ {result} ); diff --git a/app/web/src/render/ed/panel/popup/api/api-server.tsx b/app/web/src/render/ed/panel/popup/api/api-server.tsx new file mode 100644 index 00000000..4f74cb3c --- /dev/null +++ b/app/web/src/render/ed/panel/popup/api/api-server.tsx @@ -0,0 +1,44 @@ +import { forwardRef } from "react"; +import { useGlobal, useLocal } from "web-utils"; +import { EDGlobal } from "../../../logic/ed-global"; + +export const EdApiServer = forwardRef((arg, ref) => { + const p = useGlobal(EDGlobal, "EDITOR"); + const local = useLocal({ api_url: p.site.config.api_url }); + + return ( +
+
Server URL:
+
+
+ { + local.api_url = e.currentTarget.value; + local.render(); + }} + onFocus={(e) => { + if (!e.currentTarget.value) { + local.api_url = `https://`; + local.render(); + } + }} + type="text" + className="outline-none focus:border-blue-500 flex-1" + placeholder="https://..." + /> +
+ {local.api_url !== p.site.config.api_url && ( +
+
+ Check +
+
+ )} +
+
+ ); +}); diff --git a/app/web/src/render/ed/panel/popup/npm/npm-import.tsx b/app/web/src/render/ed/panel/popup/npm/npm-import.tsx index ca21de2b..1728c081 100644 --- a/app/web/src/render/ed/panel/popup/npm/npm-import.tsx +++ b/app/web/src/render/ed/panel/popup/npm/npm-import.tsx @@ -234,17 +234,25 @@ export const EdNpmImport = ({ mode }: { mode: "page" | "site" }) => { /> )} {local.list.length === 0 && ( -
-
`, - }} - >
-
- No package for {mode === "site" ? "whole site" : "current page"} -
-
+ <> + {local.status === "init" ? ( +
+ Loading... +
+ ) : ( +
+
`, + }} + >
+
+ No package for {mode === "site" ? "whole site" : "current page"} +
+
+ )} + )}
); diff --git a/app/web/src/render/ed/panel/popup/site/site-head.tsx b/app/web/src/render/ed/panel/popup/site/site-head.tsx new file mode 100644 index 00000000..4f53ec22 --- /dev/null +++ b/app/web/src/render/ed/panel/popup/site/site-head.tsx @@ -0,0 +1,69 @@ +import { NodeModel } from "@minoru/react-dnd-treeview"; +import { SiteGroupItem } from "./site-tree"; +import { useGlobal } from "web-utils"; +import { EDGlobal } from "../../../logic/ed-global"; + +export const EdSiteHead = ({ + group, + update, + reload, + orglen, + conf, +}: { + orglen: number; + group: NodeModel[]; + update: (val: NodeModel[]) => void; + reload: (id?: string) => Promise; + conf: { group: any }; +}) => { + const p = useGlobal(EDGlobal, "EDITOR"); + + return ( +
+
+ {orglen} Organization{orglen > 1 ? "s" : ""} +
+
{ + const neworg = prompt("New Organization Name"); + if (neworg) { + const res = await db.org.create({ + data: { + name: neworg, + org_user: { + create: { id_user: p.user.id, role: "owner" }, + }, + }, + }); + + update([]); + + setTimeout(() => { + reload(res.id); + }); + } + }} + > +
`, + }} + >
+
New
+
+ +
{ + conf.group = null; + reload(); + }} + > + Refresh +
+
+ ); +}; diff --git a/app/web/src/render/ed/panel/popup/site/site-tree.tsx b/app/web/src/render/ed/panel/popup/site/site-tree.tsx new file mode 100644 index 00000000..1dd41383 --- /dev/null +++ b/app/web/src/render/ed/panel/popup/site/site-tree.tsx @@ -0,0 +1,382 @@ +import { + MultiBackend, + NodeModel, + Tree, + getBackendOptions, +} from "@minoru/react-dnd-treeview"; +import { DndProvider } from "react-dnd"; +import { EdPopUser } from "./site-user"; +import { useGlobal, useLocal } from "web-utils"; +import { EDGlobal } from "../../../logic/ed-global"; + +export type SiteGroupItem = { + id: string; + name: string; +} & ( + | { + type: "group"; + site_len: number; + users: { id: string; username: string }[]; + renaming?: boolean; + } + | { type: "site"; domain: string; responsive: string } +); + +export const EdSiteTree = ({ + group, + update, + reload, + orglen, +}: { + orglen: number; + group: NodeModel[]; + update: (val: NodeModel[]) => void; + reload: (id?: string) => Promise; +}) => { + const p = useGlobal(EDGlobal, "EDITOR"); + const local = useLocal({}); + const TypedTree = Tree; + + return ( + + { + const target = dropTarget?.data; + const from = dragSource?.data; + if (target && from) { + if (target.type === "group") { + await db.site.update({ + where: { + id: from.id, + }, + data: { + org: { + connect: { + id: target.id, + }, + }, + }, + select: { id: true }, + }); + reload(); + } + } + }} + initialOpen={true} + canDrag={(node) => { + if (node && node?.data?.type === "site") return true; + return false; + }} + canDrop={(_, { dragSource, dropTarget }) => { + if (dragSource?.parent === dropTarget?.id) return false; + return true; + }} + sort={(a, b) => { + if (a.text === "new") return 1; + if (b.text === "new") return -1; + return a.text > b.text ? 1 : -1; + }} + dragPreviewRender={() => <>} + classes={{ + root: cx( + "flex flex-1 flex-col items-stretch overflow-auto", + css` + flex-wrap: nowrap; + background: white; + & > li { + padding-bottom: 10px; + } + & > li:nth-child(odd) { + border-top: 1px solid #ececeb; + border-bottom: 1px solid #ececeb; + background: rgb(237, 245, 254); + } + ` + ), + container: "flex flex-row flex-wrap pb-2", + }} + render={( + node, + { depth, isOpen, onToggle, isDropTarget, isDragging } + ) => { + const item = node.data; + + if (node.text === "new") { + return ( +
{ + if (typeof node.id === "string") { + p.ui.popup.site_form = { + group_id: node.id.replace("new-", ""), + id: "new", + }; + p.render(); + } + }} + > +
+
`, + }} + >
+
New Site
+
+
+ ); + } + + if (!item) return <>; + + if (item.type === "group") { + return ( +
+ {item.renaming ? ( + { + item.name = e.currentTarget.value; + local.render(); + }} + onBlur={async () => { + if (item.renaming && item.name !== node.text) { + node.text = item.name; + item.renaming = false; + local.render(); + await db.org.update({ + where: { id: item.id }, + data: { name: item.name }, + }); + reload(); + } else { + item.renaming = false; + local.render(); + } + }} + onKeyDown={async (e) => { + if (e.key === "Escape") { + item.name = node.text; + item.renaming = false; + local.render(); + } else if (e.key === "Enter") { + e.currentTarget.blur(); + } + }} + /> + ) : ( + <> +
{node.text}
+
{ + item.renaming = true; + local.render(); + }} + > +
`, + }} + >
+
+ + )} + { + await db.org_user.deleteMany({ + where: { id_org: item.id, id_user: u.id }, + }); + item.users = item.users.filter((e) => e.id !== u.id); + local.render(); + }} + onAdd={async (u) => { + await db.org_user.create({ + data: { id_org: item.id, id_user: u.id }, + }); + item.users = [...item.users, u]; + local.render(); + }} + > +
+ Team: {item.users.length} user + {item.users.length > 1 ? "s" : ""} +
+
+ {isDropTarget && ( +
+ Drop here... +
+ )} + {item.site_len === 0 && ( +
{ + if (confirm("Remove this organization ?")) { + await db.org_user.deleteMany({ + where: { id_org: item.id }, + }); + await db.org.delete({ + where: { + id: item.id, + }, + }); + reload(); + } + }} + > +
`, + }} + >
+
+ )} +
+ ); + } + + return ( + { + e.preventDefault(); + e.stopPropagation(); + if (p.ui.popup.site) { + p.ui.popup.site(item.id); + } + p.ui.popup.site = null; + p.render(); + }} + className={cx( + "flex flex-col ml-2 mt-1 mb-1 w-[150px] h-[80px] text-[14px] border bg-white hover:bg-blue-100 cursor-pointer relative", + isDragging && "opacity-20", + css` + .edit { + opacity: 0; + } + &:hover .edit { + opacity: 1; + } + ` + )} + > +
+
+ {item.name} +
+
+
`, + }} + >
+
{item.domain}  
+
+
+ {item.responsive === "all" && ( +
+ `, + }} + > + Responsive +
+ )} + {item.responsive === "mobile-only" && ( +
+ `, + }} + > + Mobile +
+ )} + {item.responsive === "desktop-only" && ( +
+ `, + }} + > + Desktop +
+ )} +
+
+ {orglen > 1 && ( +
{ + e.stopPropagation(); + e.preventDefault(); + + if (typeof node.parent === "string") { + p.ui.popup.site_form = { + group_id: node.parent, + id: item.id, + domain: item.domain, + name: item.name, + responsive: item.responsive, + }; + p.render(); + } + }} + > + Drag me +
+ )} +
{ + e.stopPropagation(); + e.preventDefault(); + + if (typeof node.parent === "string") { + p.ui.popup.site_form = { + group_id: node.parent, + id: item.id, + domain: item.domain, + name: item.name, + responsive: item.responsive, + }; + p.render(); + } + }} + > + Edit Site + `, + }} + > +
+
+ ); + }} + /> +
+ ); +}; diff --git a/app/web/src/render/ed/panel/popup/site/site.tsx b/app/web/src/render/ed/panel/popup/site/site.tsx index c911ec3f..180a2140 100644 --- a/app/web/src/render/ed/panel/popup/site/site.tsx +++ b/app/web/src/render/ed/panel/popup/site/site.tsx @@ -1,30 +1,13 @@ -import { - MultiBackend, - NodeModel, - Tree, - getBackendOptions, -} from "@minoru/react-dnd-treeview"; +import { NodeModel, Tree } from "@minoru/react-dnd-treeview"; import { useEffect } from "react"; -import { DndProvider } from "react-dnd"; import { useGlobal, useLocal } from "web-utils"; import { Loading } from "../../../../../utils/ui/loading"; import { Modal } from "../../../../../utils/ui/modal"; import { EDGlobal } from "../../../logic/ed-global"; -import { EdPopUser } from "./site-user"; import { EdFormSite } from "./site-form"; +import { EdSiteTree, SiteGroupItem } from "./site-tree"; +import { EdSiteHead } from "./site-head"; -type GItem = { - id: string; - name: string; -} & ( - | { - type: "group"; - site_len: number; - users: { id: string; username: string }[]; - renaming?: boolean; - } - | { type: "site"; domain: string; responsive: string } -); const conf = { group: null as any }; export const EdPopSite = () => { @@ -32,7 +15,7 @@ export const EdPopSite = () => { const local = useLocal( { status: "init" as "init" | "loading" | "ready", - group: (conf.group || []) as NodeModel[], + group: (conf.group || []) as NodeModel[], }, () => {} ); @@ -43,7 +26,7 @@ export const EdPopSite = () => { const res = await p.sync.site.group(); - const group: NodeModel[] = []; + const group: NodeModel[] = []; for (const item of res) { group.push({ id: `new-${item.id}`, @@ -174,403 +157,27 @@ const SitePicker = ({ reload, update, }: { - group: NodeModel[]; - update: (val: NodeModel[]) => void; + group: NodeModel[]; + update: (val: NodeModel[]) => void; reload: (id?: string) => Promise; }) => { const p = useGlobal(EDGlobal, "EDITOR"); - const local = useLocal({}); - const TypedTree = Tree; const orglen = group.filter((e) => e.parent === "site-root").length; return (
-
-
- {orglen} Organization{orglen > 1 ? "s" : ""} -
-
{ - const neworg = prompt("New Organization Name"); - if (neworg) { - const res = await db.org.create({ - data: { - name: neworg, - org_user: { - create: { id_user: p.user.id, role: "owner" }, - }, - }, - }); - - update([]); - - setTimeout(() => { - reload(res.id); - }); - } - }} - > -
`, - }} - >
-
New
-
- -
{ - conf.group = null; - reload(); - }} - > - Refresh -
-
- - - { - const target = dropTarget?.data; - const from = dragSource?.data; - if (target && from) { - if (target.type === "group") { - await db.site.update({ - where: { - id: from.id, - }, - data: { - org: { - connect: { - id: target.id, - }, - }, - }, - select: { id: true }, - }); - reload(); - } - } - }} - initialOpen={true} - canDrag={(node) => { - if (node && node?.data?.type === "site") return true; - return false; - }} - canDrop={(_, { dragSource, dropTarget }) => { - if (dragSource?.parent === dropTarget?.id) return false; - return true; - }} - sort={(a, b) => { - if (a.text === "new") return 1; - if (b.text === "new") return -1; - return a.text > b.text ? 1 : -1; - }} - dragPreviewRender={() => <>} - classes={{ - root: cx( - "flex flex-1 flex-col items-stretch overflow-auto", - css` - flex-wrap: nowrap; - background: white; - & > li { - padding-bottom: 10px; - } - & > li:nth-child(odd) { - border-top: 1px solid #ececeb; - border-bottom: 1px solid #ececeb; - background: rgb(237, 245, 254); - } - ` - ), - container: "flex flex-row flex-wrap pb-2", - }} - render={( - node, - { depth, isOpen, onToggle, isDropTarget, isDragging } - ) => { - const item = node.data; - - if (node.text === "new") { - return ( -
{ - if (typeof node.id === "string") { - p.ui.popup.site_form = { - group_id: node.id.replace("new-", ""), - id: "new", - }; - p.render(); - } - }} - > -
-
`, - }} - >
-
New Site
-
-
- ); - } - - if (!item) return <>; - - if (item.type === "group") { - return ( -
- {item.renaming ? ( - { - item.name = e.currentTarget.value; - local.render(); - }} - onBlur={async () => { - if (item.renaming && item.name !== node.text) { - node.text = item.name; - item.renaming = false; - local.render(); - await db.org.update({ - where: { id: item.id }, - data: { name: item.name }, - }); - reload(); - } else { - item.renaming = false; - local.render(); - } - }} - onKeyDown={async (e) => { - if (e.key === "Escape") { - item.name = node.text; - item.renaming = false; - local.render(); - } else if (e.key === "Enter") { - e.currentTarget.blur(); - } - }} - /> - ) : ( - <> -
{node.text}
-
{ - item.renaming = true; - local.render(); - }} - > -
`, - }} - >
-
- - )} - { - await db.org_user.deleteMany({ - where: { id_org: item.id, id_user: u.id }, - }); - item.users = item.users.filter((e) => e.id !== u.id); - local.render(); - }} - onAdd={async (u) => { - await db.org_user.create({ - data: { id_org: item.id, id_user: u.id }, - }); - item.users = [...item.users, u]; - local.render(); - }} - > -
- Team: {item.users.length} user - {item.users.length > 1 ? "s" : ""} -
-
- {isDropTarget && ( -
- Drop here... -
- )} - {item.site_len === 0 && ( -
{ - if (confirm("Remove this organization ?")) { - await db.org_user.deleteMany({ - where: { id_org: item.id }, - }); - await db.org.delete({ - where: { - id: item.id, - }, - }); - reload(); - } - }} - > -
`, - }} - >
-
- )} -
- ); - } - - return ( - { - e.preventDefault(); - e.stopPropagation(); - if (p.ui.popup.site) { - p.ui.popup.site(item.id); - } - p.ui.popup.site = null; - p.render(); - }} - className={cx( - "flex flex-col ml-2 mt-1 mb-1 w-[150px] h-[80px] text-[14px] border bg-white hover:bg-blue-100 cursor-pointer relative", - isDragging && "opacity-20", - css` - .edit { - opacity: 0; - } - &:hover .edit { - opacity: 1; - } - ` - )} - > -
-
- {item.name} -
-
-
`, - }} - >
-
{item.domain}  
-
-
- {item.responsive === "all" && ( -
- `, - }} - > - Responsive -
- )} - {item.responsive === "mobile-only" && ( -
- `, - }} - > - Mobile -
- )} - {item.responsive === "desktop-only" && ( -
- `, - }} - > - Desktop -
- )} -
-
- {orglen > 1 && ( -
{ - e.stopPropagation(); - e.preventDefault(); - - if (typeof node.parent === "string") { - p.ui.popup.site_form = { - group_id: node.parent, - id: item.id, - domain: item.domain, - name: item.name, - responsive: item.responsive, - }; - p.render(); - } - }} - > - Drag me -
- )} -
{ - e.stopPropagation(); - e.preventDefault(); - - if (typeof node.parent === "string") { - p.ui.popup.site_form = { - group_id: node.parent, - id: item.id, - domain: item.domain, - name: item.name, - responsive: item.responsive, - }; - p.render(); - } - }} - > - Edit Site - `, - }} - > -
-
- ); - }} - /> -
+ +
); }; diff --git a/app/web/src/render/ed/panel/tree/node/render.tsx b/app/web/src/render/ed/panel/tree/node/render.tsx index d39a7e16..516ef05b 100644 --- a/app/web/src/render/ed/panel/tree/node/render.tsx +++ b/app/web/src/render/ed/panel/tree/node/render.tsx @@ -86,7 +86,7 @@ export const nodeRender: NodeRender = (node, prm) => { if (up) { let next = up.nextElementSibling; - while (next) { + while (next && next.children[0]) { if (next.children[0].classList.contains("has-child")) { const c = next.children[0] as HTMLInputElement; if (c) { diff --git a/app/web/src/render/editor/panel/toolbar/center/api/External.tsx b/app/web/src/render/editor/panel/toolbar/center/api/External.tsx index fdecc21c..4bd4fb57 100644 --- a/app/web/src/render/editor/panel/toolbar/center/api/External.tsx +++ b/app/web/src/render/editor/panel/toolbar/center/api/External.tsx @@ -23,7 +23,6 @@ export const ExternalAPI = ({
Checking...
)} {status === "started" &&
Saved
} - {status === "stopped" && (
Invalid Server
)} diff --git a/app/web/src/utils/sync/ws-client.ts b/app/web/src/utils/sync/ws-client.ts index 61ac96ca..0706a24f 100644 --- a/app/web/src/utils/sync/ws-client.ts +++ b/app/web/src/utils/sync/ws-client.ts @@ -9,12 +9,10 @@ import { SyncActionPaths, } from "../../../../srv/ws/sync/actions-def"; import { UserConf } from "../../../../srv/ws/sync/entity/user"; -import { SyncType } from "../../../../srv/ws/sync/type"; +import { ActivityList, SyncType } from "../../../../srv/ws/sync/type"; import { ESite } from "../../render/ed/logic/ed-global"; import { w } from "../types/general"; import { initIDB } from "./idb"; -import { ActivityList } from "../../../../srv/ws/sync/entity/actstore"; -import { wconns } from "../../../../srv/ws/sync/entity/conn"; const packr = new Packr({ structuredClone: true }); /** CONSTANT */ diff --git a/app/web/src/utils/ui/popover.tsx b/app/web/src/utils/ui/popover.tsx index 59d8f335..81bfff93 100644 --- a/app/web/src/utils/ui/popover.tsx +++ b/app/web/src/utils/ui/popover.tsx @@ -58,7 +58,7 @@ export function usePopover({ offset(typeof popoverOffset === "number" ? popoverOffset : 5), flip({ fallbackAxisSideDirection: "end", - padding: 5, + padding: 0, }), shift({ padding: 5 }), arrow({ element: arrowRef }),