merge: integrate feat/auth into develop
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user