From 285f7e535163d50624469d6340dd82d908b8ed24 Mon Sep 17 00:00:00 2001 From: Rizky Date: Sun, 29 Oct 2023 07:35:53 +0700 Subject: [PATCH] fix --- .../render/ed/panel/popup/site/site-head.tsx | 16 +- .../render/ed/panel/popup/site/site-tree.tsx | 599 +++++++++--------- .../src/render/ed/panel/popup/site/site.tsx | 40 +- .../render/ed/panel/tree/node/item/name.tsx | 2 +- app/web/src/render/ed/panel/tree/search.tsx | 69 +- app/web/src/utils/ui/fuzzy.tsx | 62 ++ 6 files changed, 426 insertions(+), 362 deletions(-) create mode 100644 app/web/src/utils/ui/fuzzy.tsx 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 index f58d581a..47c2a832 100644 --- a/app/web/src/render/ed/panel/popup/site/site-head.tsx +++ b/app/web/src/render/ed/panel/popup/site/site-head.tsx @@ -1,6 +1,6 @@ import { NodeModel } from "@minoru/react-dnd-treeview"; import { SiteGroupItem } from "./site-tree"; -import { useGlobal } from "web-utils"; +import { useGlobal, useLocal } from "web-utils"; import { EDGlobal } from "../../../logic/ed-global"; export const EdSiteHead = ({ @@ -16,7 +16,10 @@ export const EdSiteHead = ({ update: (val: NodeModel[]) => void; reload: (id?: string) => Promise; conf: { group: any }; - local: { search: string; render: () => void }; + local: { + search: { text: string; ref: null | HTMLInputElement }; + render: () => void; + }; }) => { const p = useGlobal(EDGlobal, "EDITOR"); @@ -70,11 +73,14 @@ export const EdSiteHead = ({ { + local.search.ref = e; + }} + className="outline-none mr-2 text-[14px] w-[150px] focus:w-[250px] transition-all px-1 border focus:border-blue-500" onChange={(e) => { - local.search = e.currentTarget.value; + local.search.text = e.currentTarget.value; local.render(); }} /> 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 index 1dd41383..175a3c2b 100644 --- a/app/web/src/render/ed/panel/popup/site/site-tree.tsx +++ b/app/web/src/render/ed/panel/popup/site/site-tree.tsx @@ -1,13 +1,15 @@ import { MultiBackend, NodeModel, + NodeRender, Tree, getBackendOptions, } from "@minoru/react-dnd-treeview"; +import { useCallback } from "react"; import { DndProvider } from "react-dnd"; -import { EdPopUser } from "./site-user"; import { useGlobal, useLocal } from "web-utils"; import { EDGlobal } from "../../../logic/ed-global"; +import { EdPopUser } from "./site-user"; export type SiteGroupItem = { id: string; @@ -27,16 +29,303 @@ export const EdSiteTree = ({ update, reload, orglen, + search, }: { orglen: number; group: NodeModel[]; update: (val: NodeModel[]) => void; reload: (id?: string) => Promise; + search: string; }) => { const p = useGlobal(EDGlobal, "EDITOR"); const local = useLocal({}); const TypedTree = Tree; + const render = useCallback( + ((node, { depth, isOpen, onToggle, isDropTarget, isDragging }) => { + const gitem = node.data as SiteGroupItem; + if (!search) { + 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 (!gitem) return <>; + + if (gitem.type === "group") { + return ( +
+ {gitem.renaming ? ( + { + gitem.name = e.currentTarget.value; + local.render(); + }} + onBlur={async () => { + if (gitem.renaming && gitem.name !== node.text) { + node.text = gitem.name; + gitem.renaming = false; + local.render(); + await db.org.update({ + where: { id: gitem.id }, + data: { name: gitem.name }, + }); + reload(); + } else { + gitem.renaming = false; + local.render(); + } + }} + onKeyDown={async (e) => { + if (e.key === "Escape") { + gitem.name = node.text; + gitem.renaming = false; + local.render(); + } else if (e.key === "Enter") { + e.currentTarget.blur(); + } + }} + /> + ) : ( + <> +
{node.text}
+
{ + gitem.renaming = true; + local.render(); + }} + > +
`, + }} + >
+
+ + )} + { + await db.org_user.deleteMany({ + where: { id_org: gitem.id, id_user: u.id }, + }); + gitem.users = gitem.users.filter((e) => e.id !== u.id); + local.render(); + }} + onAdd={async (u) => { + await db.org_user.create({ + data: { id_org: gitem.id, id_user: u.id }, + }); + gitem.users = [...gitem.users, u]; + local.render(); + }} + > +
+ Team: {gitem.users.length} user + {gitem.users.length > 1 ? "s" : ""} +
+
+ {isDropTarget && ( +
+ Drop here... +
+ )} + {gitem.site_len === 0 && ( +
{ + if (confirm("Remove this organization ?")) { + await db.org_user.deleteMany({ + where: { id_org: gitem.id }, + }); + await db.org.delete({ + where: { + id: gitem.id, + }, + }); + reload(); + } + }} + > +
`, + }} + >
+
+ )} +
+ ); + } + } + + let sitem = node.data as SiteGroupItem; + + if (!sitem || (sitem && sitem.type === "group") || !gitem) return <>; + + return ( + { + e.preventDefault(); + e.stopPropagation(); + if (p.ui.popup.site) { + p.ui.popup.site(sitem.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; + } + ` + )} + > +
+
+ {node.text} +
+
+
`, + }} + >
+
{sitem.domain}  
+
+
+ {sitem.responsive === "all" && ( +
+ `, + }} + > + Responsive +
+ )} + {sitem.responsive === "mobile-only" && ( +
+ `, + }} + > + Mobile +
+ )} + {sitem.responsive === "desktop-only" && ( +
+ `, + }} + > + Desktop +
+ )} +
+
+ {orglen > 1 && ( +
{ + e.stopPropagation(); + e.preventDefault(); + + if (typeof node.parent === "string" && sitem.type === "site") { + p.ui.popup.site_form = { + group_id: node.parent, + id: sitem.id, + domain: sitem.domain, + name: sitem.name, + responsive: sitem.responsive, + }; + p.render(); + } + }} + > + Drag me +
+ )} +
{ + e.stopPropagation(); + e.preventDefault(); + + if (typeof node.parent === "string" && sitem.type === "site") { + p.ui.popup.site_form = { + group_id: node.parent, + id: sitem.id, + domain: sitem.domain, + name: sitem.name, + responsive: sitem.responsive, + }; + p.render(); + } + }} + > + Edit Site + `, + }} + > +
+
+ ); + }) as NodeRender, + [search] + ); + return ( <>} 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); - } - ` + "flex flex-1 items-stretch overflow-auto", + search ? "flex-row" : "flex-col", + !search && + 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 - `, - }} - > -
-
- ); - }} + render={render} />
); 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 c7abdec3..72aae5e8 100644 --- a/app/web/src/render/ed/panel/popup/site/site.tsx +++ b/app/web/src/render/ed/panel/popup/site/site.tsx @@ -8,6 +8,10 @@ import { EdFormSite } from "./site-form"; import { EdSiteHead } from "./site-head"; import { EdSiteTree, SiteGroupItem } from "./site-tree"; +import uFuzzy, { Info } from "@leeoniya/ufuzzy"; +import { fuzzy } from "../../../../../utils/ui/fuzzy"; +const uf = new uFuzzy({}); + const conf = { group: null as any }; export const EdPopSite = () => { @@ -162,24 +166,54 @@ const SitePicker = ({ reload: (id?: string) => Promise; }) => { const local = useLocal({ - search: "", + search: { + text: "", + ref: null as null | HTMLInputElement, + }, }); + + let result = group; + if (local.search.text) { + const found = fuzzy(group, "text", local.search.text); + result = found.map((e) => ({ ...e, parent: "site-root" })); + } + + useEffect(() => { + const keydown = (e: KeyboardEvent) => { + const el = document.activeElement as HTMLDivElement; + if (el.classList.contains("modal")) { + local.search.ref?.focus(); + } + }; + addEventListener("keydown", keydown); + return () => { + removeEventListener("keydown", keydown); + }; + }, []); + const orglen = group.filter((e) => e.parent === "site-root").length; return (
+ + {result.length === 0 && local.search.text && ( +
+ No search results found. +
+ )}
); diff --git a/app/web/src/render/ed/panel/tree/node/item/name.tsx b/app/web/src/render/ed/panel/tree/node/item/name.tsx index 28bec61e..3e1e069d 100644 --- a/app/web/src/render/ed/panel/tree/node/item/name.tsx +++ b/app/web/src/render/ed/panel/tree/node/item/name.tsx @@ -64,7 +64,7 @@ export const EdTreeName = ({ /> ) : (
- {node.data?.el || item.name} + {node.text} {/*
{item.id}
*/}
)} diff --git a/app/web/src/render/ed/panel/tree/search.tsx b/app/web/src/render/ed/panel/tree/search.tsx index 62d8e01c..60e21479 100644 --- a/app/web/src/render/ed/panel/tree/search.tsx +++ b/app/web/src/render/ed/panel/tree/search.tsx @@ -1,10 +1,8 @@ -import { useGlobal, useLocal } from "web-utils"; -import { EDGlobal, EdMeta, PG, active } from "../../logic/ed-global"; import { NodeModel } from "@minoru/react-dnd-treeview"; - -import uFuzzy from "@leeoniya/ufuzzy"; -import { useEffect, useState } from "react"; -const uf = new uFuzzy({}); +import { useEffect } from "react"; +import { useGlobal, useLocal } from "web-utils"; +import { EDGlobal, EdMeta, PG } from "../../logic/ed-global"; +import { fuzzy } from "../../../../utils/ui/fuzzy"; export const EdTreeSearch = () => { const p = useGlobal(EDGlobal, "EDITOR"); @@ -105,61 +103,12 @@ export const doTreeSearch = (p: PG) => { let tree: Record }> = {}; if (p.ui.tree.search_mode.Name) { - const [idxs, info] = uf.search( - p.page.tree.map((e) => e.text), - p.ui.tree.search - ); - if (idxs && info) { - let i = 0; - for (const idx of idxs) { - const item = p.page.tree[idx]; - const range = info.ranges[i]; - let text = ""; + const found = fuzzy(p.page.tree, "text", p.ui.tree.search); - let cur = range.shift(); - let open = true; - for (let i = 0; i < item.text.length; i++) { - if (typeof cur === "number") { - if (i === cur) { - if (open) { - text += ``; - open = false; - } else { - text += ``; - open = true; - } - cur = range.shift(); - } - text += item.text[i]; - } else { - text += item.text[i]; - } - } - const el = ( -
- ); - tree[item.id] = { - idx: i, - node: { - ...item, - parent: "root", - data: item.data - ? { - ...item.data, - el, - } - : undefined, - }, - }; - i++; + let i = 0; + for (const row of found) { + if (row.data) { + tree[row.id] = { idx: i++, node: { ...row, parent: "root" } }; } } } diff --git a/app/web/src/utils/ui/fuzzy.tsx b/app/web/src/utils/ui/fuzzy.tsx new file mode 100644 index 00000000..b174ccca --- /dev/null +++ b/app/web/src/utils/ui/fuzzy.tsx @@ -0,0 +1,62 @@ +import uFuzzy from "@leeoniya/ufuzzy"; +const uf = new uFuzzy({}); + +export const fuzzy = ( + array: T[], + field: keyof T, + search: string +) => { + const [idxs, info] = uf.search( + array.map((e) => e[field]) as string[], + search + ); + + if (idxs && info) { + const result = [] as T[]; + let i = 0; + for (const idx of idxs) { + const item = array[idx]; + const range = [...info.ranges[i]]; + const val = item[field] as string; + + let cur = range.shift(); + let openBold = false; + let text = ""; + for (let i = 0; i < val.length; i++) { + if (typeof cur === "number") { + if (i === cur) { + if (!openBold) { + text += ``; + openBold = true; + } else { + text += ``; + openBold = false; + } + cur = range.shift(); + } + text += val[i]; + } else { + text += val[i]; + } + } + if (openBold) { + text += ``; + } + + const el = ( +
+ ); + result.push({ ...item, [field]: el }); + } + return result; + } + return array; +};