fix(fe/files_panel): use table layout and fix issues

This commit is contained in:
hexxa 2021-12-11 20:44:11 +08:00 committed by Hexxa
parent 0fc878ea7b
commit 3133720d79
14 changed files with 262 additions and 133 deletions

View file

@ -75,7 +75,7 @@ describe("Login", () => {
uploadSpeedLimit: 3,
downloadSpeedLimit: 3,
},
captchaID: "",
captchaID: "mockCaptchaID",
preferences: {
bg: {
url: "bgUrl",

View file

@ -75,7 +75,7 @@ describe("State Manager", () => {
downloadSpeedLimit: 3,
},
authed: true,
captchaID: "",
captchaID: "mockCaptchaID",
preferences: {
bg: {
url: "bgUrl",
@ -185,7 +185,7 @@ describe("State Manager", () => {
quota: mockSelfResp.data.quota,
usedSpace: mockSelfResp.data.usedSpace,
authed: false,
captchaID: "",
captchaID: "mockCaptchaID",
preferences: {
bg: {
url: "",

View file

@ -79,6 +79,7 @@ describe("TopBar", () => {
updater().setClients(usersCl, filesCl, settingsCl);
const topbar = new TopBar({
ui: coreState.ui,
login: coreState.login,
msg: coreState.msg,
update: (updater: (prevState: ICoreState) => ICoreState) => {},

View file

@ -2,7 +2,7 @@ import * as React from "react";
import { List } from "immutable";
export interface Props {
children: List<JSX.Element>;
children: List<React.ReactNode>;
childrenStyles?: List<React.CSSProperties>;
style?: React.CSSProperties;
className?: string;
@ -25,7 +25,7 @@ const childrenStyle = {
export const Flexbox = (props: Props) => {
const childrenCount = props.children.size;
const children = props.children.map(
(child: JSX.Element, i: number): JSX.Element => {
(child: React.ReactNode, i: number): React.ReactNode => {
return (
<div
key={`fb-${i}`}

View file

@ -2,14 +2,14 @@ import * as React from "react";
import { List } from "immutable";
export interface Props {
grids: List<JSX.Element>;
grids: List<React.ReactNode>;
style?: React.CSSProperties;
className?: string;
}
export const Flowgrid = (props: Props) => {
const children = props.grids.map(
(child: JSX.Element, i: number): JSX.Element => {
(child: React.ReactNode, i: number): React.ReactNode => {
return (
<span key={`flowgrid-${i}`} className="inline-block">
{child}

View file

@ -0,0 +1,60 @@
import * as React from "react";
import { List } from "immutable";
export interface Props {
head: List<React.ReactNode>;
rows: List<List<React.ReactNode>>;
foot: List<React.ReactNode>;
colStyles?: List<React.CSSProperties>;
id?: string;
style?: React.CSSProperties;
className?: string;
}
export const Table = (props: Props) => {
const headCols = props.head.map(
(elem: React.ReactNode, i: number): React.ReactNode => {
const style = props.colStyles != null ? props.colStyles.get(i) : {};
return (
<th key={`h-${i}`} style={style}>
{elem}
</th>
);
}
);
const bodyRows = props.rows.map(
(row: List<React.ReactNode>, i: number): React.ReactNode => {
const tds = row.map((elem: React.ReactNode, j: number) => {
const style = props.colStyles != null ? props.colStyles.get(j) : {};
return (
<td key={`rc-${i}-${j}`} style={style}>
{elem}
</td>
);
});
return <tr key={`r-${i}`}>{tds}</tr>;
}
);
const footCols = props.foot.map(
(elem: React.ReactNode, i: number): React.ReactNode => {
const style = props.colStyles != null ? props.colStyles.get(i) : {};
return (
<th key={`f-${i}`} style={style}>
{elem}
</th>
);
}
);
return (
<table id={props.id} style={props.style} className={props.className}>
<thead>
<tr>{headCols}</tr>
</thead>
<tbody>{bodyRows}</tbody>
<tfoot>
<tr>{footCols}</tr>
</tfoot>
</table>
);
};

View file

@ -63,13 +63,13 @@ export class AuthPane extends React.Component<Props, State, {}> {
this.state.captchaInput
)
.then((ok: boolean): Promise<any> => {
this.setState({ captchaInput: "" });
if (ok) {
const params = new URLSearchParams(
document.location.search.substring(1)
);
return updater().initAll(params);
} else {
this.setState({ user: "", pwd: "", captchaInput: "" });
alertMsg(this.props.msg.pkg.get("op.fail"));
return updater().getCaptchaID();
}
@ -145,10 +145,7 @@ export class AuthPane extends React.Component<Props, State, {}> {
</span>
<span className="float-input">
<button
id="btn-login"
onClick={this.login}
>
<button id="btn-login" onClick={this.login}>
{this.props.msg.pkg.get("login.login")}
</button>
</span>

View file

@ -4,8 +4,9 @@ import { List, Map, Set } from "immutable";
import FileSize from "filesize";
import { RiFolder2Fill } from "@react-icons/all-files/ri/RiFolder2Fill";
import { RiHomeSmileFill } from "@react-icons/all-files/ri/RiHomeSmileFill";
import { RiArchiveDrawerFill } from "@react-icons/all-files/ri/RiArchiveDrawerFill";
import { RiFile2Fill } from "@react-icons/all-files/ri/RiFile2Fill";
import { RiFileList2Fill } from "@react-icons/all-files/ri/RiFileList2Fill";
import { alertMsg, confirmMsg } from "../common/env";
import { updater } from "./state_updater";
@ -14,6 +15,7 @@ import { LoginProps } from "./pane_login";
import { MetadataResp, roleVisitor, roleAdmin } from "../client";
import { Flexbox } from "./layout/flexbox";
import { Container } from "./layout/container";
import { Table } from "./layout/table";
import { Up } from "../worker/upload_mgr";
import { UploadEntry, UploadState } from "../worker/interface";
import { getIcon } from "./visual/icons";
@ -222,6 +224,14 @@ export class FilesPanel extends React.Component<Props, State, {}> {
return this.chdir(this.props.filesInfo.dirPath.push(childDirName));
};
goHome = async () => {
return updater()
.setHomeItems()
.then(() => {
this.props.update(updater().updateFilesInfo);
});
};
chdir = async (dirPath: List<string>) => {
if (dirPath === this.props.filesInfo.dirPath) {
return;
@ -330,16 +340,12 @@ export class FilesPanel extends React.Component<Props, State, {}> {
}
className="item"
>
{pathPart}
<span className="content">{pathPart}</span>
</button>
);
}
);
const nameWidthClass = `item-name item-name-${
this.props.ui.isVertical ? "vertical" : "horizontal"
} pointer`;
const ops = (
<div id="upload-op">
<div className="float">
@ -382,111 +388,52 @@ export class FilesPanel extends React.Component<Props, State, {}> {
}
);
const itemList = sortedItems.map((item: MetadataResp) => {
const items = sortedItems.map((item: MetadataResp) => {
const isSelected = this.state.selectedItems.has(item.name);
const dirPath = this.props.filesInfo.dirPath.join("/");
const itemPath = dirPath.endsWith("/")
? `${dirPath}${item.name}`
: `${dirPath}/${item.name}`;
return item.isDir ? (
<Flexbox
key={item.name}
children={List([
<Flexbox
children={List([
<RiFolder2Fill
size="3rem"
className="yellow0-font margin-r-m"
/>,
<span className={`${nameWidthClass}`}>
<span
className="title-m"
onClick={() => this.gotoChild(item.name)}
>
{item.name}
</span>
<div className="desc-m grey0-font">
<span>
{item.modTime.slice(0, item.modTime.indexOf("T"))}
</span>
</div>
</span>,
])}
childrenStyles={List([
{ flex: "0 0 auto" },
{ flex: "0 0 auto" },
])}
/>,
<span className={`item-op ${showOp}`}>
<span onClick={() => this.select(item.name)} className="float-l">
{isSelected
? getIcon("RiCheckboxFill", "1.8rem", "cyan0")
: getIcon("RiCheckboxBlankFill", "1.8rem", "grey1")}
</span>
</span>,
])}
childrenStyles={List([
{ flex: "0 0 auto", width: "60%" },
{ flex: "0 0 auto", justifyContent: "flex-end", width: "40%" },
])}
/>
const icon = item.isDir ? (
<div className="v-mid item-cell">
<RiFolder2Fill size="3rem" className="yellow0-font" />
</div>
) : (
<div key={item.name}>
<Flexbox
key={item.name}
children={List([
<Flexbox
children={List([
<RiFile2Fill size="3rem" className="cyan0-font margin-r-m" />,
<div className="v-mid item-cell">
<RiFile2Fill size="3rem" className="cyan0-font" />
</div>
);
<span className={`${nameWidthClass}`}>
<a
className="title-m"
href={`/v1/fs/files?fp=${itemPath}`}
target="_blank"
>
{item.name}
</a>
<div className="desc-m grey0-font">
<span>
{item.modTime.slice(0, item.modTime.indexOf("T"))}
</span>
&nbsp;/&nbsp;
<span>{FileSize(item.size, { round: 0 })}</span>
</div>
</span>,
])}
childrenStyles={List([
{ flex: "0 0 auto" },
{ flex: "0 0 auto" },
])}
/>,
<span className={`item-op ${showOp}`}>
<span
onClick={() => this.toggleDetail(item.name)}
className="float-l"
>
{getIcon("RiInformationFill", "1.8rem", "grey1")}
</span>
<span
onClick={() => this.select(item.name)}
className="float-l"
>
{isSelected
? getIcon("RiCheckboxFill", "1.8rem", "cyan0")
: getIcon("RiCheckboxBlankFill", "1.8rem", "grey1")}
</span>
</span>,
])}
childrenStyles={List([
{ flex: "0 0 auto", width: "60%" },
{ flex: "0 0 auto", justifyContent: "flex-end", width: "40%" },
])}
/>
const content = item.isDir ? (
<div className={`v-mid item-cell`}>
<div className="full-width">
<div className="title-m clickable" onClick={() => this.gotoChild(item.name)}>
{item.name}
</div>
<div className="desc-m grey0-font">
<span>{item.modTime.slice(0, item.modTime.indexOf("T"))}</span>
</div>
</div>
</div>
) : (
<div>
<div className={`v-mid item-cell`}>
<div className="full-width">
<a
className="title-m clickable"
href={`/v1/fs/files?fp=${itemPath}`}
target="_blank"
>
{item.name}
</a>
<div className="desc-m grey0-font">
<span>{item.modTime.slice(0, item.modTime.indexOf("T"))}</span>
&nbsp;/&nbsp;
<span>{FileSize(item.size, { round: 0 })}</span>
</div>
</div>
</div>
<div
className={`${
@ -509,6 +456,33 @@ export class FilesPanel extends React.Component<Props, State, {}> {
</div>
</div>
);
const op = item.isDir ? (
<div className={`v-mid item-cell item-op ${showOp}`}>
<span onClick={() => this.select(item.name)} className="float-l">
{isSelected
? getIcon("RiCheckboxFill", "1.8rem", "cyan0")
: getIcon("RiCheckboxBlankFill", "1.8rem", "grey1")}
</span>
</div>
) : (
<div className={`v-mid item-cell item-op ${showOp}`}>
<span
onClick={() => this.toggleDetail(item.name)}
className="float-l"
>
{getIcon("RiInformationFill", "1.8rem", "grey1")}
</span>
<span onClick={() => this.select(item.name)} className="float-l">
{isSelected
? getIcon("RiCheckboxFill", "1.8rem", "cyan0")
: getIcon("RiCheckboxBlankFill", "1.8rem", "grey1")}
</span>
</div>
);
return List([icon, content, op]);
});
const usedSpace = FileSize(parseInt(this.props.login.usedSpace, 10), {
@ -521,8 +495,16 @@ export class FilesPanel extends React.Component<Props, State, {}> {
}
);
const tableTitles = List([
<div className="font-s grey0-font">
<RiFileList2Fill size="3rem" className="black-font" />
</div>,
<div className="font-s grey0-font">Name</div>,
<div className="font-s grey0-font">Action</div>,
]);
const itemListPane = (
<div id="item-list">
<div>
<div className={showOp}>
<Container>{ops}</Container>
</div>
@ -595,7 +577,12 @@ export class FilesPanel extends React.Component<Props, State, {}> {
<span id="breadcrumb">
<Flexbox
children={List([
<RiHomeSmileFill size="3rem" id="icon-home" />,
<RiArchiveDrawerFill
size="3rem"
id="icon-home"
className="clickable"
onClick={this.goHome}
/>,
<Flexbox children={breadcrumb} />,
])}
childrenStyles={List([
@ -614,7 +601,17 @@ export class FilesPanel extends React.Component<Props, State, {}> {
childrenStyles={List([{}, { justifyContent: "flex-end" }])}
/>
{itemList}
<Table
colStyles={List([
{ width: "3rem", paddingRight: "1rem" },
{ width: "calc(100% - 12rem)", textAlign: "left" },
{ width: "8rem", textAlign: "right" },
])}
id="item-table"
head={tableTitles}
foot={List()}
rows={items}
/>
</Container>
</div>
);

View file

@ -77,6 +77,7 @@ export class RootFrame extends React.Component<Props, State, {}> {
<TopBar
login={this.props.login}
msg={this.props.msg}
ui={this.props.ui}
update={this.props.update}
/>

View file

@ -417,6 +417,9 @@ export class Updater {
.then(() => {
return this.syncCwd();
})
.then(() => {
return this.getCaptchaID();
})
.then(() => {
if (this.props.login.userRole === roleAdmin) {
return this.initStateForAdmin();

View file

@ -2,16 +2,23 @@ import * as React from "react";
import { List } from "immutable";
import { alertMsg, confirmMsg } from "../common/env";
import { ICoreState, MsgProps } from "./core_state";
import {
ICoreState,
MsgProps,
UIProps,
ctrlOn,
ctrlHidden,
} from "./core_state";
import { LoginProps } from "./pane_login";
import { updater } from "./state_updater";
import { Flexbox } from "./layout/flexbox";
import { getIcon } from "./visual/icons";
import { settingsDialogCtrl } from "./layers";
export interface State {}
export interface Props {
login: LoginProps;
msg: MsgProps;
ui: UIProps;
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
}
@ -20,8 +27,8 @@ export class TopBar extends React.Component<Props, State, {}> {
super(p);
}
showSettings = () => {
updater().setControlOption("settingsDialog", "on");
openSettings = () => {
updater().setControlOption(settingsDialogCtrl, ctrlOn);
this.props.update(updater().updateUI);
};
@ -66,6 +73,10 @@ export class TopBar extends React.Component<Props, State, {}> {
render() {
const showLogin = this.props.login.authed ? "" : "hidden";
const showSettings =
this.props.ui.control.controls.get(settingsDialogCtrl) === ctrlHidden
? "hidden"
: "";
return (
<div id="top-bar">
@ -82,7 +93,10 @@ export class TopBar extends React.Component<Props, State, {}> {
<Flexbox
children={List([
<button onClick={this.showSettings} className={`margin-r-m`}>
<button
onClick={this.openSettings}
className={`margin-r-m ${showSettings}`}
>
{this.props.msg.pkg.get("settings")}
{/* {getIcon("RiSettings4Line", "1.8rem", "cyan0")} */}
</button>,

View file

@ -12,6 +12,9 @@ import { RiCheckboxFill } from "@react-icons/all-files/ri/RiCheckboxFill";
import { RiMenuFill } from "@react-icons/all-files/ri/RiMenuFill";
import { RiInformationFill } from "@react-icons/all-files/ri/RiInformationFill";
import { RiDeleteBin2Fill } from "@react-icons/all-files/ri/RiDeleteBin2Fill";
import { RiArchiveDrawerFill } from "@react-icons/all-files/ri/RiArchiveDrawerFill";
import { RiFileList2Fill } from "@react-icons/all-files/ri/RiFileList2Fill";
import { colorClass } from "./colors";
@ -23,6 +26,7 @@ export interface IconProps {
}
const icons = Map<string, IconType>({
RiFileList2Fill: RiFileList2Fill,
RiFolder2Fill: RiFolder2Fill,
RiShareBoxLine: RiShareBoxLine,
RiUploadCloudFill: RiUploadCloudFill,
@ -33,6 +37,7 @@ const icons = Map<string, IconType>({
RiMenuFill: RiMenuFill,
RiInformationFill: RiInformationFill,
RiDeleteBin2Fill: RiDeleteBin2Fill,
RiArchiveDrawerFill: RiArchiveDrawerFill,
});
export function getIconWithProps(