213 lines
5.5 KiB
TypeScript
213 lines
5.5 KiB
TypeScript
import { type MouseEvent as ReactMouseEvent, useState } from "react";
|
|
|
|
import Icon from "../../components/Icon";
|
|
import type {
|
|
ScriptItem,
|
|
WorkspaceDirectory,
|
|
} from "../../services/api";
|
|
|
|
export type WorkspaceTreeTarget = {
|
|
kind: "root" | "directory" | "file";
|
|
path: string;
|
|
script?: ScriptItem;
|
|
};
|
|
|
|
type WorkspaceTreeProps = {
|
|
title: string;
|
|
scripts: ScriptItem[];
|
|
directories: WorkspaceDirectory[];
|
|
selectedId: string | null;
|
|
onSelect: (id: string) => void;
|
|
onContextMenu?: (
|
|
event: ReactMouseEvent,
|
|
target: WorkspaceTreeTarget,
|
|
) => void;
|
|
readOnly?: boolean;
|
|
};
|
|
|
|
type WorkspaceTreeItemsProps = Omit<WorkspaceTreeProps, "title" | "readOnly"> & {
|
|
path: string;
|
|
depth: number;
|
|
};
|
|
|
|
function formatTime(value: string) {
|
|
return new Intl.DateTimeFormat("zh-CN", {
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
}).format(new Date(value));
|
|
}
|
|
|
|
export function scriptIcon(item: ScriptItem) {
|
|
return item.script_type === "notebook" ? "notebook" : "python";
|
|
}
|
|
|
|
function ownedScriptPath(item: ScriptItem) {
|
|
return item.relative_path.replaceAll("\\", "/").split("/").slice(2).join("/");
|
|
}
|
|
|
|
function parentOf(path: string) {
|
|
const parts = path.split("/");
|
|
parts.pop();
|
|
return parts.join("/");
|
|
}
|
|
|
|
export function WorkspaceTreeGroup({
|
|
title,
|
|
scripts,
|
|
directories,
|
|
selectedId,
|
|
onSelect,
|
|
onContextMenu,
|
|
readOnly = false,
|
|
}: WorkspaceTreeProps) {
|
|
const [open, setOpen] = useState(true);
|
|
return (
|
|
<div className="tree-group">
|
|
<button
|
|
className={`tree-group__title${open ? " is-open" : ""}`}
|
|
type="button"
|
|
aria-expanded={open}
|
|
onClick={() => setOpen((current) => !current)}
|
|
onContextMenu={onContextMenu
|
|
? (event) => onContextMenu(event, { kind: "root", path: "" })
|
|
: undefined}
|
|
>
|
|
<Icon name="chevron" size={14} />
|
|
<Icon name="folder" size={17} />
|
|
<span>{title}</span>
|
|
<em>{scripts.length}</em>
|
|
</button>
|
|
{open && (
|
|
<div className="tree-group__items">
|
|
<WorkspaceTreeItems
|
|
path=""
|
|
depth={0}
|
|
scripts={scripts}
|
|
directories={directories}
|
|
selectedId={selectedId}
|
|
onSelect={onSelect}
|
|
onContextMenu={onContextMenu}
|
|
/>
|
|
{scripts.length === 0 && directories.length === 0 && (
|
|
<p className="tree-group__empty">
|
|
{readOnly ? "暂无文件" : "右键此目录即可新建或上传"}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function WorkspaceTreeItems({
|
|
path,
|
|
depth,
|
|
scripts,
|
|
directories,
|
|
selectedId,
|
|
onSelect,
|
|
onContextMenu,
|
|
}: WorkspaceTreeItemsProps) {
|
|
const childDirectories = directories.filter(
|
|
(item) => item.parent_path === path,
|
|
);
|
|
const childScripts = scripts.filter(
|
|
(item) => parentOf(ownedScriptPath(item)) === path,
|
|
);
|
|
return (
|
|
<>
|
|
{childDirectories.map((directory) => (
|
|
<DirectoryBranch
|
|
key={directory.path}
|
|
directory={directory}
|
|
depth={depth}
|
|
scripts={scripts}
|
|
directories={directories}
|
|
selectedId={selectedId}
|
|
onSelect={onSelect}
|
|
onContextMenu={onContextMenu}
|
|
/>
|
|
))}
|
|
{childScripts.map((item) => (
|
|
<button
|
|
className={`script-row${
|
|
selectedId === item.script_id ? " script-row--active" : ""
|
|
}`}
|
|
style={{ paddingLeft: 20 + depth * 16 }}
|
|
key={item.script_id}
|
|
type="button"
|
|
onClick={() => onSelect(item.script_id)}
|
|
onContextMenu={onContextMenu
|
|
? (event) => onContextMenu(event, {
|
|
kind: "file",
|
|
path: ownedScriptPath(item),
|
|
script: item,
|
|
})
|
|
: undefined}
|
|
>
|
|
<span className={`file-icon file-icon--${item.script_type}`}>
|
|
<Icon name={scriptIcon(item)} size={17} />
|
|
</span>
|
|
<span className="script-row__copy">
|
|
<strong title={item.script_name}>{item.script_name}</strong>
|
|
<small>{formatTime(item.updated_at)}</small>
|
|
</span>
|
|
{item.visibility !== "private" && (
|
|
<span className="visibility-dot" title="Workspace 可见" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function DirectoryBranch({
|
|
directory,
|
|
depth,
|
|
scripts,
|
|
directories,
|
|
selectedId,
|
|
onSelect,
|
|
onContextMenu,
|
|
}: Omit<WorkspaceTreeItemsProps, "path"> & {
|
|
directory: WorkspaceDirectory;
|
|
}) {
|
|
const [open, setOpen] = useState(true);
|
|
return (
|
|
<div className="directory-branch">
|
|
<button
|
|
className="directory-row"
|
|
style={{ paddingLeft: 10 + depth * 16 }}
|
|
type="button"
|
|
onClick={() => setOpen((current) => !current)}
|
|
onContextMenu={onContextMenu
|
|
? (event) => onContextMenu(event, {
|
|
kind: "directory",
|
|
path: directory.path,
|
|
})
|
|
: undefined}
|
|
>
|
|
<span className={`directory-row__chevron${open ? " is-open" : ""}`}>
|
|
<Icon name="chevron" size={13} />
|
|
</span>
|
|
<Icon name="folder" size={17} />
|
|
<strong title={directory.path}>{directory.name}</strong>
|
|
</button>
|
|
{open && (
|
|
<WorkspaceTreeItems
|
|
path={directory.path}
|
|
depth={depth + 1}
|
|
scripts={scripts}
|
|
directories={directories}
|
|
selectedId={selectedId}
|
|
onSelect={onSelect}
|
|
onContextMenu={onContextMenu}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|