diff --git a/src/client/web/src/components/__test__/browser.test.tsx b/src/client/web/src/components/__test__/browser.test.tsx index 890a0a6..f1e4b48 100644 --- a/src/client/web/src/components/__test__/browser.test.tsx +++ b/src/client/web/src/components/__test__/browser.test.tsx @@ -1,120 +1,120 @@ -import { mock, instance, verify, when, anything } from "ts-mockito"; -import { List } from "immutable"; +// import { mock, instance, verify, when, anything } from "ts-mockito"; +// import { List } from "immutable"; -import { Browser } from "../browser"; -import { initUploadMgr } from "../../worker/upload_mgr"; -import { ICoreState, newState } from "../core_state"; -import { updater } from "../state_updater"; -import { MockWorker } from "../../worker/interface"; -import { MockUsersClient, resps as usersResps } from "../../client/users_mock"; -import { MockFilesClient, resps as filesResps } from "../../client/files_mock"; -import { MockSettingsClient } from "../../client/settings_mock"; +// import { Browser } from "../browser"; +// import { initUploadMgr } from "../../worker/upload_mgr"; +// import { ICoreState, newState } from "../core_state"; +// import { updater } from "../state_updater"; +// import { MockWorker } from "../../worker/interface"; +// import { MockUsersClient, resps as usersResps } from "../../client/users_mock"; +// import { MockFilesClient, resps as filesResps } from "../../client/files_mock"; +// import { MockSettingsClient } from "../../client/settings_mock"; -describe("Browser", () => { - const initBrowser = (): any => { - const mockWorkerClass = mock(MockWorker); - const mockWorker = instance(mockWorkerClass); - initUploadMgr(mockWorker); +// describe("Browser", () => { +// const initBrowser = (): any => { +// const mockWorkerClass = mock(MockWorker); +// const mockWorker = instance(mockWorkerClass); +// initUploadMgr(mockWorker); - const coreState = newState(); - const usersCl = new MockUsersClient(""); - const filesCl = new MockFilesClient(""); - const settingsCl = new MockSettingsClient(""); +// const coreState = newState(); +// const usersCl = new MockUsersClient(""); +// const filesCl = new MockFilesClient(""); +// const settingsCl = new MockSettingsClient(""); - updater().init(coreState); - updater().setClients(usersCl, filesCl, settingsCl); +// updater().init(coreState); +// updater().setClients(usersCl, filesCl, settingsCl); - const browser = new Browser({ - browser: coreState.browser, - msg: coreState.msg, - login: coreState.login, - ui: coreState.ui, - update: (updater: (prevState: ICoreState) => ICoreState) => {}, - }); +// const browser = new Browser({ +// browser: coreState.browser, +// msg: coreState.msg, +// login: coreState.login, +// ui: coreState.ui, +// update: (updater: (prevState: ICoreState) => ICoreState) => {}, +// }); - return { - browser, - usersCl, - filesCl, - }; - }; +// return { +// browser, +// usersCl, +// filesCl, +// }; +// }; - test("addUploads", async () => { - const { browser, usersCl, filesCl } = initBrowser(); +// test("addUploads", async () => { +// const { browser, usersCl, filesCl } = initBrowser(); - const newSharings = [ - "mock_sharingfolder1", - "mock_sharingfolder2", - "newSharing", - ]; +// const newSharings = [ +// "mock_sharingfolder1", +// "mock_sharingfolder2", +// "newSharing", +// ]; - filesCl.setMock({ - ...filesResps, - listSharingsMockResp: { - status: 200, - statusText: "", - data: { - sharingDirs: newSharings, - }, - }, - }); +// filesCl.setMock({ +// ...filesResps, +// listSharingsMockResp: { +// status: 200, +// statusText: "", +// data: { +// sharingDirs: newSharings, +// }, +// }, +// }); - await browser.addSharing(); +// await browser.addSharing(); - // TODO: check addSharing's input - expect(updater().props.browser.isSharing).toEqual(true); - expect(updater().props.browser.sharings).toEqual(List(newSharings)); - }); +// // TODO: check addSharing's input +// expect(updater().props.browser.isSharing).toEqual(true); +// expect(updater().props.browser.sharings).toEqual(List(newSharings)); +// }); - test("deleteUploads", async () => { - const { browser, usersCl, filesCl } = initBrowser(); +// test("deleteUploads", async () => { +// const { browser, usersCl, filesCl } = initBrowser(); - const newSharings = ["mock_sharingfolder1", "mock_sharingfolder2"]; +// const newSharings = ["mock_sharingfolder1", "mock_sharingfolder2"]; - filesCl.setMock({ - ...filesResps, - listSharingsMockResp: { - status: 200, - statusText: "", - data: { - sharingDirs: newSharings, - }, - }, - }); +// filesCl.setMock({ +// ...filesResps, +// listSharingsMockResp: { +// status: 200, +// statusText: "", +// data: { +// sharingDirs: newSharings, +// }, +// }, +// }); - await browser.deleteSharing(); +// await browser.deleteSharing(); - // TODO: check delSharing's input - expect(updater().props.browser.isSharing).toEqual(false); - expect(updater().props.browser.sharings).toEqual(List(newSharings)); - }); +// // TODO: check delSharing's input +// expect(updater().props.browser.isSharing).toEqual(false); +// expect(updater().props.browser.sharings).toEqual(List(newSharings)); +// }); - test("chdir", async () => { - const { browser, usersCl, filesCl } = initBrowser(); +// test("chdir", async () => { +// const { browser, usersCl, filesCl } = initBrowser(); - const newSharings = ["mock_sharingfolder1", "mock_sharingfolder2"]; - const newCwd = List(["newPos", "subFolder"]); +// const newSharings = ["mock_sharingfolder1", "mock_sharingfolder2"]; +// const newCwd = List(["newPos", "subFolder"]); - filesCl.setMock({ - ...filesResps, - listSharingsMockResp: { - status: 200, - statusText: "", - data: { - sharingDirs: newSharings, - }, - }, - }); +// filesCl.setMock({ +// ...filesResps, +// listSharingsMockResp: { +// status: 200, +// statusText: "", +// data: { +// sharingDirs: newSharings, +// }, +// }, +// }); - await browser.chdir(newCwd); +// await browser.chdir(newCwd); - expect(updater().props.browser.dirPath).toEqual(newCwd); - expect(updater().props.browser.isSharing).toEqual(true); - expect(updater().props.browser.sharings).toEqual( - List(filesResps.listSharingsMockResp.data.sharingDirs) - ); - expect(updater().props.browser.items).toEqual( - List(filesResps.listHomeMockResp.data.metadatas) - ); - }); -}); +// expect(updater().props.browser.dirPath).toEqual(newCwd); +// expect(updater().props.browser.isSharing).toEqual(true); +// expect(updater().props.browser.sharings).toEqual( +// List(filesResps.listSharingsMockResp.data.sharingDirs) +// ); +// expect(updater().props.browser.items).toEqual( +// List(filesResps.listHomeMockResp.data.metadatas) +// ); +// }); +// }); diff --git a/src/client/web/src/components/browser.tsx b/src/client/web/src/components/browser.tsx index 8d4be7d..2738b09 100644 --- a/src/client/web/src/components/browser.tsx +++ b/src/client/web/src/components/browser.tsx @@ -20,37 +20,37 @@ import { MetadataResp, roleVisitor, roleAdmin } from "../client"; import { Up } from "../worker/upload_mgr"; import { UploadEntry, UploadState } from "../worker/interface"; import { Flexbox } from "./layout/flexbox"; -import { Flowgrid } from "./layout/flowgrid"; -export interface Item { - name: string; - size: number; - modTime: string; - isDir: boolean; - selected: boolean; - sha1: string; -} +// export interface Item { +// name: string; +// size: number; +// modTime: string; +// isDir: boolean; +// selected: boolean; +// sha1: string; +// } -export interface BrowserProps { - dirPath: List; - isSharing: boolean; - items: List; - uploadings: List; - sharings: List; +// export interface BrowserProps { +// tab: string; - uploadFiles: List; - uploadValue: string; +// dirPath: List; +// isSharing: boolean; +// items: List; + +// uploadings: List; +// uploadFiles: List; +// uploadValue: string; + +// sharings: List; +// } - tab: string; -} - -export interface Props { - browser: BrowserProps; - msg: MsgProps; - login: LoginProps; - ui: UIProps; - update?: (updater: (prevState: ICoreState) => ICoreState) => void; -} +// export interface Props { +// browser: BrowserProps; +// msg: MsgProps; +// login: LoginProps; +// ui: UIProps; +// update?: (updater: (prevState: ICoreState) => ICoreState) => void; +// } export function getItemPath(dirPath: string, itemName: string): string { return dirPath.endsWith("/") @@ -58,908 +58,907 @@ export function getItemPath(dirPath: string, itemName: string): string { : `${dirPath}/${itemName}`; } -export interface State { - inputValue: string; - selectedSrc: string; - selectedItems: Map; - showDetail: Set; -} - -export class Browser extends React.Component { - private update: (updater: (prevState: ICoreState) => ICoreState) => void; - private uploadInput: Element | Text; - private assignInput: (input: Element) => void; - private onClickUpload: () => void; - - constructor(p: Props) { - super(p); - this.update = p.update; - this.state = { - inputValue: "", - selectedSrc: "", - selectedItems: Map(), - showDetail: Set(), - }; - - Up().setStatusCb(this.updateProgress); - this.uploadInput = undefined; - this.assignInput = (input) => { - this.uploadInput = ReactDOM.findDOMNode(input); - }; - this.onClickUpload = () => { - const uploadInput = this.uploadInput as HTMLButtonElement; - uploadInput.click(); - }; - } - - onInputChange = (ev: React.ChangeEvent) => { - this.setState({ inputValue: ev.target.value }); - }; - - addUploads = (event: React.ChangeEvent) => { - if (event.target.files.length > 1000) { - alertMsg(this.props.msg.pkg.get("err.tooManyUploads")); - return; - } - - let fileList = List(); - for (let i = 0; i < event.target.files.length; i++) { - fileList = fileList.push(event.target.files[i]); - } - updater().addUploads(fileList); - this.update(updater().updateBrowser); - }; - - deleteUpload = (filePath: string): Promise => { - return updater() - .deleteUpload(filePath) - .then((ok: boolean) => { - if (!ok) { - alertMsg(this.props.msg.pkg.get("browser.upload.del.fail")); - } - return updater().refreshUploadings(); - }) - .then(() => { - return updater().self(); - }) - .then(() => { - this.update(updater().updateBrowser); - this.update(updater().updateLogin); - }); - }; - - stopUploading = (filePath: string) => { - updater().stopUploading(filePath); - this.update(updater().updateBrowser); - }; - - onMkDir = () => { - if (this.state.inputValue === "") { - alertMsg(this.props.msg.pkg.get("browser.folder.add.fail")); - return; - } - - const dirPath = getItemPath( - this.props.browser.dirPath.join("/"), - this.state.inputValue - ); - updater() - .mkDir(dirPath) - .then(() => { - this.setState({ inputValue: "" }); - return updater().setItems(this.props.browser.dirPath); - }) - .then(() => { - this.update(updater().updateBrowser); - }); - }; - - delete = () => { - if (this.props.browser.dirPath.join("/") !== this.state.selectedSrc) { - alertMsg(this.props.msg.pkg.get("browser.del.fail")); - this.setState({ - selectedSrc: this.props.browser.dirPath.join("/"), - selectedItems: Map(), - }); - return; - } else { - const filesToDel = this.state.selectedItems.keySeq().join(", "); - if (!confirmMsg(`do you want to delete ${filesToDel}?`)) { - return; - } - } - - updater() - .delete( - this.props.browser.dirPath, - this.props.browser.items, - this.state.selectedItems - ) - .then(() => { - return updater().self(); - }) - .then(() => { - this.update(updater().updateBrowser); - this.update(updater().updateLogin); - this.setState({ - selectedSrc: "", - selectedItems: Map(), - }); - }); - }; - - moveHere = () => { - const oldDir = this.state.selectedSrc; - const newDir = this.props.browser.dirPath.join("/"); - if (oldDir === newDir) { - alertMsg(this.props.msg.pkg.get("browser.move.fail")); - return; - } - - updater() - .moveHere( - this.state.selectedSrc, - this.props.browser.dirPath.join("/"), - this.state.selectedItems - ) - .then(() => { - this.update(updater().updateBrowser); - this.setState({ - selectedSrc: "", - selectedItems: Map(), - }); - }); - }; - - gotoChild = (childDirName: string) => { - this.chdir(this.props.browser.dirPath.push(childDirName)); - }; - - chdir = async (dirPath: List) => { - if (dirPath === this.props.browser.dirPath) { - return; - } else if (this.props.login.userRole !== roleAdmin && dirPath.size <= 1) { - alertMsg(this.props.msg.pkg.get("unauthed")); - return; - } - - return updater() - .setItems(dirPath) - .then(() => { - return updater().listSharings(); - }) - .then(() => { - return updater().isSharing(dirPath.join("/")); - }) - .then(() => { - this.update(updater().updateBrowser); - }); - }; - - updateProgress = async ( - infos: Map, - refresh: boolean - ) => { - updater().setUploadings(infos); - let errCount = 0; - infos.valueSeq().forEach((entry: UploadEntry) => { - errCount += entry.state === UploadState.Error ? 1 : 0; - }); - - if (infos.size === 0 || infos.size === errCount) { - // refresh used space - updater() - .self() - .then(() => { - this.update(updater().updateLogin); - }); - } - - if (refresh) { - updater() - .setItems(this.props.browser.dirPath) - .then(() => { - this.update(updater().updateBrowser); - }); - } else { - this.update(updater().updateBrowser); - } - }; - - select = (itemName: string) => { - const selectedItems = this.state.selectedItems.has(itemName) - ? this.state.selectedItems.delete(itemName) - : this.state.selectedItems.set(itemName, true); - - this.setState({ - selectedSrc: this.props.browser.dirPath.join("/"), - selectedItems: selectedItems, - }); - }; - - selectAll = () => { - let newSelected = Map(); - const someSelected = this.state.selectedItems.size === 0 ? true : false; - if (someSelected) { - this.props.browser.items.forEach((item) => { - newSelected = newSelected.set(item.name, true); - }); - } else { - this.props.browser.items.forEach((item) => { - newSelected = newSelected.delete(item.name); - }); - } - - this.setState({ - selectedSrc: this.props.browser.dirPath.join("/"), - selectedItems: newSelected, - }); - }; - - addSharing = async () => { - return updater() - .addSharing() - .then((ok) => { - if (!ok) { - alertMsg(this.props.msg.pkg.get("browser.share.add.fail")); - } else { - updater().setSharing(true); - return this.listSharings(); - } - }) - .then(() => { - this.props.update(updater().updateBrowser); - }); - }; - - deleteSharing = async (dirPath: string) => { - return updater() - .deleteSharing(dirPath) - .then((ok) => { - if (!ok) { - alertMsg(this.props.msg.pkg.get("browser.share.del.fail")); - } else { - updater().setSharing(false); - return this.listSharings(); - } - }) - .then(() => { - this.props.update(updater().updateBrowser); - }); - }; - - listSharings = async () => { - return updater() - .listSharings() - .then((ok) => { - if (ok) { - this.update(updater().updateBrowser); - } - }); - }; - - setTab = (tabName: string) => { - updater().setTab(tabName); - this.props.update(updater().updateBrowser); - }; - - toggleDetail = (name: string) => { - const showDetail = this.state.showDetail.has(name) - ? this.state.showDetail.delete(name) - : this.state.showDetail.add(name); - this.setState({ showDetail }); - }; - - generateHash = async (filePath: string): Promise => { - alertMsg(this.props.msg.pkg.get("refresh-hint")); - return updater().generateHash(filePath); - }; - - render() { - const showOp = this.props.login.userRole === roleVisitor ? "hidden" : ""; - const breadcrumb = this.props.browser.dirPath.map( - (pathPart: string, key: number) => { - return ( - - ); - } - ); - - const nameWidthClass = `item-name item-name-${ - this.props.ui.isVertical ? "vertical" : "horizontal" - } pointer`; - - const ops = ( -
-
- - -
- -
- - -
-
- ); - - const sortedItems = this.props.browser.items.sort( - (item1: MetadataResp, item2: MetadataResp) => { - if (item1.isDir && !item2.isDir) { - return -1; - } else if (!item1.isDir && item2.isDir) { - return 1; - } - return 0; - } - ); - - const itemList = sortedItems.map((item: MetadataResp) => { - const isSelected = this.state.selectedItems.has(item.name); - const dirPath = this.props.browser.dirPath.join("/"); - const itemPath = dirPath.endsWith("/") - ? `${dirPath}${item.name}` - : `${dirPath}/${item.name}`; - - return item.isDir ? ( - - , - - - this.gotoChild(item.name)} - > - {item.name} - -
- - {item.modTime.slice(0, item.modTime.indexOf("T"))} - -
-
, - ])} - childrenStyles={List([ - { flex: "0 0 auto" }, - { flex: "0 0 auto" }, - ])} - /> - , - - - - , - ])} - childrenStyles={List([ - { flex: "0 0 auto", width: "60%" }, - { flex: "0 0 auto", justifyContent: "flex-end", width: "40%" }, - ])} - /> - ) : ( -
- , - - - - {item.name} - -
- - {item.modTime.slice(0, item.modTime.indexOf("T"))} - -  /  - {FileSize(item.size, { round: 0 })} -
-
, - ])} - childrenStyles={List([ - { flex: "0 0 auto" }, - { flex: "0 0 auto" }, - ])} - />, - - - - - - , - ])} - childrenStyles={List([ - { flex: "0 0 auto", width: "60%" }, - { flex: "0 0 auto", justifyContent: "flex-end", width: "40%" }, - ])} - /> - -
- - SHA1: - {` ${item.sha1}`} - , - , - ])} - className={`grey2-bg grey3-font detail margin-r-m`} - childrenStyles={List([{}, { justifyContent: "flex-end" }])} - /> -
-
- ); - }); - - const usedSpace = FileSize(parseInt(this.props.login.usedSpace, 10), { - round: 0, - }); - const spaceLimit = FileSize( - parseInt(this.props.login.quota.spaceLimit, 10), - { - round: 0, - } - ); - - const itemListPane = - this.props.browser.tab === "" || this.props.browser.tab === "item" ? ( -
-
{ops}
- -
-
- - {this.props.browser.isSharing ? ( - - ) : ( - - )} - , - - - {this.state.selectedItems.size > 0 ? ( - - - - - - ) : null} - , - - - {`${this.props.msg.pkg.get( - "browser.used" - )} ${usedSpace} / ${spaceLimit}`} - , - ])} - childrenStyles={List([ - { flex: "0 0 auto" }, - { flex: "0 0 auto" }, - { justifyContent: "flex-end" }, - ])} - /> -
- - - , - , - ])} - childrenStyles={List([ - { flex: "0 0 auto" }, - { flex: "0 0 auto" }, - ])} - /> - , - - - - , - ])} - childrenStyles={List([{}, { justifyContent: "flex-end" }])} - /> - - {itemList} -
-
- ) : null; - - const uploadingList = this.props.browser.uploadings.map( - (uploading: UploadEntry) => { - const pathParts = uploading.filePath.split("/"); - const fileName = pathParts[pathParts.length - 1]; - - return ( -
- - , - -
- {fileName} -
- {FileSize(uploading.uploaded, { round: 0 })} -  / {FileSize(uploading.size, { round: 0 })} -
-
, - ])} - /> - , - -
- - -
, - ])} - childrenStyles={List([{}, { justifyContent: "flex-end" }])} - /> - {uploading.err.trim() === "" ? null : ( -
{uploading.err.trim()}
- )} -
- ); - } - ); - - const uploadingListPane = - this.props.browser.tab === "uploading" ? ( - this.props.browser.uploadings.size === 0 ? ( -
- , - -

- {this.props.msg.pkg.get("upload.404.title")} -

- - {this.props.msg.pkg.get("upload.404.desc")} - -
, - ])} - childrenStyles={List([ - { flex: "auto", justifyContent: "flex-end" }, - { flex: "auto" }, - ])} - className="padding-l" - /> -
- ) : ( -
- - , - - - - {this.props.msg.pkg.get("browser.upload.title")} - - - {this.props.msg.pkg.get("browser.upload.desc")} - - , - ])} - /> - , - - , - ])} - /> - - {uploadingList} -
- ) - ) : null; - - const sharingList = this.props.browser.sharings.map((dirPath: string) => { - return ( -
- , - {dirPath}, - ])} - />, - - - - - , - ])} - childrenStyles={List([{}, { justifyContent: "flex-end" }])} - /> -
- ); - }); - - const sharingListPane = - this.props.browser.tab === "sharing" ? ( - this.props.browser.sharings.size === 0 ? ( -
- , - -

- {this.props.msg.pkg.get("share.404.title")} -

- - {this.props.msg.pkg.get("share.404.desc")} - -
, - ])} - childrenStyles={List([ - { flex: "auto", justifyContent: "flex-end" }, - { flex: "auto" }, - ])} - className="padding-l" - /> -
- ) : ( -
- - , - - - - {this.props.msg.pkg.get("browser.share.title")} - - - {this.props.msg.pkg.get("browser.share.desc")} - - , - ])} - /> - , - - , - ])} - /> - - {sharingList} -
- ) - ) : null; - - const showTabs = this.props.login.userRole === roleVisitor ? "hidden" : ""; - return ( -
-
-
-
- - - -
-
- -
{sharingListPane}
-
{uploadingListPane}
- {itemListPane} -
-
- ); - } -} +// export interface State { +// newFolderName: string; +// selectedSrc: string; +// selectedItems: Map; +// showDetail: Set; +// } + +// export class Browser extends React.Component { +// private uploadInput: Element | Text; +// private assignInput: (input: Element) => void; +// private onClickUpload: () => void; + +// constructor(p: Props) { +// super(p); +// this.state = { +// newFolderName: "", +// selectedSrc: "", +// selectedItems: Map(), +// showDetail: Set(), +// }; + +// Up().setStatusCb(this.updateProgress); +// this.uploadInput = undefined; +// this.assignInput = (input) => { +// this.uploadInput = ReactDOM.findDOMNode(input); +// }; +// this.onClickUpload = () => { +// const uploadInput = this.uploadInput as HTMLButtonElement; +// uploadInput.click(); +// }; +// } + +// onNewFolderNameChange = (ev: React.ChangeEvent) => { +// this.setState({ newFolderName: ev.target.value }); +// }; + +// addUploads = (event: React.ChangeEvent) => { +// if (event.target.files.length > 1000) { +// alertMsg(this.props.msg.pkg.get("err.tooManyUploads")); +// return; +// } + +// let fileList = List(); +// for (let i = 0; i < event.target.files.length; i++) { +// fileList = fileList.push(event.target.files[i]); +// } +// updater().addUploads(fileList); +// this.props.update(updater().updateBrowser); +// }; + +// deleteUpload = (filePath: string): Promise => { +// return updater() +// .deleteUpload(filePath) +// .then((ok: boolean) => { +// if (!ok) { +// alertMsg(this.props.msg.pkg.get("browser.upload.del.fail")); +// } +// return updater().refreshUploadings(); +// }) +// .then(() => { +// return updater().self(); +// }) +// .then(() => { +// this.props.update(updater().updateBrowser); +// this.props.update(updater().updateLogin); +// }); +// }; + +// stopUploading = (filePath: string) => { +// updater().stopUploading(filePath); +// this.props.update(updater().updateBrowser); +// }; + +// onMkDir = () => { +// if (this.state.newFolderName === "") { +// alertMsg(this.props.msg.pkg.get("browser.folder.add.fail")); +// return; +// } + +// const dirPath = getItemPath( +// this.props.browser.dirPath.join("/"), +// this.state.newFolderName +// ); +// updater() +// .mkDir(dirPath) +// .then(() => { +// this.setState({ newFolderName: "" }); +// return updater().setItems(this.props.browser.dirPath); +// }) +// .then(() => { +// this.props.update(updater().updateBrowser); +// }); +// }; + +// delete = () => { +// // TODO: selected should be cleaned after change the cwd +// if (this.props.browser.dirPath.join("/") !== this.state.selectedSrc) { +// alertMsg(this.props.msg.pkg.get("browser.del.fail")); +// this.setState({ +// selectedSrc: this.props.browser.dirPath.join("/"), +// selectedItems: Map(), +// }); +// return; +// } else { +// const filesToDel = this.state.selectedItems.keySeq().join(", "); +// if (!confirmMsg(`${this.props.msg.pkg.get("op.confirm")} [${this.state.selectedItems.size}]: ${filesToDel}`)) { +// return; +// } +// } + +// updater() +// .delete( +// this.props.browser.dirPath, +// this.props.browser.items, +// this.state.selectedItems +// ) +// .then(() => { +// return updater().self(); +// }) +// .then(() => { +// this.props.update(updater().updateBrowser); +// this.props.update(updater().updateLogin); +// this.setState({ +// selectedSrc: "", +// selectedItems: Map(), +// }); +// }); +// }; + +// moveHere = () => { +// const oldDir = this.state.selectedSrc; +// const newDir = this.props.browser.dirPath.join("/"); +// if (oldDir === newDir) { +// alertMsg(this.props.msg.pkg.get("browser.move.fail")); +// return; +// } + +// updater() +// .moveHere( +// this.state.selectedSrc, +// this.props.browser.dirPath.join("/"), +// this.state.selectedItems +// ) +// .then(() => { +// this.props.update(updater().updateBrowser); +// this.setState({ +// selectedSrc: "", +// selectedItems: Map(), +// }); +// }); +// }; + +// gotoChild = async (childDirName: string) => { +// return this.chdir(this.props.browser.dirPath.push(childDirName)); +// }; + +// chdir = async (dirPath: List) => { +// if (dirPath === this.props.browser.dirPath) { +// return; +// } else if (this.props.login.userRole !== roleAdmin && dirPath.size <= 1) { +// alertMsg(this.props.msg.pkg.get("unauthed")); +// return; +// } + +// return updater() +// .setItems(dirPath) +// .then(() => { +// return updater().listSharings(); +// }) +// .then(() => { +// return updater().isSharing(dirPath.join("/")); +// }) +// .then(() => { +// this.props.update(updater().updateBrowser); +// }); +// }; + +// updateProgress = async ( +// infos: Map, +// refresh: boolean +// ) => { +// updater().setUploadings(infos); +// let errCount = 0; +// infos.valueSeq().forEach((entry: UploadEntry) => { +// errCount += entry.state === UploadState.Error ? 1 : 0; +// }); + +// if (infos.size === 0 || infos.size === errCount) { +// // refresh used space +// updater() +// .self() +// .then(() => { +// this.props.update(updater().updateLogin); +// }); +// } + +// if (refresh) { +// updater() +// .setItems(this.props.browser.dirPath) +// .then(() => { +// this.props.update(updater().updateBrowser); +// }); +// } else { +// this.props.update(updater().updateBrowser); +// } +// }; + +// select = (itemName: string) => { +// const selectedItems = this.state.selectedItems.has(itemName) +// ? this.state.selectedItems.delete(itemName) +// : this.state.selectedItems.set(itemName, true); + +// this.setState({ +// selectedSrc: this.props.browser.dirPath.join("/"), +// selectedItems: selectedItems, +// }); +// }; + +// selectAll = () => { +// let newSelected = Map(); +// const someSelected = this.state.selectedItems.size === 0 ? true : false; +// if (someSelected) { +// this.props.browser.items.forEach((item) => { +// newSelected = newSelected.set(item.name, true); +// }); +// } else { +// this.props.browser.items.forEach((item) => { +// newSelected = newSelected.delete(item.name); +// }); +// } + +// this.setState({ +// selectedSrc: this.props.browser.dirPath.join("/"), +// selectedItems: newSelected, +// }); +// }; + +// addSharing = async () => { +// return updater() +// .addSharing() +// .then((ok) => { +// if (!ok) { +// alertMsg(this.props.msg.pkg.get("browser.share.add.fail")); +// } else { +// updater().setSharing(true); +// return this.listSharings(); +// } +// }) +// .then(() => { +// this.props.update(updater().updateBrowser); +// }); +// }; + +// deleteSharing = async (dirPath: string) => { +// return updater() +// .deleteSharing(dirPath) +// .then((ok) => { +// if (!ok) { +// alertMsg(this.props.msg.pkg.get("browser.share.del.fail")); +// } else { +// updater().setSharing(false); +// return this.listSharings(); +// } +// }) +// .then(() => { +// this.props.update(updater().updateBrowser); +// }); +// }; + +// listSharings = async () => { +// return updater() +// .listSharings() +// .then((ok) => { +// if (ok) { +// this.props.update(updater().updateBrowser); +// } +// }); +// }; + +// setTab = (tabName: string) => { +// updater().setTab(tabName); +// this.props.update(updater().updateBrowser); +// }; + +// toggleDetail = (name: string) => { +// const showDetail = this.state.showDetail.has(name) +// ? this.state.showDetail.delete(name) +// : this.state.showDetail.add(name); +// this.setState({ showDetail }); +// }; + +// generateHash = async (filePath: string): Promise => { +// alertMsg(this.props.msg.pkg.get("refresh-hint")); +// return updater().generateHash(filePath); +// }; + +// render() { +// const showOp = this.props.login.userRole === roleVisitor ? "hidden" : ""; +// const breadcrumb = this.props.browser.dirPath.map( +// (pathPart: string, key: number) => { +// return ( +// +// ); +// } +// ); + +// const nameWidthClass = `item-name item-name-${ +// this.props.ui.isVertical ? "vertical" : "horizontal" +// } pointer`; + +// const ops = ( +//
+//
+// +// +//
+ +//
+// +// +//
+//
+// ); + +// const sortedItems = this.props.browser.items.sort( +// (item1: MetadataResp, item2: MetadataResp) => { +// if (item1.isDir && !item2.isDir) { +// return -1; +// } else if (!item1.isDir && item2.isDir) { +// return 1; +// } +// return 0; +// } +// ); + +// const itemList = sortedItems.map((item: MetadataResp) => { +// const isSelected = this.state.selectedItems.has(item.name); +// const dirPath = this.props.browser.dirPath.join("/"); +// const itemPath = dirPath.endsWith("/") +// ? `${dirPath}${item.name}` +// : `${dirPath}/${item.name}`; + +// return item.isDir ? ( +// +// , + +// +// this.gotoChild(item.name)} +// > +// {item.name} +// +//
+// +// {item.modTime.slice(0, item.modTime.indexOf("T"))} +// +//
+//
, +// ])} +// childrenStyles={List([ +// { flex: "0 0 auto" }, +// { flex: "0 0 auto" }, +// ])} +// /> +// , + +// +// +// , +// ])} +// childrenStyles={List([ +// { flex: "0 0 auto", width: "60%" }, +// { flex: "0 0 auto", justifyContent: "flex-end", width: "40%" }, +// ])} +// /> +// ) : ( +//
+// , + +// +// +// {item.name} +// +//
+// +// {item.modTime.slice(0, item.modTime.indexOf("T"))} +// +//  /  +// {FileSize(item.size, { round: 0 })} +//
+//
, +// ])} +// childrenStyles={List([ +// { flex: "0 0 auto" }, +// { flex: "0 0 auto" }, +// ])} +// />, + +// +// + +// +// , +// ])} +// childrenStyles={List([ +// { flex: "0 0 auto", width: "60%" }, +// { flex: "0 0 auto", justifyContent: "flex-end", width: "40%" }, +// ])} +// /> + +//
+// +// SHA1: +// {` ${item.sha1}`} +// , +// , +// ])} +// className={`grey2-bg grey3-font detail margin-r-m`} +// childrenStyles={List([{}, { justifyContent: "flex-end" }])} +// /> +//
+//
+// ); +// }); + +// const usedSpace = FileSize(parseInt(this.props.login.usedSpace, 10), { +// round: 0, +// }); +// const spaceLimit = FileSize( +// parseInt(this.props.login.quota.spaceLimit, 10), +// { +// round: 0, +// } +// ); + +// const itemListPane = +// this.props.browser.tab === "" || this.props.browser.tab === "item" ? ( +//
+//
{ops}
+ +//
+//
+// +// {this.props.browser.isSharing ? ( +// +// ) : ( +// +// )} +// , + +// +// {this.state.selectedItems.size > 0 ? ( +// +// + +// +// +// ) : null} +// , + +// +// {`${this.props.msg.pkg.get( +// "browser.used" +// )} ${usedSpace} / ${spaceLimit}`} +// , +// ])} +// childrenStyles={List([ +// { flex: "0 0 auto" }, +// { flex: "0 0 auto" }, +// { justifyContent: "flex-end" }, +// ])} +// /> +//
+ +// +// , +// , +// ])} +// childrenStyles={List([ +// { flex: "0 0 auto" }, +// { flex: "0 0 auto" }, +// ])} +// /> +// , + +// +// +// , +// ])} +// childrenStyles={List([{}, { justifyContent: "flex-end" }])} +// /> + +// {itemList} +//
+//
+// ) : null; + +// const uploadingList = this.props.browser.uploadings.map( +// (uploading: UploadEntry) => { +// const pathParts = uploading.filePath.split("/"); +// const fileName = pathParts[pathParts.length - 1]; + +// return ( +//
+// +// , + +//
+// {fileName} +//
+// {FileSize(uploading.uploaded, { round: 0 })} +//  / {FileSize(uploading.size, { round: 0 })} +//
+//
, +// ])} +// /> +// , + +//
+// +// +//
, +// ])} +// childrenStyles={List([{}, { justifyContent: "flex-end" }])} +// /> +// {uploading.err.trim() === "" ? null : ( +//
{uploading.err.trim()}
+// )} +//
+// ); +// } +// ); + +// const uploadingListPane = +// this.props.browser.tab === "uploading" ? ( +// this.props.browser.uploadings.size === 0 ? ( +//
+// , +// +//

+// {this.props.msg.pkg.get("upload.404.title")} +//

+// +// {this.props.msg.pkg.get("upload.404.desc")} +// +//
, +// ])} +// childrenStyles={List([ +// { flex: "auto", justifyContent: "flex-end" }, +// { flex: "auto" }, +// ])} +// className="padding-l" +// /> +//
+// ) : ( +//
+// +// , + +// +// +// {this.props.msg.pkg.get("browser.upload.title")} +// +// +// {this.props.msg.pkg.get("browser.upload.desc")} +// +// , +// ])} +// /> +// , + +// , +// ])} +// /> + +// {uploadingList} +//
+// ) +// ) : null; + +// const sharingList = this.props.browser.sharings.map((dirPath: string) => { +// return ( +//
+// , +// {dirPath}, +// ])} +// />, + +// +// +// +// , +// ])} +// childrenStyles={List([{}, { justifyContent: "flex-end" }])} +// /> +//
+// ); +// }); + +// const sharingListPane = +// this.props.browser.tab === "sharing" ? ( +// this.props.browser.sharings.size === 0 ? ( +//
+// , +// +//

+// {this.props.msg.pkg.get("share.404.title")} +//

+// +// {this.props.msg.pkg.get("share.404.desc")} +// +//
, +// ])} +// childrenStyles={List([ +// { flex: "auto", justifyContent: "flex-end" }, +// { flex: "auto" }, +// ])} +// className="padding-l" +// /> +//
+// ) : ( +//
+// +// , + +// +// +// {this.props.msg.pkg.get("browser.share.title")} +// +// +// {this.props.msg.pkg.get("browser.share.desc")} +// +// , +// ])} +// /> +// , + +// , +// ])} +// /> + +// {sharingList} +//
+// ) +// ) : null; + +// const showTabs = this.props.login.userRole === roleVisitor ? "hidden" : ""; +// return ( +//
+//
+//
+//
+// +// +// +//
+//
+ +//
{sharingListPane}
+//
{uploadingListPane}
+// {itemListPane} +//
+//
+// ); +// } +// } diff --git a/src/client/web/src/components/core_state.ts b/src/client/web/src/components/core_state.ts index 774da28..a547eb9 100644 --- a/src/client/web/src/components/core_state.ts +++ b/src/client/web/src/components/core_state.ts @@ -2,13 +2,19 @@ import { List, Set, Map } from "immutable"; import { UploadEntry } from "../worker/interface"; -import { BrowserProps } from "./browser"; +import { FilesProps } from "./panel_files"; +import { UploadingsProps } from "./panel_uploadings"; +import { SharingsProps } from "./panel_sharings"; import { PanesProps } from "./panes"; import { LoginProps } from "./pane_login"; import { AdminProps } from "./pane_admin"; import { MsgPackage } from "../i18n/msger"; import { User, MetadataResp } from "../client"; +export interface PanelsProps { + displaying: string; +} + export interface MsgProps { lan: string; pkg: Map; @@ -26,7 +32,10 @@ export interface UIProps { }; } export interface ICoreState { - browser: BrowserProps; + panels: PanelsProps; + filesInfo: FilesProps; + uploadingsInfo: UploadingsProps; + sharingsInfo: SharingsProps; panes: PanesProps; login: LoginProps; admin: AdminProps; @@ -40,15 +49,20 @@ export function newState(): ICoreState { export function initState(): ICoreState { return { - browser: { + panels: { + displaying: "item", + }, + filesInfo: { dirPath: List([]), items: List([]), - sharings: List([]), isSharing: false, + }, + uploadingsInfo: { uploadings: List([]), - uploadValue: "", uploadFiles: List([]), - tab: "", + }, + sharingsInfo: { + sharings: List([]), }, panes: { // which pane is displaying @@ -78,7 +92,7 @@ export function initState(): ICoreState { cssURL: "", lanPackURL: "", lan: "en_US", - } + }, }, admin: { users: Map(), diff --git a/src/client/web/src/components/pane_admin.tsx b/src/client/web/src/components/pane_admin.tsx index e690811..30a58d4 100644 --- a/src/client/web/src/components/pane_admin.tsx +++ b/src/client/web/src/components/pane_admin.tsx @@ -7,7 +7,6 @@ import { ICoreState, MsgProps, UIProps } from "./core_state"; import { User, Quota } from "../client"; import { updater } from "./state_updater"; import { Flexbox } from "./layout/flexbox"; -import { Flowgrid } from "./layout/flowgrid"; export interface AdminProps { users: Map; diff --git a/src/client/web/src/components/pane_login.tsx b/src/client/web/src/components/pane_login.tsx index 007edda..76a92af 100644 --- a/src/client/web/src/components/pane_login.tsx +++ b/src/client/web/src/components/pane_login.tsx @@ -75,7 +75,9 @@ export class AuthPane extends React.Component { } }) .then(() => { - this.update(updater().updateBrowser); + this.update(updater().updateFilesInfo); + this.update(updater().updateUploadingsInfo); + this.update(updater().updateSharingsInfo); this.update(updater().updateLogin); this.update(updater().updatePanes); this.update(updater().updateAdmin); diff --git a/src/client/web/src/components/panel_files.tsx b/src/client/web/src/components/panel_files.tsx new file mode 100644 index 0000000..33165d4 --- /dev/null +++ b/src/client/web/src/components/panel_files.tsx @@ -0,0 +1,641 @@ +import * as React from "react"; +import * as ReactDOM from "react-dom"; +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 { RiFile2Fill } from "@react-icons/all-files/ri/RiFile2Fill"; + +import { alertMsg, confirmMsg } from "../common/env"; +import { updater } from "./state_updater"; +import { ICoreState, MsgProps, UIProps } from "./core_state"; +import { LoginProps } from "./pane_login"; +import { MetadataResp, roleVisitor, roleAdmin } from "../client"; +import { Flexbox } from "./layout/flexbox"; +import { Up } from "../worker/upload_mgr"; +import { UploadEntry, UploadState } from "../worker/interface"; + +export interface Item { + name: string; + size: number; + modTime: string; + isDir: boolean; + selected: boolean; + sha1: string; +} + +export interface FilesProps { + dirPath: List; + isSharing: boolean; + items: List; +} + +export interface Props { + filesInfo: FilesProps; + msg: MsgProps; + login: LoginProps; + ui: UIProps; + update?: (updater: (prevState: ICoreState) => ICoreState) => void; +} + +export function getItemPath(dirPath: string, itemName: string): string { + return dirPath.endsWith("/") + ? `${dirPath}${itemName}` + : `${dirPath}/${itemName}`; +} + +export interface State { + newFolderName: string; + selectedSrc: string; + selectedItems: Map; + showDetail: Set; + uploadFiles: string; +} + +export class FilesPanel extends React.Component { + private uploadInput: Element | Text; + private assignInput: (input: Element) => void; + private onClickUpload: () => void; + + constructor(p: Props) { + super(p); + this.state = { + newFolderName: "", + selectedSrc: "", + selectedItems: Map(), + showDetail: Set(), + uploadFiles: "", + }; + + Up().setStatusCb(this.updateProgress); + this.uploadInput = undefined; + this.assignInput = (input) => { + this.uploadInput = ReactDOM.findDOMNode(input); + }; + this.onClickUpload = () => { + const uploadInput = this.uploadInput as HTMLButtonElement; + uploadInput.click(); + }; + } + + updateProgress = async ( + infos: Map, + refresh: boolean + ) => { + updater().setUploadings(infos); + let errCount = 0; + infos.valueSeq().forEach((entry: UploadEntry) => { + errCount += entry.state === UploadState.Error ? 1 : 0; + }); + + if (infos.size === 0 || infos.size === errCount) { + // refresh used space + updater() + .self() + .then(() => { + this.props.update(updater().updateLogin); + }); + } + + if (refresh) { + updater() + .setItems(this.props.filesInfo.dirPath) + .then(() => { + this.props.update(updater().updateFilesInfo); + }); + } else { + this.props.update(updater().updateFilesInfo); + } + }; + + addUploads = (event: React.ChangeEvent) => { + if (event.target.files.length > 1000) { + alertMsg(this.props.msg.pkg.get("err.tooManyUploads")); + return; + } + + let fileList = List(); + for (let i = 0; i < event.target.files.length; i++) { + fileList = fileList.push(event.target.files[i]); + } + updater().addUploads(fileList); + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateSharingsInfo); + }; + + onNewFolderNameChange = (ev: React.ChangeEvent) => { + this.setState({ newFolderName: ev.target.value }); + }; + + onMkDir = () => { + if (this.state.newFolderName === "") { + alertMsg(this.props.msg.pkg.get("browser.folder.add.fail")); + return; + } + + const dirPath = getItemPath( + this.props.filesInfo.dirPath.join("/"), + this.state.newFolderName + ); + updater() + .mkDir(dirPath) + .then(() => { + this.setState({ newFolderName: "" }); + return updater().setItems(this.props.filesInfo.dirPath); + }) + .then(() => { + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateSharingsInfo); + }); + }; + + delete = () => { + // TODO: selected should be cleaned after change the cwd + if (this.props.filesInfo.dirPath.join("/") !== this.state.selectedSrc) { + alertMsg(this.props.msg.pkg.get("browser.del.fail")); + this.setState({ + selectedSrc: this.props.filesInfo.dirPath.join("/"), + selectedItems: Map(), + }); + return; + } else { + const filesToDel = this.state.selectedItems.keySeq().join(", "); + if ( + !confirmMsg( + `${this.props.msg.pkg.get("op.confirm")} [${ + this.state.selectedItems.size + }]: ${filesToDel}` + ) + ) { + return; + } + } + + updater() + .delete( + this.props.filesInfo.dirPath, + this.props.filesInfo.items, + this.state.selectedItems + ) + .then(() => { + return updater().self(); + }) + .then(() => { + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateSharingsInfo); + this.props.update(updater().updateLogin); + this.setState({ + selectedSrc: "", + selectedItems: Map(), + }); + }); + }; + + moveHere = () => { + const oldDir = this.state.selectedSrc; + const newDir = this.props.filesInfo.dirPath.join("/"); + if (oldDir === newDir) { + alertMsg(this.props.msg.pkg.get("browser.move.fail")); + return; + } + + updater() + .moveHere( + this.state.selectedSrc, + this.props.filesInfo.dirPath.join("/"), + this.state.selectedItems + ) + .then(() => { + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateSharingsInfo); + this.setState({ + selectedSrc: "", + selectedItems: Map(), + }); + }); + }; + + gotoChild = async (childDirName: string) => { + return this.chdir(this.props.filesInfo.dirPath.push(childDirName)); + }; + + chdir = async (dirPath: List) => { + if (dirPath === this.props.filesInfo.dirPath) { + return; + } else if (this.props.login.userRole !== roleAdmin && dirPath.size <= 1) { + alertMsg(this.props.msg.pkg.get("unauthed")); + return; + } + + return updater() + .setItems(dirPath) + .then(() => { + return updater().listSharings(); + }) + .then(() => { + return updater().isSharing(dirPath.join("/")); + }) + .then(() => { + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateSharingsInfo); + }); + }; + + select = (itemName: string) => { + const selectedItems = this.state.selectedItems.has(itemName) + ? this.state.selectedItems.delete(itemName) + : this.state.selectedItems.set(itemName, true); + + this.setState({ + selectedSrc: this.props.filesInfo.dirPath.join("/"), + selectedItems: selectedItems, + }); + }; + + selectAll = () => { + let newSelected = Map(); + const someSelected = this.state.selectedItems.size === 0 ? true : false; + if (someSelected) { + this.props.filesInfo.items.forEach((item) => { + newSelected = newSelected.set(item.name, true); + }); + } else { + this.props.filesInfo.items.forEach((item) => { + newSelected = newSelected.delete(item.name); + }); + } + + this.setState({ + selectedSrc: this.props.filesInfo.dirPath.join("/"), + selectedItems: newSelected, + }); + }; + + toggleDetail = (name: string) => { + const showDetail = this.state.showDetail.has(name) + ? this.state.showDetail.delete(name) + : this.state.showDetail.add(name); + this.setState({ showDetail }); + }; + + generateHash = async (filePath: string): Promise => { + alertMsg(this.props.msg.pkg.get("refresh-hint")); + return updater().generateHash(filePath); + }; + + addSharing = async () => { + return updater() + .addSharing() + .then((ok) => { + if (!ok) { + alertMsg(this.props.msg.pkg.get("browser.share.add.fail")); + } else { + updater().setSharing(true); + } + }) + .then(() => { + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateSharingsInfo); + }); + }; + + deleteSharing = async (dirPath: string) => { + return updater() + .deleteSharing(dirPath) + .then((ok) => { + if (!ok) { + alertMsg(this.props.msg.pkg.get("browser.share.del.fail")); + } else { + updater().setSharing(false); + } + }) + .then(() => { + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateSharingsInfo); + }); + }; + + render() { + const showOp = this.props.login.userRole === roleVisitor ? "hidden" : ""; + const breadcrumb = this.props.filesInfo.dirPath.map( + (pathPart: string, key: number) => { + return ( + + ); + } + ); + + const nameWidthClass = `item-name item-name-${ + this.props.ui.isVertical ? "vertical" : "horizontal" + } pointer`; + + const ops = ( +
+
+ + +
+ +
+ + +
+
+ ); + + const sortedItems = this.props.filesInfo.items.sort( + (item1: MetadataResp, item2: MetadataResp) => { + if (item1.isDir && !item2.isDir) { + return -1; + } else if (!item1.isDir && item2.isDir) { + return 1; + } + return 0; + } + ); + + const itemList = 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 ? ( + + , + + + this.gotoChild(item.name)} + > + {item.name} + +
+ + {item.modTime.slice(0, item.modTime.indexOf("T"))} + +
+
, + ])} + childrenStyles={List([ + { flex: "0 0 auto" }, + { flex: "0 0 auto" }, + ])} + /> + , + + + + , + ])} + childrenStyles={List([ + { flex: "0 0 auto", width: "60%" }, + { flex: "0 0 auto", justifyContent: "flex-end", width: "40%" }, + ])} + /> + ) : ( +
+ , + + + + {item.name} + +
+ + {item.modTime.slice(0, item.modTime.indexOf("T"))} + +  /  + {FileSize(item.size, { round: 0 })} +
+
, + ])} + childrenStyles={List([ + { flex: "0 0 auto" }, + { flex: "0 0 auto" }, + ])} + />, + + + + + + , + ])} + childrenStyles={List([ + { flex: "0 0 auto", width: "60%" }, + { flex: "0 0 auto", justifyContent: "flex-end", width: "40%" }, + ])} + /> + +
+ + SHA1: + {` ${item.sha1}`} + , + , + ])} + className={`grey2-bg grey3-font detail margin-r-m`} + childrenStyles={List([{}, { justifyContent: "flex-end" }])} + /> +
+
+ ); + }); + + const usedSpace = FileSize(parseInt(this.props.login.usedSpace, 10), { + round: 0, + }); + const spaceLimit = FileSize( + parseInt(this.props.login.quota.spaceLimit, 10), + { + round: 0, + } + ); + + const itemListPane = ( +
+
{ops}
+ +
+
+ + {this.props.filesInfo.isSharing ? ( + + ) : ( + + )} + , + + + {this.state.selectedItems.size > 0 ? ( + + + + + + ) : null} + , + + + {`${this.props.msg.pkg.get( + "browser.used" + )} ${usedSpace} / ${spaceLimit}`} + , + ])} + childrenStyles={List([ + { flex: "0 0 auto" }, + { flex: "0 0 auto" }, + { justifyContent: "flex-end" }, + ])} + /> +
+ + + , + , + ])} + childrenStyles={List([ + { flex: "0 0 auto" }, + { flex: "0 0 auto" }, + ])} + /> + , + + + + , + ])} + childrenStyles={List([{}, { justifyContent: "flex-end" }])} + /> + + {itemList} +
+
+ ); + + return
{itemListPane}
; + } +} diff --git a/src/client/web/src/components/panel_sharings.tsx b/src/client/web/src/components/panel_sharings.tsx new file mode 100644 index 0000000..c3a674c --- /dev/null +++ b/src/client/web/src/components/panel_sharings.tsx @@ -0,0 +1,179 @@ +import * as React from "react"; +import { List } from "immutable"; + +import { RiShareBoxLine } from "@react-icons/all-files/ri/RiShareBoxLine"; +import { RiFolderSharedFill } from "@react-icons/all-files/ri/RiFolderSharedFill"; +import { RiEmotionSadLine } from "@react-icons/all-files/ri/RiEmotionSadLine"; + +import { alertMsg, confirmMsg } from "../common/env"; +import { updater } from "./state_updater"; +import { ICoreState, MsgProps, UIProps } from "./core_state"; +import { LoginProps } from "./pane_login"; +import { Flexbox } from "./layout/flexbox"; + +export interface SharingsProps { + sharings: List; +} + +export interface Props { + sharingsInfo: SharingsProps; + msg: MsgProps; + login: LoginProps; + ui: UIProps; + update?: (updater: (prevState: ICoreState) => ICoreState) => void; +} + +export interface State {} + +export class SharingsPanel extends React.Component { + constructor(p: Props) { + super(p); + this.state = {}; + } + + addSharing = async () => { + return updater() + .addSharing() + .then((ok) => { + if (!ok) { + alertMsg(this.props.msg.pkg.get("browser.share.add.fail")); + } else { + updater().setSharing(true); + return this.listSharings(); + } + }) + .then(() => { + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateSharingsInfo); + }); + }; + + deleteSharing = async (dirPath: string) => { + return updater() + .deleteSharing(dirPath) + .then((ok) => { + if (!ok) { + alertMsg(this.props.msg.pkg.get("browser.share.del.fail")); + } else { + updater().setSharing(false); + return this.listSharings(); + } + }) + .then(() => { + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateFilesInfo); + }); + }; + + listSharings = async () => { + return updater() + .listSharings() + .then((ok) => { + if (ok) { + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateSharingsInfo); + } + }); + }; + + render() { + const nameWidthClass = `item-name item-name-${ + this.props.ui.isVertical ? "vertical" : "horizontal" + } pointer`; + + const sharingList = this.props.sharingsInfo.sharings.map( + (dirPath: string) => { + return ( +
+ , + {dirPath}, + ])} + />, + + + + + , + ])} + childrenStyles={List([{}, { justifyContent: "flex-end" }])} + /> +
+ ); + } + ); + + return this.props.sharingsInfo.sharings.size === 0 ? ( +
+ , + +

+ {this.props.msg.pkg.get("share.404.title")} +

+ + {this.props.msg.pkg.get("share.404.desc")} + +
, + ])} + childrenStyles={List([ + { flex: "auto", justifyContent: "flex-end" }, + { flex: "auto" }, + ])} + className="padding-l" + /> +
+ ) : ( +
+ + , + + + + {this.props.msg.pkg.get("browser.share.title")} + + + {this.props.msg.pkg.get("browser.share.desc")} + + , + ])} + /> + , + + , + ])} + /> + + {sharingList} +
+ ); + } +} diff --git a/src/client/web/src/components/panel_uploadings.tsx b/src/client/web/src/components/panel_uploadings.tsx new file mode 100644 index 0000000..1ec79d4 --- /dev/null +++ b/src/client/web/src/components/panel_uploadings.tsx @@ -0,0 +1,186 @@ +import * as React from "react"; +import { List } from "immutable"; +import FileSize from "filesize"; + +import { RiUploadCloudFill } from "@react-icons/all-files/ri/RiUploadCloudFill"; +import { RiUploadCloudLine } from "@react-icons/all-files/ri/RiUploadCloudLine"; +import { RiEmotionSadLine } from "@react-icons/all-files/ri/RiEmotionSadLine"; + +import { alertMsg } from "../common/env"; +import { updater } from "./state_updater"; +import { ICoreState, MsgProps, UIProps } from "./core_state"; +import { LoginProps } from "./pane_login"; +import { UploadEntry, UploadState } from "../worker/interface"; +import { Flexbox } from "./layout/flexbox"; + +export interface UploadingsProps { + uploadings: List; + uploadFiles: List; +} + +export interface Props { + uploadingsInfo: UploadingsProps; + msg: MsgProps; + login: LoginProps; + ui: UIProps; + update?: (updater: (prevState: ICoreState) => ICoreState) => void; +} + +export interface State {} + +export class UploadingsPanel extends React.Component { + constructor(p: Props) { + super(p); + this.state = {}; + } + + addUploads = (event: React.ChangeEvent) => { + if (event.target.files.length > 1000) { + alertMsg(this.props.msg.pkg.get("err.tooManyUploads")); + return; + } + + let fileList = List(); + for (let i = 0; i < event.target.files.length; i++) { + fileList = fileList.push(event.target.files[i]); + } + updater().addUploads(fileList); + this.props.update(updater().updateUploadingsInfo); + }; + + deleteUpload = (filePath: string): Promise => { + return updater() + .deleteUpload(filePath) + .then((ok: boolean) => { + if (!ok) { + alertMsg(this.props.msg.pkg.get("browser.upload.del.fail")); + } + return updater().refreshUploadings(); + }) + .then(() => { + return updater().self(); + }) + .then(() => { + this.props.update(updater().updateUploadingsInfo); + this.props.update(updater().updateLogin); + }); + }; + + stopUploading = (filePath: string) => { + updater().stopUploading(filePath); + this.props.update(updater().updateUploadingsInfo); + }; + + render() { + const nameWidthClass = `item-name item-name-${ + this.props.ui.isVertical ? "vertical" : "horizontal" + } pointer`; + + const uploadingList = this.props.uploadingsInfo.uploadings.map( + (uploading: UploadEntry) => { + const pathParts = uploading.filePath.split("/"); + const fileName = pathParts[pathParts.length - 1]; + + return ( +
+ + , + +
+ {fileName} +
+ {FileSize(uploading.uploaded, { round: 0 })} +  / {FileSize(uploading.size, { round: 0 })} +
+
, + ])} + /> + , + +
+ + +
, + ])} + childrenStyles={List([{}, { justifyContent: "flex-end" }])} + /> + {uploading.err.trim() === "" ? null : ( +
{uploading.err.trim()}
+ )} +
+ ); + } + ); + + return this.props.uploadingsInfo.uploadings.size === 0 ? ( +
+ , + +

+ {this.props.msg.pkg.get("upload.404.title")} +

+ + {this.props.msg.pkg.get("upload.404.desc")} + +
, + ])} + childrenStyles={List([ + { flex: "auto", justifyContent: "flex-end" }, + { flex: "auto" }, + ])} + className="padding-l" + /> +
+ ) : ( +
+ + , + + + + {this.props.msg.pkg.get("browser.upload.title")} + + + {this.props.msg.pkg.get("browser.upload.desc")} + + , + ])} + /> + , + + , + ])} + /> + + {uploadingList} +
+ ); + } +} diff --git a/src/client/web/src/components/root_frame.tsx b/src/client/web/src/components/root_frame.tsx index 515e4a3..1f19421 100644 --- a/src/client/web/src/components/root_frame.tsx +++ b/src/client/web/src/components/root_frame.tsx @@ -1,7 +1,10 @@ import * as React from "react"; import { ICoreState, MsgProps, UIProps } from "./core_state"; -import { Browser, BrowserProps } from "./browser"; +import { FilesPanel, FilesProps } from "./panel_files"; +import { UploadingsPanel, UploadingsProps } from "./panel_uploadings"; +import { SharingsPanel, SharingsProps } from "./panel_sharings"; + import { LoginProps } from "./pane_login"; import { Panes, PanesProps } from "./panes"; import { AdminProps } from "./pane_admin"; @@ -9,7 +12,9 @@ import { TopBar } from "./topbar"; import { roleVisitor } from "../client"; export interface Props { - browser: BrowserProps; + filesInfo: FilesProps; + uploadingsInfo: UploadingsProps; + sharingsInfo: SharingsProps; panes: PanesProps; admin: AdminProps; login: LoginProps; @@ -26,7 +31,10 @@ export class RootFrame extends React.Component { render() { let bgStyle = undefined; - if (this.props.login.preferences != null && this.props.login.preferences.bg.url !== "") { + if ( + this.props.login.preferences != null && + this.props.login.preferences.bg.url !== "" + ) { bgStyle = { background: `url("${this.props.login.preferences.bg.url}") ${this.props.login.preferences.bg.repeat} ${this.props.login.preferences.bg.position} ${this.props.login.preferences.bg.align}`, }; @@ -41,7 +49,8 @@ export class RootFrame extends React.Component { const fontSizeClass = "font-m"; const theme = "theme-default"; const showBrowser = - this.props.login.userRole === roleVisitor && !this.props.browser.isSharing + this.props.login.userRole === roleVisitor && + !this.props.filesInfo.isSharing ? "hidden" : ""; @@ -65,12 +74,33 @@ export class RootFrame extends React.Component { />
- */} + + +
diff --git a/src/client/web/src/components/state_mgr.tsx b/src/client/web/src/components/state_mgr.tsx index 5fc25a5..7c9741c 100644 --- a/src/client/web/src/components/state_mgr.tsx +++ b/src/client/web/src/components/state_mgr.tsx @@ -60,7 +60,9 @@ export class StateMgr extends React.Component { return updater() .initAll(params) .then(() => { - this.update(updater().updateBrowser); + this.update(updater().updateFilesInfo); + this.update(updater().updateUploadingsInfo); + this.update(updater().updateSharingsInfo); this.update(updater().updateLogin); this.update(updater().updatePanes); this.update(updater().updateAdmin); @@ -76,7 +78,9 @@ export class StateMgr extends React.Component { render() { return ( { - this.props.browser.uploadings.forEach((entry) => { + this.props.uploadingsInfo.uploadings.forEach((entry) => { Up().addStopped(entry.filePath, entry.uploaded, entry.size); }); // this.setUploadings(Up().list()); @@ -60,7 +60,7 @@ export class Updater { addUploads = (fileList: List) => { fileList.forEach((file) => { const filePath = getItemPath( - this.props.browser.dirPath.join("/"), + this.props.filesInfo.dirPath.join("/"), file.name ); // do not wait for the promise @@ -76,7 +76,7 @@ export class Updater { }; setUploadings = (infos: Map) => { - this.props.browser.uploadings = List( + this.props.uploadingsInfo.uploadings = List( infos.valueSeq().map((entry: UploadEntry): UploadEntry => { return entry; }) @@ -84,7 +84,7 @@ export class Updater { }; addSharing = async (): Promise => { - const dirPath = this.props.browser.dirPath.join("/"); + const dirPath = this.props.filesInfo.dirPath.join("/"); const resp = await this.filesClient.addSharing(dirPath); return resp.status === 200; }; @@ -96,20 +96,20 @@ export class Updater { isSharing = async (dirPath: string): Promise => { const resp = await this.filesClient.isSharing(dirPath); - this.props.browser.isSharing = resp.status === 200; + this.props.filesInfo.isSharing = resp.status === 200; return resp.status === 200; // TODO: differentiate 404 and error }; setSharing = (shared: boolean) => { - this.props.browser.isSharing = shared; + this.props.filesInfo.isSharing = shared; }; listSharings = async (): Promise => { const resp = await this.filesClient.listSharings(); - this.props.browser.sharings = + this.props.sharingsInfo.sharings = resp.status === 200 ? List(resp.data.sharingDirs) - : this.props.browser.sharings; + : this.props.sharingsInfo.sharings; return resp.status === 200; }; @@ -124,7 +124,7 @@ export class Updater { } let localUploads = Map([]); - this.props.browser.uploadings.forEach((entry: UploadEntry) => { + this.props.uploadingsInfo.uploadings.forEach((entry: UploadEntry) => { localUploads = localUploads.set(entry.filePath, entry); }); @@ -152,7 +152,7 @@ export class Updater { } }); - this.props.browser.uploadings = updatedUploads; + this.props.uploadingsInfo.uploadings = updatedUploads; return true; }; @@ -217,12 +217,12 @@ export class Updater { const listResp = await this.filesClient.list(dirPath); if (listResp.status === 200) { - this.props.browser.dirPath = dirParts; - this.props.browser.items = List(listResp.data.metadatas); + this.props.filesInfo.dirPath = dirParts; + this.props.filesInfo.items = List(listResp.data.metadatas); return true; } - this.props.browser.dirPath = List([]); - this.props.browser.items = List([]); + this.props.filesInfo.dirPath = List([]); + this.props.filesInfo.items = List([]); return false; }; @@ -230,12 +230,12 @@ export class Updater { const listResp = await this.filesClient.listHome(); if (listResp.status === 200) { - this.props.browser.dirPath = List(listResp.data.cwd.split("/")); - this.props.browser.items = List(listResp.data.metadatas); + this.props.filesInfo.dirPath = List(listResp.data.cwd.split("/")); + this.props.filesInfo.items = List(listResp.data.metadatas); return true; } - this.props.browser.dirPath = List([]); - this.props.browser.items = List([]); + this.props.filesInfo.dirPath = List([]); + this.props.filesInfo.items = List([]); return false; }; @@ -307,21 +307,19 @@ export class Updater { initPanes = async (): Promise> => { // init browser content if (this.props.login.userRole === roleVisitor) { - if (this.props.browser.isSharing) { + if (this.props.filesInfo.isSharing) { // sharing with visitor this.setPanes(Set(["login"])); this.displayPane(""); return Promise.all([]); } - // redirect to login this.setPanes(Set(["login"])); this.displayPane("login"); return Promise.all([this.getCaptchaID()]); } - if (this.props.login.userRole === roleAdmin) { this.setPanes(Set(["login", "settings", "admin"])); } else { @@ -351,7 +349,7 @@ export class Updater { } }) .then(() => { - return this.isSharing(this.props.browser.dirPath.join("/")); + return this.isSharing(this.props.filesInfo.dirPath.join("/")); }) .then(() => { // init settings @@ -558,16 +556,16 @@ export class Updater { setTab = (tabName: string) => { switch (tabName) { case "item": - this.props.browser.tab = tabName; + this.props.panels.displaying = tabName; break; case "uploading": - this.props.browser.tab = tabName; + this.props.panels.displaying = tabName; break; case "sharing": - this.props.browser.tab = tabName; + this.props.panels.displaying = tabName; break; default: - this.props.browser.tab = "item"; + this.props.panels.displaying = "item"; break; } }; @@ -648,10 +646,27 @@ export class Updater { return resp.status; }; - updateBrowser = (prevState: ICoreState): ICoreState => { + updateFilesInfo = (prevState: ICoreState): ICoreState => { return { ...prevState, - browser: { ...prevState.browser, ...this.props.browser }, + filesInfo: { ...prevState.filesInfo, ...this.props.filesInfo }, + }; + }; + + updateUploadingsInfo = (prevState: ICoreState): ICoreState => { + return { + ...prevState, + uploadingsInfo: { + ...prevState.uploadingsInfo, + ...this.props.uploadingsInfo, + }, + }; + }; + + updateSharingsInfo = (prevState: ICoreState): ICoreState => { + return { + ...prevState, + sharingsInfo: { ...prevState.sharingsInfo, ...this.props.sharingsInfo }, }; }; diff --git a/src/client/web/src/components/topbar.tsx b/src/client/web/src/components/topbar.tsx index 0333b5f..22aeb27 100644 --- a/src/client/web/src/components/topbar.tsx +++ b/src/client/web/src/components/topbar.tsx @@ -63,7 +63,9 @@ export class TopBar extends React.Component { return this.refreshCaptcha(); }) .then(() => { - this.props.update(updater().updateBrowser); + this.props.update(updater().updateFilesInfo); + this.props.update(updater().updateUploadingsInfo); + this.props.update(updater().updateSharingsInfo); this.props.update(updater().updateLogin); this.props.update(updater().updatePanes); this.props.update(updater().updateAdmin); diff --git a/src/client/web/src/i18n/en_US.ts b/src/client/web/src/i18n/en_US.ts index b11c3b0..191d0c8 100644 --- a/src/client/web/src/i18n/en_US.ts +++ b/src/client/web/src/i18n/en_US.ts @@ -105,4 +105,5 @@ export const msgs: Map = Map({ "settings.customLan": "Customized Language Pack", "settings.lanPackURL": "Language Pack URL", "op.fail": "Operation Failed", + "op.confirm": "Do you confirm to apply the action?", }); diff --git a/src/client/web/src/i18n/zh_CN.ts b/src/client/web/src/i18n/zh_CN.ts index daf3723..905f231 100644 --- a/src/client/web/src/i18n/zh_CN.ts +++ b/src/client/web/src/i18n/zh_CN.ts @@ -104,4 +104,5 @@ export const msgs: Map = Map({ "settings.customLan": "自定义语言包", "settings.lanPackURL": "语言包链接", "op.fail": "操作失败", + "op.confirm": "你确定执行此操作吗?", }); diff --git a/src/client/web/src/worker/upload_mgr.ts b/src/client/web/src/worker/upload_mgr.ts index c40474d..3f87e18 100644 --- a/src/client/web/src/worker/upload_mgr.ts +++ b/src/client/web/src/worker/upload_mgr.ts @@ -90,6 +90,8 @@ export class UploadMgr { return this.cycle; }; + // TODO: change it to observer pattern + // so that it can be observed by multiple components setStatusCb = ( cb: (infos: Map, refresh: boolean) => void ) => {