This commit is contained in:
Rizky 2023-10-25 15:54:47 +07:00
parent 49e1569b07
commit 286be55e1d
7 changed files with 246 additions and 33 deletions

View File

@ -15,6 +15,9 @@ export const site_group = async function (this: SyncConnection) {
name: true,
domain: true,
},
where: {
is_deleted: false,
},
},
},
});

View File

@ -52,6 +52,7 @@
"react-dnd": "^16.0.1",
"react-dom": "18.2.0",
"react-is": "^18.2.0",
"react-select": "^5.7.7",
"react-use-error-boundary": "^3.0.0",
"react-virtuoso": "^4.6.2",
"safe-stable-stringify": "^2.4.3",

View File

@ -21,9 +21,16 @@ export const edRoute = async (p: PG) => {
!p.page.cur.snapshot ||
!p.page.list[p.page.cur.id]
) {
if (p.page.cur.snapshot && p.page.list[p.page.cur.id]) {
console.log("loading page from cache");
} else {
let loadFromServer = true;
if (p.page.list[params.page_id]) {
const cur = p.page.list[params.page_id];
p.page.cur = cur.page;
p.page.doc = cur.doc;
loadFromServer = false;
treeRebuild(p);
}
if (loadFromServer) {
p.status = "loading";
const page = await p.sync.page.load(params.page_id);

View File

@ -17,6 +17,23 @@ export const edInitSync = (p: PG) => {
p.user.id = session.data.user.id;
p.user.username = session.data.user.username;
if (p.sync) {
if (!params.page_id && params.site_id) {
db.page
.findFirst({
where: {
is_deleted: false,
is_default_layout: false,
id_site: params.site_id,
},
select: { id: true },
})
.then((e) => {
if (e) navigate(`/ed/${params.site_id}/${e.id}`);
});
return false;
}
}
if (!p.sync) {
clientStartSync({
user_id: session.data.user.id,

View File

@ -1,20 +1,161 @@
import { ReactNode } from "react";
import { Popover } from "../../../../utils/ui/popover";
import { Placement } from "@floating-ui/react";
import Select from "react-select";
import { useLocal } from "web-utils";
const user = {
loading: null as null | Promise<void>,
all: [] as { id: string; username: string }[],
};
export const EdPopUser = ({
users,
children,
placement = "right",
onDel,
onAdd,
}: {
users: { id: string; username: string }[];
children: ReactNode;
placement?: Placement;
onAdd?: (u: { id: string; username: string }) => void | Promise<void>;
onDel?: (u: { id: string; username: string }) => void | Promise<void>;
}) => {
const local = useLocal(
{
menuOpen: false,
index: {} as Record<string, { id: string; username: string }>,
},
async () => {
if (!user.loading) {
user.loading = new Promise(async (done) => {
const res = await db.user.findMany({
select: {
id: true,
username: true,
},
});
if (res) {
user.all = res;
}
local.render();
done();
});
} else {
await user.loading;
local.render();
}
}
);
local.index = {};
if (users) {
for (const r of users) {
local.index[r.id] = r;
}
}
export const EdPopUser = (users: { id: string; username: string }[]) => {
return (
<Popover>
<div className="ed-pop-user">
<div className="ed-pop-user-list">
{users.map((user) => (
<div key={user.id} className="ed-pop-user-item">
<div className="ed-pop-user-item-avatar">
<img src="" alt="" />
</div>
<div className="ed-pop-user-item-username">{user.username}</div>
<Popover
backdrop={false}
placement={placement}
autoFocus={false}
className="outline-none"
content={
<div
className={cx("text-[14px] min-w-[200px] outline-none -mx-2 -my-1")}
>
<div
className={cx(css`
.sel__control {
border: 0px;
border-radius: 0px;
border-bottom: 1px solid #ececeb;
outline: none;
}
.sel__control--is-focused {
box-shadow: none !important;
border: 0px;
}
.sel__menu {
border-radius: 0px;
border: 0px;
background: white;
margin: 0px;
}
.sel__value-container {
padding-left: 5px;
}
.sel__option {
padding: 2px 5px;
border-bottom: 1px solid #ececeb;
&:hover {
background: #e0e9fa;
}
}
`)}
>
<div className="border-b px-1 pt-2 pb-1">Existing user:</div>
<div className="flex flex-col ml-4 border-l">
{users.map((user) => (
<div
key={user.id}
className={
" bg-blue-50 hover:bg-blue-100 border-b pl-2 flex justify-between items-center"
}
>
<div className="flex-1">{user.username}</div>
{onDel && (
<div
className="p-1 hover:bg-red-600 hover:text-white cursor-pointer"
onClick={() => {
onDel(user);
}}
dangerouslySetInnerHTML={{
__html: `<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.5 1C5.22386 1 5 1.22386 5 1.5C5 1.77614 5.22386 2 5.5 2H9.5C9.77614 2 10 1.77614 10 1.5C10 1.22386 9.77614 1 9.5 1H5.5ZM3 3.5C3 3.22386 3.22386 3 3.5 3H5H10H11.5C11.7761 3 12 3.22386 12 3.5C12 3.77614 11.7761 4 11.5 4H11V12C11 12.5523 10.5523 13 10 13H5C4.44772 13 4 12.5523 4 12V4L3.5 4C3.22386 4 3 3.77614 3 3.5ZM5 4H10V12H5V4Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>`,
}}
></div>
)}
</div>
))}
</div>
))}
{user.all.length > 0 && onAdd && (
<Select
options={user.all
.filter((e) => {
if (local.index[e.id]) return false;
return true;
})
.map((user) => ({
label: user.username,
value: user.id,
}))}
value={null}
menuIsOpen={local.menuOpen}
onInputChange={(e) => {
if (e) {
local.menuOpen = true;
} else {
local.menuOpen = false;
}
local.render();
}}
autoFocus
onChange={async (e) => {
if (e) {
onAdd({ username: e.label, id: e.value });
}
}}
placeholder={"Add User"}
className={cx("outline-none border-t -mt-[1px]")}
classNamePrefix={"sel"}
/>
)}
</div>
</div>
</div>
}
>
{children}
</Popover>
);
};

View File

@ -10,6 +10,7 @@ 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";
type GItem = {
id: string;
@ -23,13 +24,14 @@ type GItem = {
}
| { type: "site"; domain: string }
);
const conf = { group: null as any };
export const EdPopSite = () => {
const p = useGlobal(EDGlobal, "EDITOR");
const local = useLocal(
{
status: "init" as "init" | "loading" | "ready",
group: [] as NodeModel<GItem>[],
group: (conf.group || []) as NodeModel<GItem>[],
},
() => {
p.ui.popup.site = () => {};
@ -84,12 +86,13 @@ export const EdPopSite = () => {
});
}
local.group = group;
conf.group = group;
local.status = "ready";
local.render();
};
useEffect(() => {
if (p.ui.popup.site && local.status !== "loading") {
if (p.ui.popup.site && local.status !== "loading" && !conf.group) {
reload();
}
}, [p.ui.popup.site]);
@ -139,12 +142,12 @@ const SitePicker = ({
const orglen = group.filter((e) => e.parent === "site-root").length;
return (
<div className="flex flex-1 flex-col">
<div className="border-b text-[20px] pt-[15px] pb-[5px] pl-1 flex space-x-3 items-center">
<div className="border-b text-[20px] pt-[15px] pb-[5px] pl-1 flex items-center">
<div>
{orglen} Organization{orglen > 1 ? "s" : ""}
</div>
<div
className="text-[12px] bg-white border rounded mx-2 px-2 hover:bg-blue-100 cursor-pointer flex items-center space-x-1 "
className="text-[12px] bg-white border rounded ml-2 px-2 hover:bg-blue-100 cursor-pointer flex items-center space-x-1 "
onClick={async () => {
const neworg = prompt("New Organization Name");
if (neworg) {
@ -167,6 +170,18 @@ const SitePicker = ({
></div>
<div>New</div>
</div>
<div
className={cx(
"text-[12px] bg-white border rounded px-2 hover:bg-blue-100 cursor-pointer flex items-center ml-1 space-x-1 "
)}
onClick={() => {
conf.group = null;
reload();
}}
>
Refresh
</div>
</div>
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
@ -265,20 +280,25 @@ const SitePicker = ({
item.name = e.currentTarget.value;
local.render();
}}
onBlur={() => {
item.name = node.text;
item.renaming = false;
local.render();
}}
onKeyDown={async (e) => {
if (e.key === "Enter") {
onBlur={async () => {
if (item.renaming) {
await db.org.update({
where: { id: item.id },
data: { name: item.name },
});
item.renaming = false;
reload();
}
}}
onKeyDown={async (e) => {
if (e.key === "Escape") {
item.name = node.text;
item.renaming = false;
local.render();
} else if (e.key === "Enter") {
e.currentTarget.blur();
}
}}
/>
) : (
<>
@ -298,10 +318,28 @@ const SitePicker = ({
</div>
</>
)}
<div className="text-[12px] bg-white border rounded px-2 hover:bg-blue-100 cursor-pointer min-h-[20px] flex items-center ">
Team: {item.users.length} user
{item.users.length > 1 ? "s" : ""}
</div>
<EdPopUser
users={item.users}
onDel={async (u) => {
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();
}}
>
<div className="text-[12px] bg-white border rounded px-2 hover:bg-blue-100 cursor-pointer min-h-[20px] flex items-center ">
Team: {item.users.length} user
{item.users.length > 1 ? "s" : ""}
</div>
</EdPopUser>
{isDropTarget && (
<div className="px-2 text-slate-500 text-[13px]">
Drop here...
@ -336,7 +374,13 @@ const SitePicker = ({
}
return (
<div
<a
href={`/ed/${item.id}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate(`/ed/${item.id}`);
}}
className={cx(
"flex flex-col ml-2 mt-1 mb-1 w-[150px] text-[14px] border bg-white hover:bg-blue-100 cursor-pointer",
css`
@ -361,10 +405,10 @@ const SitePicker = ({
</div>
</div>
<div className="edit px-1 bg-white border-l-4 border-blue-400 text-blue-400 hover:bg-blue-500 hover:text-white">
<div className="edit px-1 bg-blue-50 border-l-4 border-blue-300 text-blue-400 hover:bg-blue-500 hover:text-white">
EDIT
</div>
</div>
</a>
);
}}
/>

BIN
bun.lockb

Binary file not shown.