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

@ -479,3 +479,18 @@
overflow-wrap: break-word; overflow-wrap: break-word;
display: block; display: block;
} }
.clickable {
cursor: pointer;
}
.v-mid {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-start;
}
.full-width {
width: 100%;
}

View file

@ -91,13 +91,20 @@
.theme-default #breadcrumb .item { .theme-default #breadcrumb .item {
color: #697384; color: #697384;
margin-right: 1rem; margin-right: 1rem;
/* padding: 1rem; */ max-width: 6rem;
}
.theme-default #breadcrumb .content {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
overflow-wrap: break-word;
display: block;
} }
.theme-default #breadcrumb .item { .theme-default #breadcrumb .item {
color: #697384; color: #697384;
margin-right: 1rem; margin-right: 1rem;
/* padding: 1rem; */
} }
.container { .container {
@ -136,23 +143,52 @@
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.theme-default #icon-home { .theme-default #item-table {
color: black; width: 100%;
margin-right: 1rem; max-width: 100%;
display: block;
} }
.theme-default #browser .item-op { .theme-default #item-table thead,
line-height: 4rem; .theme-default #item-table tfoot,
padding: 1rem 0; .theme-default #item-table tbody,
text-align: right; .theme-default #item-table tr {
max-width: 100%;
display: block;
padding: 0;
}
.theme-default #item-table td,
.theme-default #item-table th {
display: inline-block;
padding: 0;
}
.theme-default .item-cell {
height: 5rem;
}
.theme-default .float-menu {
position: relative;
box-shadow: 0 5px 30px 0 rgba(31, 38, 135, 0.1);
border-radius: 0.5rem;
background-color: white;
} }
.theme-default #browser .item-info { .theme-default #browser .item-info {
padding: 1rem; padding: 1rem 0;
font-size: 1.2rem; font-size: 1.2rem;
border-top: dashed 1px #7f8c8d; border-top: dashed 1px #7f8c8d;
} }
.theme-default #browser .item-op {
text-align: right;
}
.theme-default #icon-home {
color: black;
margin-right: 1rem;
}
.theme-default #browser .error { .theme-default #browser .error {
line-height: 4rem; line-height: 4rem;
border: dashed 1px #e74c3c; border: dashed 1px #e74c3c;

View file

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

View file

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

View file

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

View file

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

View file

@ -2,14 +2,14 @@ import * as React from "react";
import { List } from "immutable"; import { List } from "immutable";
export interface Props { export interface Props {
grids: List<JSX.Element>; grids: List<React.ReactNode>;
style?: React.CSSProperties; style?: React.CSSProperties;
className?: string; className?: string;
} }
export const Flowgrid = (props: Props) => { export const Flowgrid = (props: Props) => {
const children = props.grids.map( const children = props.grids.map(
(child: JSX.Element, i: number): JSX.Element => { (child: React.ReactNode, i: number): React.ReactNode => {
return ( return (
<span key={`flowgrid-${i}`} className="inline-block"> <span key={`flowgrid-${i}`} className="inline-block">
{child} {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 this.state.captchaInput
) )
.then((ok: boolean): Promise<any> => { .then((ok: boolean): Promise<any> => {
this.setState({ captchaInput: "" });
if (ok) { if (ok) {
const params = new URLSearchParams( const params = new URLSearchParams(
document.location.search.substring(1) document.location.search.substring(1)
); );
return updater().initAll(params); return updater().initAll(params);
} else { } else {
this.setState({ user: "", pwd: "", captchaInput: "" });
alertMsg(this.props.msg.pkg.get("op.fail")); alertMsg(this.props.msg.pkg.get("op.fail"));
return updater().getCaptchaID(); return updater().getCaptchaID();
} }
@ -145,10 +145,7 @@ export class AuthPane extends React.Component<Props, State, {}> {
</span> </span>
<span className="float-input"> <span className="float-input">
<button <button id="btn-login" onClick={this.login}>
id="btn-login"
onClick={this.login}
>
{this.props.msg.pkg.get("login.login")} {this.props.msg.pkg.get("login.login")}
</button> </button>
</span> </span>

View file

@ -4,8 +4,9 @@ import { List, Map, Set } from "immutable";
import FileSize from "filesize"; import FileSize from "filesize";
import { RiFolder2Fill } from "@react-icons/all-files/ri/RiFolder2Fill"; 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 { RiFile2Fill } from "@react-icons/all-files/ri/RiFile2Fill";
import { RiFileList2Fill } from "@react-icons/all-files/ri/RiFileList2Fill";
import { alertMsg, confirmMsg } from "../common/env"; import { alertMsg, confirmMsg } from "../common/env";
import { updater } from "./state_updater"; import { updater } from "./state_updater";
@ -14,6 +15,7 @@ import { LoginProps } from "./pane_login";
import { MetadataResp, roleVisitor, roleAdmin } from "../client"; import { MetadataResp, roleVisitor, roleAdmin } from "../client";
import { Flexbox } from "./layout/flexbox"; import { Flexbox } from "./layout/flexbox";
import { Container } from "./layout/container"; import { Container } from "./layout/container";
import { Table } from "./layout/table";
import { Up } from "../worker/upload_mgr"; import { Up } from "../worker/upload_mgr";
import { UploadEntry, UploadState } from "../worker/interface"; import { UploadEntry, UploadState } from "../worker/interface";
import { getIcon } from "./visual/icons"; 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)); 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>) => { chdir = async (dirPath: List<string>) => {
if (dirPath === this.props.filesInfo.dirPath) { if (dirPath === this.props.filesInfo.dirPath) {
return; return;
@ -330,16 +340,12 @@ export class FilesPanel extends React.Component<Props, State, {}> {
} }
className="item" className="item"
> >
{pathPart} <span className="content">{pathPart}</span>
</button> </button>
); );
} }
); );
const nameWidthClass = `item-name item-name-${
this.props.ui.isVertical ? "vertical" : "horizontal"
} pointer`;
const ops = ( const ops = (
<div id="upload-op"> <div id="upload-op">
<div className="float"> <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 isSelected = this.state.selectedItems.has(item.name);
const dirPath = this.props.filesInfo.dirPath.join("/"); const dirPath = this.props.filesInfo.dirPath.join("/");
const itemPath = dirPath.endsWith("/") const itemPath = dirPath.endsWith("/")
? `${dirPath}${item.name}` ? `${dirPath}${item.name}`
: `${dirPath}/${item.name}`; : `${dirPath}/${item.name}`;
return item.isDir ? ( const icon = item.isDir ? (
<Flexbox <div className="v-mid item-cell">
key={item.name} <RiFolder2Fill size="3rem" className="yellow0-font" />
children={List([ </div>
<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%" },
])}
/>
) : ( ) : (
<div key={item.name}> <div className="v-mid item-cell">
<Flexbox <RiFile2Fill size="3rem" className="cyan0-font" />
key={item.name} </div>
children={List([ );
<Flexbox
children={List([
<RiFile2Fill size="3rem" className="cyan0-font margin-r-m" />,
<span className={`${nameWidthClass}`}> const content = item.isDir ? (
<a <div className={`v-mid item-cell`}>
className="title-m" <div className="full-width">
href={`/v1/fs/files?fp=${itemPath}`} <div className="title-m clickable" onClick={() => this.gotoChild(item.name)}>
target="_blank" {item.name}
> </div>
{item.name} <div className="desc-m grey0-font">
</a> <span>{item.modTime.slice(0, item.modTime.indexOf("T"))}</span>
<div className="desc-m grey0-font"> </div>
<span> </div>
{item.modTime.slice(0, item.modTime.indexOf("T"))} </div>
</span> ) : (
&nbsp;/&nbsp; <div>
<span>{FileSize(item.size, { round: 0 })}</span> <div className={`v-mid item-cell`}>
</div> <div className="full-width">
</span>, <a
])} className="title-m clickable"
childrenStyles={List([ href={`/v1/fs/files?fp=${itemPath}`}
{ flex: "0 0 auto" }, target="_blank"
{ flex: "0 0 auto" }, >
])} {item.name}
/>, </a>
<div className="desc-m grey0-font">
<span className={`item-op ${showOp}`}> <span>{item.modTime.slice(0, item.modTime.indexOf("T"))}</span>
<span &nbsp;/&nbsp;
onClick={() => this.toggleDetail(item.name)} <span>{FileSize(item.size, { round: 0 })}</span>
className="float-l" </div>
> </div>
{getIcon("RiInformationFill", "1.8rem", "grey1")} </div>
</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%" },
])}
/>
<div <div
className={`${ className={`${
@ -509,6 +456,33 @@ export class FilesPanel extends React.Component<Props, State, {}> {
</div> </div>
</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), { 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 = ( const itemListPane = (
<div id="item-list"> <div>
<div className={showOp}> <div className={showOp}>
<Container>{ops}</Container> <Container>{ops}</Container>
</div> </div>
@ -595,7 +577,12 @@ export class FilesPanel extends React.Component<Props, State, {}> {
<span id="breadcrumb"> <span id="breadcrumb">
<Flexbox <Flexbox
children={List([ children={List([
<RiHomeSmileFill size="3rem" id="icon-home" />, <RiArchiveDrawerFill
size="3rem"
id="icon-home"
className="clickable"
onClick={this.goHome}
/>,
<Flexbox children={breadcrumb} />, <Flexbox children={breadcrumb} />,
])} ])}
childrenStyles={List([ childrenStyles={List([
@ -614,7 +601,17 @@ export class FilesPanel extends React.Component<Props, State, {}> {
childrenStyles={List([{}, { justifyContent: "flex-end" }])} 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> </Container>
</div> </div>
); );

View file

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

View file

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

View file

@ -2,16 +2,23 @@ import * as React from "react";
import { List } from "immutable"; import { List } from "immutable";
import { alertMsg, confirmMsg } from "../common/env"; 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 { LoginProps } from "./pane_login";
import { updater } from "./state_updater"; import { updater } from "./state_updater";
import { Flexbox } from "./layout/flexbox"; import { Flexbox } from "./layout/flexbox";
import { getIcon } from "./visual/icons"; import { settingsDialogCtrl } from "./layers";
export interface State {} export interface State {}
export interface Props { export interface Props {
login: LoginProps; login: LoginProps;
msg: MsgProps; msg: MsgProps;
ui: UIProps;
update?: (updater: (prevState: ICoreState) => ICoreState) => void; update?: (updater: (prevState: ICoreState) => ICoreState) => void;
} }
@ -20,8 +27,8 @@ export class TopBar extends React.Component<Props, State, {}> {
super(p); super(p);
} }
showSettings = () => { openSettings = () => {
updater().setControlOption("settingsDialog", "on"); updater().setControlOption(settingsDialogCtrl, ctrlOn);
this.props.update(updater().updateUI); this.props.update(updater().updateUI);
}; };
@ -66,6 +73,10 @@ export class TopBar extends React.Component<Props, State, {}> {
render() { render() {
const showLogin = this.props.login.authed ? "" : "hidden"; const showLogin = this.props.login.authed ? "" : "hidden";
const showSettings =
this.props.ui.control.controls.get(settingsDialogCtrl) === ctrlHidden
? "hidden"
: "";
return ( return (
<div id="top-bar"> <div id="top-bar">
@ -82,7 +93,10 @@ export class TopBar extends React.Component<Props, State, {}> {
<Flexbox <Flexbox
children={List([ 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")} {this.props.msg.pkg.get("settings")}
{/* {getIcon("RiSettings4Line", "1.8rem", "cyan0")} */} {/* {getIcon("RiSettings4Line", "1.8rem", "cyan0")} */}
</button>, </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 { RiMenuFill } from "@react-icons/all-files/ri/RiMenuFill";
import { RiInformationFill } from "@react-icons/all-files/ri/RiInformationFill"; import { RiInformationFill } from "@react-icons/all-files/ri/RiInformationFill";
import { RiDeleteBin2Fill } from "@react-icons/all-files/ri/RiDeleteBin2Fill"; 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"; import { colorClass } from "./colors";
@ -23,6 +26,7 @@ export interface IconProps {
} }
const icons = Map<string, IconType>({ const icons = Map<string, IconType>({
RiFileList2Fill: RiFileList2Fill,
RiFolder2Fill: RiFolder2Fill, RiFolder2Fill: RiFolder2Fill,
RiShareBoxLine: RiShareBoxLine, RiShareBoxLine: RiShareBoxLine,
RiUploadCloudFill: RiUploadCloudFill, RiUploadCloudFill: RiUploadCloudFill,
@ -33,6 +37,7 @@ const icons = Map<string, IconType>({
RiMenuFill: RiMenuFill, RiMenuFill: RiMenuFill,
RiInformationFill: RiInformationFill, RiInformationFill: RiInformationFill,
RiDeleteBin2Fill: RiDeleteBin2Fill, RiDeleteBin2Fill: RiDeleteBin2Fill,
RiArchiveDrawerFill: RiArchiveDrawerFill,
}); });
export function getIconWithProps( export function getIconWithProps(