wip fix prasi file
This commit is contained in:
parent
db45f936fc
commit
9ba10f56d1
File diff suppressed because one or more lines are too long
|
|
@ -12,6 +12,7 @@
|
|||
"@minoru/react-dnd-treeview": "^3.4.4",
|
||||
"@wojtekmaj/react-qr-svg": "^1.0.0",
|
||||
"constrained-editor-plugin": "^1.3.0",
|
||||
"react-resizable-panels": "^2.0.9",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@paralleldrive/cuid2": "2.2.2",
|
||||
"react-contenteditable": "^3.3.7",
|
||||
|
|
|
|||
|
|
@ -47,9 +47,10 @@ export const apiProxy = (api_url: string) => {
|
|||
|
||||
if (api_ref) {
|
||||
if (actionName === "_raw") {
|
||||
const url = `${base_url}${rest.join("")}`;
|
||||
const pathname = rest[0];
|
||||
const url = `${base_url}${pathname}`;
|
||||
|
||||
const result = await fetchSendApi(url, rest);
|
||||
const result = await fetchSendApi(url, rest.slice(1));
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,55 +4,57 @@
|
|||
let w = (typeof window !== "undefined" ? window : null) as any;
|
||||
|
||||
export const fetchViaProxy = async (
|
||||
url: string,
|
||||
target_url: string,
|
||||
data?: any,
|
||||
_headers?: any
|
||||
) => {
|
||||
const headers = { ..._headers };
|
||||
|
||||
let body = data;
|
||||
let body = null as any;
|
||||
let isFile = false;
|
||||
|
||||
const formatSingle = async (data: any) => {
|
||||
if (w !== null) {
|
||||
if (!(data instanceof w.FormData || data instanceof w.File)) {
|
||||
headers["content-type"] = "application/json";
|
||||
} else {
|
||||
if (data instanceof w.File) {
|
||||
isFile = true;
|
||||
let ab = await new Promise<ArrayBuffer | undefined>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", (e) => {
|
||||
resolve(e.target?.result as ArrayBuffer);
|
||||
});
|
||||
reader.readAsArrayBuffer(data);
|
||||
});
|
||||
if (ab) {
|
||||
data = new File([ab], data.name);
|
||||
}
|
||||
}
|
||||
const files: File[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
for (const item of data) {
|
||||
if (item instanceof File) {
|
||||
files.push(item);
|
||||
isFile = true;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
body = await Promise.all(data.map((e) => formatSingle(e)));
|
||||
} else {
|
||||
body = await formatSingle(data);
|
||||
} else if (data instanceof File) {
|
||||
isFile = true;
|
||||
files.push(data);
|
||||
}
|
||||
if (!isFile) {
|
||||
body = JSON.stringify(body);
|
||||
body = JSON.stringify(data);
|
||||
headers["content-type"] = "aplication/json";
|
||||
} else {
|
||||
const fd = new FormData();
|
||||
for (const file of files) {
|
||||
fd.append(file.name, file);
|
||||
}
|
||||
body = fd;
|
||||
delete headers["content-type"];
|
||||
headers["enctype"] = `multipart/form-data;`;
|
||||
}
|
||||
|
||||
const base = new URL(url);
|
||||
const to_url = new URL(target_url);
|
||||
|
||||
if (w !== null) {
|
||||
const cur = new URL(location.href);
|
||||
const cur_url = new URL(location.href);
|
||||
let final_url = "";
|
||||
|
||||
if (cur.host === base.host) {
|
||||
if (to_url.host === cur_url.host) {
|
||||
final_url = to_url.toString();
|
||||
} else {
|
||||
final_url = `${cur_url.protocol}//${
|
||||
cur_url.host
|
||||
}/_proxy/${encodeURIComponent(to_url.toString())}`;
|
||||
}
|
||||
|
||||
if (final_url) {
|
||||
const res = await fetch(
|
||||
base.pathname,
|
||||
final_url,
|
||||
data
|
||||
? {
|
||||
method: "POST",
|
||||
|
|
@ -67,83 +69,7 @@ export const fetchViaProxy = async (
|
|||
} catch (e) {
|
||||
return raw;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
data instanceof File ||
|
||||
(Array.isArray(data) && data[0] instanceof File)
|
||||
) {
|
||||
const target = new URL(url);
|
||||
_headers["content-type"] = "multipart/form-data";
|
||||
if (data instanceof File) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", data);
|
||||
const res = await fetch(target.pathname, {
|
||||
body: formData,
|
||||
method: "POST",
|
||||
headers: _headers,
|
||||
});
|
||||
return await res.text();
|
||||
} else {
|
||||
const formData = new FormData();
|
||||
let idx = 1;
|
||||
for (const file of data) {
|
||||
formData.append("file-" + idx++, file);
|
||||
}
|
||||
const res = await fetch(target.pathname, {
|
||||
body: formData,
|
||||
method: "POST",
|
||||
headers: _headers,
|
||||
});
|
||||
return await res.text();
|
||||
}
|
||||
} else {
|
||||
const res = await fetch(`${w.basehost ? w.basehost : ""}/_proxy`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify([
|
||||
{
|
||||
url,
|
||||
body,
|
||||
headers,
|
||||
},
|
||||
]),
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
let text = "";
|
||||
try {
|
||||
text = await res.text();
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
let formatted_body = null;
|
||||
try {
|
||||
formatted_body = JSON.stringify(JSON.parse(body), null, 2);
|
||||
} catch (e) {}
|
||||
|
||||
console.warn(
|
||||
`\n\n⚡ Failed to JSON.parse fetch result of ${url}:\n\n${JSON.stringify(
|
||||
text
|
||||
)} \n\nwith params:\n${formatted_body}`
|
||||
);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const res = await fetch(
|
||||
base,
|
||||
data
|
||||
? {
|
||||
method: "POST",
|
||||
body,
|
||||
headers,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
const raw = await res.text();
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { IItem } from "../../../utils/types/item";
|
|||
import { DCode, DComp, DPage, IRoot } from "../../../utils/types/root";
|
||||
import { GenMetaP, IMeta as LogicMeta } from "../../vi/utils/types";
|
||||
import { createRouter } from "radix3";
|
||||
import { FEntry } from "../panel/file/type";
|
||||
export type IMeta = LogicMeta;
|
||||
|
||||
export const EmptySite = {
|
||||
|
|
@ -231,7 +232,22 @@ export const EDGlobal = {
|
|||
open: {} as Record<string, string[]>,
|
||||
},
|
||||
popup: {
|
||||
file: { enabled: false, open: true },
|
||||
file: {
|
||||
enabled: false,
|
||||
open: true,
|
||||
path: "/",
|
||||
expanded: JSON.parse(
|
||||
localStorage.getItem("panel-file-expanded") || "{}"
|
||||
) as Record<string, string[]>,
|
||||
entry: {} as Record<string, FEntry[]>,
|
||||
tree: [] as NodeModel<FEntry>[],
|
||||
renaming: "",
|
||||
ctx_path: "",
|
||||
ctx_menu_event: null as null | React.MouseEvent<
|
||||
HTMLElement,
|
||||
MouseEvent
|
||||
>,
|
||||
},
|
||||
code: {
|
||||
init: false,
|
||||
open: false,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { useGlobal, useLocal } from "web-utils";
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||
import { useGlobal } from "web-utils";
|
||||
import { apiProxy } from "../../../../base/load/api/api-proxy";
|
||||
import { Modal } from "../../../../utils/ui/modal";
|
||||
import { EDGlobal } from "../../logic/ed-global";
|
||||
import { EdFileTree } from "./file-tree";
|
||||
import { EdFileTree, reloadFileTree } from "./file-tree";
|
||||
import { FEntry } from "./type";
|
||||
|
||||
import { EdFileTop } from "./file-top";
|
||||
import { uploadFile } from "./file-upload";
|
||||
export const EdFileBrowser = () => {
|
||||
const p = useGlobal(EDGlobal, "EDITOR");
|
||||
const local = useLocal({
|
||||
entry: {} as Record<string, FEntry[]>,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!p.script.api && p.site.config?.api_url) {
|
||||
|
|
@ -19,20 +18,21 @@ export const EdFileBrowser = () => {
|
|||
p.render();
|
||||
}
|
||||
|
||||
if (!p.script.api) return () => {};
|
||||
|
||||
p.script.api._raw(`/_file/?dir`).then((e: FEntry[]) => {
|
||||
if (Array.isArray(e)) {
|
||||
local.entry = { "/": e };
|
||||
p.ui.popup.file.entry = { "/": e };
|
||||
p.ui.popup.file.enabled = true;
|
||||
p.render();
|
||||
}
|
||||
});
|
||||
|
||||
reloadFileTree(p);
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: any[]) => {
|
||||
console.log(acceptedFiles);
|
||||
}, []);
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
onDrop: (files) => uploadFile(p, files),
|
||||
noClick: true,
|
||||
});
|
||||
|
||||
|
|
@ -74,44 +74,60 @@ export const EdFileBrowser = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cx(
|
||||
"bg-white select-none fixed inset-[50px] flex",
|
||||
css`
|
||||
outline: none;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive && (
|
||||
<div className="absolute inset-0 flex items-center justify-center flex-col bg-blue-50 border-4 border-blue-500">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
className="lucide lucide-upload"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
|
||||
<path d="M17 8L12 3 7 8"></path>
|
||||
<path d="M12 3L12 15"></path>
|
||||
</svg>
|
||||
<div>Drag Here to Upload</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx("bg-white select-none fixed inset-[50px] flex")}>
|
||||
<PanelGroup direction="horizontal" className="text-sm">
|
||||
<Panel
|
||||
id="tree"
|
||||
defaultSize={parseInt(
|
||||
localStorage.getItem("panel-file-left") || "18"
|
||||
)}
|
||||
minSize={8}
|
||||
order={1}
|
||||
className="border-r"
|
||||
onResize={(e) => {
|
||||
localStorage.setItem("panel-file-left", e + "");
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<EdFileTree />
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel order={2}>
|
||||
<div className="flex-1 flex h-full flex-col">
|
||||
<EdFileTop />
|
||||
|
||||
<div className="flex flex-1 items-stretch">
|
||||
<div className="flex min-w-[200px] border-r">
|
||||
<EdFileTree entry={local.entry} />
|
||||
</div>
|
||||
<div>Moko</div>
|
||||
</div>
|
||||
<div
|
||||
className={cx("flex-1 flex h-full outline-none relative")}
|
||||
{...getRootProps()}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive && (
|
||||
<div className="absolute inset-0 flex items-center justify-center flex-col bg-blue-50 border-4 border-blue-500">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
className="lucide lucide-upload"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
|
||||
<path d="M17 8L12 3 7 8"></path>
|
||||
<path d="M12 3L12 15"></path>
|
||||
</svg>
|
||||
<div>Drag Here to Upload</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import { useGlobal } from "web-utils";
|
||||
import { EDGlobal, PG } from "../../logic/ed-global";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { uploadFile } from "./file-upload";
|
||||
|
||||
export const EdFileTop = () => {
|
||||
const p = useGlobal(EDGlobal, "EDITOR");
|
||||
const f = p.ui.popup.file;
|
||||
const paths = f.path.split("/").filter((e) => e);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop: (files) => uploadFile(p, files),
|
||||
noDrag: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cx("border-b flex flex-col items-stretch")}>
|
||||
<div className={cx("flex p-1")}>
|
||||
<div className={breadClass(p)} onClick={() => breadClick(p, "/")}>
|
||||
/
|
||||
</div>
|
||||
{paths.map((part, idx) => {
|
||||
const npath: string[] = [];
|
||||
for (let i = 0; i <= idx; i++) npath.push(paths[i]);
|
||||
return (
|
||||
<div
|
||||
key={part}
|
||||
className={breadClass(p)}
|
||||
onClick={() => breadClick(p, "/" + npath.join("/"))}
|
||||
>
|
||||
{part}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={cx("p-1 border-t flex")}>
|
||||
<div className={topClass(p)} {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
Upload
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const breadClick = (p: PG, path: string) => {
|
||||
p.ui.popup.file.path = path;
|
||||
p.render();
|
||||
};
|
||||
|
||||
const breadClass = (p: PG, className?: string) =>
|
||||
cx("border px-2 mr-1 rounded-sm cursor-pointer hover:bg-blue-50", className);
|
||||
|
||||
const topClass = (p: PG, className?: string) =>
|
||||
cx(
|
||||
"border px-2 mr-1 rounded-sm cursor-pointer hover:bg-blue-100 hover:text-blue-700 border-slate-600 hover:border-blue-600 ",
|
||||
className
|
||||
);
|
||||
|
|
@ -1,52 +1,323 @@
|
|||
import {
|
||||
Tree as DNDTree,
|
||||
MultiBackend,
|
||||
NodeModel,
|
||||
Tree as DNDTree,
|
||||
getBackendOptions,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { FEntry } from "./type";
|
||||
import { FC } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { useGlobal, useLocal } from "web-utils";
|
||||
import { Menu, MenuItem } from "../../../../utils/ui/context-menu";
|
||||
import { EDGlobal, PG } from "../../logic/ed-global";
|
||||
import { FEntry } from "./type";
|
||||
|
||||
const Tree = DNDTree<FEntry>;
|
||||
|
||||
export const EdFileTree: FC<{ entry: Record<string, FEntry[]> }> = ({
|
||||
entry,
|
||||
}) => {
|
||||
const tree: NodeModel<FEntry>[] = [];
|
||||
for (const [path, entries] of Object.entries(entry)) {
|
||||
const arr = path.split("/");
|
||||
const name = arr.pop() || "/";
|
||||
tree.push({ id: path, text: name, parent: arr.join("/") });
|
||||
for (const e of entries) {
|
||||
if (e.type === "dir") {
|
||||
tree.push({
|
||||
id: (path === "/" ? "" : path) + "/" + e.name,
|
||||
text: e.name,
|
||||
parent: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
export const EdFileTree: FC<{}> = ({}) => {
|
||||
const p = useGlobal(EDGlobal, "EDITOR");
|
||||
|
||||
if (!p.ui.popup.file.expanded[p.site.id]) {
|
||||
p.ui.popup.file.expanded[p.site.id] = [];
|
||||
}
|
||||
|
||||
return (
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={tree}
|
||||
tree={p.ui.popup.file.tree}
|
||||
dragPreviewRender={() => <></>}
|
||||
rootId=""
|
||||
initialOpen={true}
|
||||
onDrop={async (newTree, opt) => {}}
|
||||
render={(node, { depth, isOpen, onToggle, hasChild }) => (
|
||||
<div
|
||||
className={cx(css`
|
||||
padding-left: ${(depth * 10) + 10}px;
|
||||
`)}
|
||||
>
|
||||
{node.text}
|
||||
</div>
|
||||
)}
|
||||
initialOpen={[...(p.ui.popup.file.expanded[p.site.id] || []), "/"]}
|
||||
canDrop={(newTree, opt) => {
|
||||
const to = opt.dropTargetId + "";
|
||||
const from = opt.dragSourceId + "";
|
||||
|
||||
if (to.startsWith(from)) return false;
|
||||
|
||||
const from_arr = from.split("/").filter((e) => e);
|
||||
const to_arr = to.split("/").filter((e) => e);
|
||||
if (
|
||||
from_arr.slice(0, from_arr.length - 1).join("/") ===
|
||||
to_arr.join("/")
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}}
|
||||
onDrop={async (newTree, { dropTargetId, dragSourceId }) => {
|
||||
await p.script.api._raw(`/_file${dragSourceId}?move=${dropTargetId}`);
|
||||
await reloadFileTree(p);
|
||||
}}
|
||||
render={(
|
||||
node,
|
||||
{ depth, isOpen, onToggle, hasChild, isDragging, isDropTarget }
|
||||
) => <TreeItem node={node} depth={depth} isDropTarget={isDropTarget} />}
|
||||
></Tree>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const TreeItem: FC<{
|
||||
node: NodeModel<FEntry>;
|
||||
depth: number;
|
||||
isDropTarget: boolean;
|
||||
}> = ({ node, depth, isDropTarget }) => {
|
||||
const p = useGlobal(EDGlobal, "EDITOR");
|
||||
const path = node.id + "";
|
||||
const f = p.ui.popup.file;
|
||||
const local = useLocal({ renaming: node.text });
|
||||
|
||||
const expanded = f.expanded[p.site.id]?.includes(path);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"flex items-center space-x-1 flex-nowrap hover:bg-blue-50 py-[2px]",
|
||||
isDropTarget && "bg-blue-500 text-white",
|
||||
css`
|
||||
padding-left: ${depth * 10 + 10}px;
|
||||
`,
|
||||
f.path === path && "border-r-2 bg-blue-100 border-r-blue-700"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (f.path === path) {
|
||||
toggleDir(p, path);
|
||||
}
|
||||
|
||||
f.path = path;
|
||||
p.render();
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!expanded) {
|
||||
toggleDir(p, path);
|
||||
}
|
||||
f.ctx_path = path;
|
||||
f.ctx_menu_event = e;
|
||||
p.render();
|
||||
}}
|
||||
>
|
||||
{f.ctx_menu_event && (
|
||||
<Menu
|
||||
mouseEvent={f.ctx_menu_event}
|
||||
onClose={() => {
|
||||
setTimeout(() => {
|
||||
f.ctx_path = "";
|
||||
f.ctx_menu_event = null;
|
||||
p.render();
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
label={"New Folder"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
f.ctx_menu_event = null;
|
||||
p.render();
|
||||
setTimeout(() => {
|
||||
f.tree.push({
|
||||
id: f.ctx_path + "/new_folder",
|
||||
parent: f.ctx_path,
|
||||
text: "new_folder",
|
||||
data: {
|
||||
name: "new_folder",
|
||||
type: "dir",
|
||||
size: 0,
|
||||
},
|
||||
});
|
||||
f.expanded[p.site.id]?.push(f.ctx_path);
|
||||
p.render();
|
||||
f.path = f.ctx_path + "/new_folder";
|
||||
f.renaming = f.ctx_path + "/new_folder";
|
||||
f.ctx_path = "";
|
||||
p.render();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
label={"Rename"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
f.path = f.ctx_path;
|
||||
f.renaming = f.ctx_path;
|
||||
p.render();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
label={"Delete"}
|
||||
disabled={
|
||||
!(f.entry[f.ctx_path] && f.entry[f.ctx_path]?.length === 0)
|
||||
}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!(f.entry[f.ctx_path] && f.entry[f.ctx_path]?.length === 0)) {
|
||||
alert("Can only delete empty folder!");
|
||||
} else {
|
||||
await p.script.api._raw(`/_file${f.ctx_path}?del`);
|
||||
await reloadFileTree(p);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleDir(p, path);
|
||||
}}
|
||||
>
|
||||
{expanded || path === "/" ? <FolderOpen /> : <Folder />}
|
||||
</div>
|
||||
{f.renaming === path ? (
|
||||
<input
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
value={local.renaming}
|
||||
autoFocus
|
||||
onChange={(e) => {
|
||||
local.renaming = e.currentTarget.value.replace(/\W/gi, "_");
|
||||
local.render();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.select();
|
||||
}}
|
||||
onBlur={async () => {
|
||||
if (local.renaming !== node.text) {
|
||||
node.text = local.renaming;
|
||||
const res = await p.script.api._raw(
|
||||
`/_file${f.renaming}?rename=${local.renaming}`
|
||||
);
|
||||
|
||||
if (res && res.newname) {
|
||||
f.path = res.newname;
|
||||
}
|
||||
await reloadFileTree(p);
|
||||
}
|
||||
f.renaming = "";
|
||||
p.render();
|
||||
}}
|
||||
className="flex-1 border border-blue-500 outline-none"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 text-ellipsis truncate">{node.text}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const size = 14;
|
||||
|
||||
const FolderOpen = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M6 14l1.5-2.9A2 2 0 019.24 10H20a2 2 0 011.94 2.5l-1.54 6a2 2 0 01-1.95 1.5H4a2 2 0 01-2-2V5a2 2 0 012-2h3.9a2 2 0 011.69.9l.81 1.2a2 2 0 001.67.9H18a2 2 0 012 2v2"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Folder = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M20 20a2 2 0 002-2V8a2 2 0 00-2-2h-7.9a2 2 0 01-1.69-.9L9.6 3.9A2 2 0 007.93 3H4a2 2 0 00-2 2v13a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const toggleDir = (p: PG, path: string) => {
|
||||
if (path === "/") return;
|
||||
const expanded = p.ui.popup.file.expanded[p.site.id];
|
||||
if (expanded) {
|
||||
if (expanded.includes(path)) {
|
||||
p.ui.popup.file.expanded[p.site.id] = expanded.filter((e) => e !== path);
|
||||
} else {
|
||||
p.ui.popup.file.expanded[p.site.id] = [...expanded, path];
|
||||
}
|
||||
}
|
||||
localStorage.setItem(
|
||||
"panel-file-expanded",
|
||||
JSON.stringify(p.ui.popup.file.expanded)
|
||||
);
|
||||
reloadFileTree(p);
|
||||
};
|
||||
|
||||
export const reloadFileTree = async (p: PG) => {
|
||||
const exp = p.ui.popup.file.expanded[p.site.id];
|
||||
|
||||
const e = await p.script.api._raw(`/_file/?dir`);
|
||||
if (Array.isArray(e)) {
|
||||
p.ui.popup.file.entry = { "/": e };
|
||||
}
|
||||
|
||||
if (exp) {
|
||||
for (const [k, v] of Object.entries(p.ui.popup.file.entry)) {
|
||||
if (p.ui.popup.file.entry[k] && !exp.includes(k) && k !== "/") {
|
||||
delete p.ui.popup.file.entry[k];
|
||||
}
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
for (const e of exp) {
|
||||
if (e) {
|
||||
if (!p.ui.popup.file.entry[e]) {
|
||||
promises.push(
|
||||
p.script.api._raw(`/_file${e}/?dir`).then((fe: FEntry[]) => {
|
||||
if (Array.isArray(fe)) {
|
||||
p.ui.popup.file.entry[e] = fe;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
const tree: NodeModel<FEntry>[] = p.ui.popup.file.tree;
|
||||
|
||||
tree.length = 0;
|
||||
tree.push({ id: "/", text: "/", parent: "" });
|
||||
const added = new Set<string>(["/"]);
|
||||
for (const [path, entries] of Object.entries(p.ui.popup.file.entry)) {
|
||||
const arr = path.split("/");
|
||||
for (const e of entries) {
|
||||
let id = path + (path.endsWith("/") ? "" : "/") + e.name;
|
||||
if (!id.startsWith("/")) id = "/" + id;
|
||||
|
||||
if (e.type === "dir" && !added.has(id)) {
|
||||
tree.push({
|
||||
id,
|
||||
text: e.name,
|
||||
parent: path || "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
p.ui.popup.file.tree = tree.sort(
|
||||
(a, b) => (a.id + "").length - (b.id + "").length
|
||||
);
|
||||
p.render();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import { fetchViaProxy } from "../../../../base/load/proxy";
|
||||
import { PG } from "../../logic/ed-global";
|
||||
|
||||
export const uploadFile = async (p: PG, files: File[]) => {
|
||||
await p.script.api._raw(`/_upload?to=${p.ui.popup.file.path}`, ...files);
|
||||
};
|
||||
|
|
@ -42,6 +42,7 @@ export const viLoadLegacy = async (vi: {
|
|||
|
||||
let api_url = vi.site.api_url;
|
||||
if (!api_url) api_url = ((site.config as any) || {}).api_url || "";
|
||||
if (!api_url) return;
|
||||
|
||||
try {
|
||||
const apiURL = new URL(api_url);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { apiContext } from "service-srv";
|
||||
import { gzipAsync } from "../../../app/srv/ws/sync/entity/zlib";
|
||||
import { CORS_HEADERS } from "../server/serve-api";
|
||||
import brotliPromise from "brotli-wasm";
|
||||
|
|
@ -6,44 +7,30 @@ const brotli = await brotliPromise;
|
|||
|
||||
export const _ = {
|
||||
url: "/_proxy/*",
|
||||
async api(arg: {
|
||||
url: string;
|
||||
method: "POST" | "GET";
|
||||
headers: any;
|
||||
body: any;
|
||||
}) {
|
||||
if ((!!arg && !arg.url) || !arg) return new Response(null, { status: 403 });
|
||||
raw: true,
|
||||
async api() {
|
||||
const { req } = apiContext(this);
|
||||
|
||||
const res = await fetch(
|
||||
arg.url,
|
||||
arg.body
|
||||
? {
|
||||
method: arg.method || "POST",
|
||||
headers: arg.headers,
|
||||
body: arg.body,
|
||||
}
|
||||
: {
|
||||
headers: arg.headers,
|
||||
}
|
||||
);
|
||||
|
||||
let body: any = null;
|
||||
const headers: any = {};
|
||||
res.headers.forEach((v, k) => {
|
||||
headers[k] = v;
|
||||
});
|
||||
|
||||
body = await res.arrayBuffer();
|
||||
|
||||
if (headers["content-encoding"] === "gzip") {
|
||||
body = await gzipAsync(new Uint8Array(body));
|
||||
} else if (headers["content-encoding"] === "br") {
|
||||
body = brotli.decompress(new Uint8Array(body));
|
||||
delete headers["content-encoding"];
|
||||
} else {
|
||||
delete headers["content-encoding"];
|
||||
try {
|
||||
const url = new URL(decodeURIComponent(req.params["_"]));
|
||||
const body = await req.arrayBuffer();
|
||||
return await fetch(url, {
|
||||
method: req.method || "POST",
|
||||
headers: req.headers,
|
||||
body,
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
status: "failed",
|
||||
reason: e.message,
|
||||
}),
|
||||
{
|
||||
status: 403,
|
||||
headers: { "content-type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(body, { headers: { ...headers, ...CORS_HEADERS } });
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export const prepareApiRoutes = async () => {
|
|||
const route = {
|
||||
url: api._.url,
|
||||
args,
|
||||
raw: !!api._.raw,
|
||||
fn: api._.api,
|
||||
path: importPath.substring((root || path).length + 1),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ export const createServer = async () => {
|
|||
|
||||
g.server = Bun.serve({
|
||||
port: g.port,
|
||||
maxRequestBodySize: 9999999,
|
||||
development: true,
|
||||
maxRequestBodySize: 1024 * 1024 * 128,
|
||||
websocket: await serveWS(wsHandler),
|
||||
async fetch(req, server) {
|
||||
const url = new URL(req.url);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const serveAPI = {
|
|||
return params[e];
|
||||
});
|
||||
|
||||
if (req.method !== "GET") {
|
||||
if (req.method !== "GET" && !found.raw) {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response("OK", {
|
||||
headers: CORS_HEADERS,
|
||||
|
|
@ -57,7 +57,7 @@ export const serveAPI = {
|
|||
const text = await req.text();
|
||||
const json = JSON.parse(text, replacer);
|
||||
|
||||
if (typeof json === "object") {
|
||||
if (typeof json === "object" && !!json) {
|
||||
if (Array.isArray(json)) {
|
||||
args = json;
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { dbProxy } from "../../../app/web/src/base/load/db/db-proxy";
|
|||
type SingleRoute = {
|
||||
url: string;
|
||||
args: string[];
|
||||
raw: boolean;
|
||||
fn: (...arg: any[]) => Promise<any>;
|
||||
path: string;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue