fix(fe/files_panel): use table layout and fix issues
This commit is contained in:
parent
0fc878ea7b
commit
3133720d79
14 changed files with 262 additions and 133 deletions
|
@ -75,7 +75,7 @@ describe("Login", () => {
|
|||
uploadSpeedLimit: 3,
|
||||
downloadSpeedLimit: 3,
|
||||
},
|
||||
captchaID: "",
|
||||
captchaID: "mockCaptchaID",
|
||||
preferences: {
|
||||
bg: {
|
||||
url: "bgUrl",
|
||||
|
|
|
@ -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: "",
|
||||
|
|
|
@ -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) => {},
|
||||
|
|
|
@ -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}`}
|
||||
|
|
|
@ -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}
|
||||
|
|
60
src/client/web/src/components/layout/table.tsx
Normal file
60
src/client/web/src/components/layout/table.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
/
|
||||
<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>
|
||||
/
|
||||
<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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue