1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-05 00:43:53 +00:00

Shared dir view redesign (#7468)

* [shared dir view] redesigned it

- changed to a full-screen 'side panel & main panel' layout
- added a 'folder tree' panel, a current path bar with an operation dropdown
  menu, a toolbar for selected items, a 'view mode' menu, and a 'sort'
  menu

* [shared dir view] added a resizing bar (enable users to resize the width of side panel and main panel)

* [shared dir view] path bar: added a 'plus' icon to the dropdown menu toggle when 'upload' is offered in the menu

* [shared dir view] folder tree: cleaned up the code

* [shared dir view] improved 'visit a folder'

* [shared dir view] improvements & cleanup
This commit is contained in:
llj
2025-02-17 14:58:30 +08:00
committed by GitHub
parent 06410d217d
commit 79413cb4fd
12 changed files with 1236 additions and 205 deletions

View File

@@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import MD5 from 'MD5';
import { createRoot } from 'react-dom/client';
import { Button, Dropdown, DropdownToggle, DropdownItem, UncontrolledTooltip } from 'reactstrap';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, UncontrolledTooltip } from 'reactstrap';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import Account from './components/common/account';
@@ -20,9 +20,18 @@ import SaveSharedDirDialog from './components/dialog/save-shared-dir-dialog';
import CopyMoveDirentProgressDialog from './components/dialog/copy-move-dirent-progress-dialog';
import RepoInfoBar from './components/repo-info-bar';
import RepoTag from './models/repo-tag';
import { GRID_MODE, LIST_MODE } from './components/dir-view-mode/constants';
import { LIST_MODE } from './components/dir-view-mode/constants';
import { MetadataAIOperationsProvider } from './hooks/metadata-ai-operation';
import ViewModes from './components/view-modes';
import SortMenu from './components/sort-menu';
import { TreeHelper, TreeNode, TreeView } from './components/shared-dir-tree-view';
import ResizeBar from './components/resize-bar';
import {
DRAG_HANDLER_HEIGHT, INIT_SIDE_PANEL_RATE, MAX_SIDE_PANEL_RATE, MIN_SIDE_PANEL_RATE
} from './components/resize-bar/constants';
import './css/layout.css';
import './css/header.css';
import './css/shared-dir-view.css';
import './css/grid-view.css';
@@ -33,7 +42,7 @@ let loginUser = window.app.pageOptions.name;
let {
token, dirName, dirPath, sharedBy,
repoID, relativePath,
mode, thumbnailSize, zipped,
mode, thumbnailSize,
trafficOverLimit, canDownload,
noQuota, canUpload, enableVideoThumbnail, enablePDFThumbnail
} = window.shared.pageOptions;
@@ -45,10 +54,19 @@ class SharedDirView extends React.Component {
constructor(props) {
super(props);
this.state = {
isTreeDataLoading: true,
treeData: TreeHelper.buildTree(),
// for resizing side/main panels
inResizing: false,
sidePanelRate: parseFloat(localStorage.getItem('sf_side_panel_rate') || INIT_SIDE_PANEL_RATE),
isLoading: true,
errorMsg: '',
items: [],
path: relativePath,
isDropdownMenuOpen: false,
currentMode: mode,
isAllItemsSelected: false,
@@ -61,7 +79,6 @@ class SharedDirView extends React.Component {
zipFolderPath: '',
usedRepoTags: [],
isRepoInfoBarShow: false,
isSaveSharedDirDialogShow: false,
itemsForSave: [],
@@ -75,6 +92,9 @@ class SharedDirView extends React.Component {
imageItems: [],
imageIndex: 0
};
this.resizeBarRef = React.createRef();
this.dragHandlerRef = React.createRef();
}
componentDidMount() {
@@ -84,12 +104,54 @@ class SharedDirView extends React.Component {
});
}
this.listItems(thumbnailSize);
this.loadTreePanel();
this.listItems();
this.getShareLinkRepoTags();
}
listItems = (thumbnailSize) => {
seafileAPI.listSharedDir(token, relativePath, thumbnailSize).then((res) => {
loadTreePanel = () => {
seafileAPI.listSharedDir(token, '/', thumbnailSize).then((res) => {
const { dirent_list } = res.data;
let tree = this.state.treeData;
this.addResponseListToNode(dirent_list, tree.root);
this.setState({
isTreeDataLoading: false,
treeData: tree
});
}).catch(() => {
this.setState({ isTreeDataLoading: false });
});
/* keep it for now
if (relativePath == '/') {
} else {
this.loadNodeAndParentsByPath(relativePath);
}
*/
};
/*
loadNodeAndParentsByPath = (path) => {
};
*/
addResponseListToNode = (list, node) => {
node.isLoaded = true;
node.isExpanded = true;
// only display folders in the tree
const dirList = list.filter(item => item.is_dir);
const direntList = Utils.sortDirentsInSharedDir(dirList, this.state.sortBy, this.state.sortOrder);
const nodeList = direntList.map(object => {
return new TreeNode({ object });
});
node.addChildren(nodeList);
};
listItems = () => {
const { path, currentMode } = this.state;
const thumbnailSize = currentMode == LIST_MODE ? thumbnailDefaultSize : thumbnailSizeForGrid;
seafileAPI.listSharedDir(token, path, thumbnailSize).then((res) => {
const items = res.data['dirent_list'].map(item => {
item.isSelected = false;
return item;
@@ -107,6 +169,21 @@ class SharedDirView extends React.Component {
errorMsg: errorMsg
});
});
// update the URL
let normalizedPath = '';
if (path == '/') {
normalizedPath = path;
} else {
normalizedPath = path[path.length - 1] === '/' ? path.slice(0, path.length - 1) : path;
}
let url = new URL(location.href);
let searchParams = new URLSearchParams(url.search);
searchParams.set('p', normalizedPath);
searchParams.set('mode', currentMode);
url.search = searchParams.toString();
url = url.toString();
window.history.pushState({ url: url, path: path }, path, url);
};
sortItems = (sortBy, sortOrder) => {
@@ -151,22 +228,136 @@ class SharedDirView extends React.Component {
getThumbnail(0);
};
toggleDropdownMenu = () => {
this.setState({
isDropdownMenuOpen: !this.state.isDropdownMenuOpen
});
};
onDropdownToggleKeyDown = (e) => {
if (e.key == 'Enter' || e.key == 'Space') {
this.toggleDropdownMenu();
}
};
onMenuItemKeyDown = (item, e) => {
if (e.key == 'Enter' || e.key == 'Space') {
item.onClick();
}
};
visitFolder = (folderPath) => {
this.setState({
path: folderPath
}, () => {
this.listItems();
});
};
renderPath = () => {
let opList = [];
if (showDownloadIcon) {
opList.push({
'icon': 'download1',
'text': gettext('ZIP'),
'onClick': this.zipDownloadFolder.bind(this, this.state.path)
});
if (canDownload && loginUser && (loginUser !== sharedBy)) {
opList.push({
'icon': 'save',
'text': gettext('Save'),
'onClick': this.saveAllItems
});
}
}
if (canUpload) {
opList.push({
'icon': 'upload-files',
'disabled': noQuota,
'title': noQuota ? gettext('The owner of this library has run out of space.') : '',
'text': gettext('Upload'),
'onClick': this.onUploadFile
});
}
const zipped = []; // be compatible with the old code
const rootItem = {
path: '/',
name: dirName
};
zipped.push(rootItem);
const { path } = this.state;
if (path != '/') {
const normalizedPath = path[path.length - 1] === '/' ? path.slice(0, path.length - 1) : path;
const pathList = normalizedPath.split('/');
pathList.shift();
let itemPath = '';
const subItems = pathList.map((item, index) => {
itemPath += '/' + item;
return {
path: itemPath + '/', // the ending '/' is necessary
name: item
};
});
zipped.push(...subItems);
}
return (
<React.Fragment>
{zipped.map((item, index) => {
if (index != zipped.length - 1) {
return (
<React.Fragment key={index}>
<a href={`?p=${encodeURIComponent(item.path)}&mode=${mode}`} className="mx-1 ellipsis" title={item.name}>{item.name}</a>
<span> / </span>
<span className="path-item" title={item.name} role="button" onClick={this.visitFolder.bind(this, item.path)}>{item.name}</span>
<span className="path-split"> / </span>
</React.Fragment>
);
}
return null;
})
})}
{(!showDownloadIcon && !canUpload)
? <span className="path-item" title={zipped[zipped.length - 1].name}>{zipped[zipped.length - 1].name}</span>
: (
<Dropdown isOpen={this.state.isDropdownMenuOpen} toggle={this.toggleDropdownMenu}>
<DropdownToggle
tag="div"
role="button"
className="path-item path-item-dropdown-toggle"
onClick={this.toggleDropdownMenu}
onKeyDown={this.onDropdownToggleKeyDown}
data-toggle="dropdown"
>
<span title={zipped[zipped.length - 1].name}>{zipped[zipped.length - 1].name}</span>
{canUpload
? <><i className="sf3-font-new sf3-font main-icon ml-2"></i><i className="sf3-font-down sf3-font"></i></>
: <i className="sf3-font-down sf3-font ml-1"></i>
}
</DropdownToggle>
<DropdownMenu positionFixed={true}>
{opList.map((item, index) => {
if (item == 'Divider') {
return <DropdownItem key={index} divider />;
} else {
return (
<DropdownItem
key={index}
onClick={item.onClick}
onKeyDown={this.onMenuItemKeyDown.bind(this, item)}
disabled={item.disabled || false}
title={item.title || ''}
>
<i className={`sf3-font-${item.icon} sf3-font mr-2 dropdown-item-icon`}></i>
{item.text}
</DropdownItem>
);
}
})}
</DropdownMenu>
</Dropdown>
)
}
<span className="ml-1 ellipsis" title={zipped[zipped.length - 1].name}>{zipped[zipped.length - 1].name}</span>
</React.Fragment>
);
};
@@ -193,17 +384,18 @@ class SharedDirView extends React.Component {
};
zipDownloadSelectedItems = () => {
const { path } = this.state;
if (!useGoFileserver) {
this.setState({
isZipDialogOpen: true,
zipFolderPath: relativePath,
zipFolderPath: path,
selectedItems: this.state.items.filter(item => item.isSelected)
.map(item => item.file_name || item.folder_name)
});
}
else {
let target = this.state.items.filter(item => item.isSelected).map(item => item.file_name || item.folder_name);
seafileAPI.getShareLinkDirentsZipTask(token, relativePath, target).then((res) => {
seafileAPI.getShareLinkDirentsZipTask(token, path, target).then((res) => {
const zipToken = res.data['zip_token'];
location.href = `${fileServerRoot}zip/${zipToken}`;
}).catch((error) => {
@@ -277,10 +469,8 @@ class SharedDirView extends React.Component {
};
handleSaveSharedDir = (destRepoID, dstPath) => {
const itemsForSave = this.state.itemsForSave;
seafileAPI.saveSharedDir(destRepoID, dstPath, token, relativePath, itemsForSave).then((res) => {
const { path, itemsForSave } = this.state;
seafileAPI.saveSharedDir(destRepoID, dstPath, token, path, itemsForSave).then((res) => {
this.setState({
isSaveSharedDirDialogShow: false,
itemsForSave: [],
@@ -376,6 +566,16 @@ class SharedDirView extends React.Component {
}));
};
unselectItems = () => {
this.setState({
isAllItemsSelected: false,
items: this.state.items.map((item) => {
item.isSelected = false;
return item;
})
});
};
toggleAllSelected = () => {
this.setState((prevState) => ({
isAllItemsSelected: !prevState.isAllItemsSelected,
@@ -407,11 +607,12 @@ class SharedDirView extends React.Component {
};
onFileUploadSuccess = (direntObject) => {
const { path } = this.state;
const { name, size } = direntObject;
const newItem = {
isSelected: false,
file_name: name,
file_path: Utils.joinPath(relativePath, name),
file_path: Utils.joinPath(path, name),
is_dir: false,
last_modified: dayjs().format(),
size: size
@@ -434,9 +635,6 @@ class SharedDirView extends React.Component {
}
});
this.setState({ usedRepoTags: usedRepoTags });
if (Utils.isDesktop() && usedRepoTags.length != 0 && relativePath == '/') {
this.setState({ isRepoInfoBarShow: true });
}
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@@ -452,76 +650,203 @@ class SharedDirView extends React.Component {
currentMode: mode,
isLoading: true
}, () => {
const thumbnailSize = mode == LIST_MODE ? thumbnailDefaultSize : thumbnailSizeForGrid;
this.listItems(thumbnailSize);
this.listItems();
});
}
};
onSelectSortOption = (item) => {
const [sortBy, sortOrder] = item.value.split('-');
this.sortItems(sortBy, sortOrder);
};
onTreeNodeCollapse = (node) => {
const tree = TreeHelper.collapseNode(this.state.treeData, node);
this.setState({ treeData: tree });
};
onTreeNodeExpanded = (node) => {
let tree = this.state.treeData.clone();
node = tree.getNodeByPath(node.path);
if (!node.isLoaded) {
seafileAPI.listSharedDir(token, node.path, thumbnailSize).then((res) => {
const { dirent_list } = res.data;
this.addResponseListToNode(dirent_list, node);
this.setState({ treeData: tree });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else {
tree.expandNode(node);
this.setState({ treeData: tree });
}
};
onTreeNodeClick = (node) => {
if (node.object.is_dir) {
if (node.isLoaded && node.path === this.state.path) {
if (node.isExpanded) {
let tree = TreeHelper.collapseNode(this.state.treeData, node);
this.setState({ treeData: tree });
} else {
let tree = this.state.treeData.clone();
node = tree.getNodeByPath(node.path);
tree.expandNode(node);
this.setState({ treeData: tree });
}
}
if (!node.isLoaded) {
let tree = this.state.treeData.clone();
node = tree.getNodeByPath(node.path);
seafileAPI.listSharedDir(token, node.path, thumbnailSize).then((res) => {
const { dirent_list } = res.data;
this.addResponseListToNode(dirent_list, node);
tree.collapseNode(node);
this.setState({ treeData: tree });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
if (node.path === this.state.path) {
return;
}
this.visitFolder(node.path);
}
};
onResizeMouseUp = () => {
if (this.state.inResizing) {
this.setState({
inResizing: false
});
}
localStorage.setItem('sf_side_panel_rate', this.state.sidePanelRate);
};
onResizeMouseDown = () => {
this.setState({
inResizing: true
});
};
onResizeMouseMove = (e) => {
let rate = e.nativeEvent.clientX / window.innerWidth;
this.setState({
sidePanelRate: Math.max(Math.min(rate, MAX_SIDE_PANEL_RATE), MIN_SIDE_PANEL_RATE),
});
};
onResizeMouseOver = (event) => {
if (!this.dragHandlerRef.current) return;
const { top } = this.resizeBarRef.current.getBoundingClientRect();
const dragHandlerRefTop = event.pageY - top - DRAG_HANDLER_HEIGHT / 2;
this.setDragHandlerTop(dragHandlerRefTop);
};
setDragHandlerTop = (top) => {
this.dragHandlerRef.current.style.top = top + 'px';
};
render() {
const { currentMode: mode } = this.state;
const {
usedRepoTags, currentMode: mode,
sortBy, sortOrder, isTreeDataLoading, treeData, path,
sidePanelRate, inResizing
} = this.state;
const mainPanelStyle = {
userSelect: inResizing ? 'none' : '',
flex: sidePanelRate ? `1 0 ${(1 - sidePanelRate) * 100}%` : `0 0 ${100 - INIT_SIDE_PANEL_RATE * 100}%`,
};
const sidePanelStyle = {
userSelect: inResizing ? 'none' : '',
flex: sidePanelRate ? `0 0 ${sidePanelRate * 100}%` : `0 0 ${INIT_SIDE_PANEL_RATE * 100}%`,
};
const isDesktop = Utils.isDesktop();
const modeBaseClass = 'btn btn-secondary btn-icon sf-view-mode-btn';
const selectedItemsLength = this.state.items.filter(item => item.isSelected).length;
const isRepoInfoBarShown = isDesktop && path == '/' && usedRepoTags.length != 0;
return (
<MetadataAIOperationsProvider repoID={repoID} enableMetadata={false} enableOCR={false} repoInfo={{ permission: 'r' }} >
<div className="h-100 d-flex flex-column">
<div className="top-header d-flex justify-content-between">
<div id="shared-dir-view" className="h-100 d-flex flex-column">
<div className="top-header d-flex justify-content-between flex-shrink-0">
<a href={siteRoot}>
<img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
</a>
{loginUser && <Account />}
</div>
<div className="o-auto">
<div className="shared-dir-view-main">
<h2 className="h3 text-truncate" title={dirName}>{dirName}</h2>
<p>{gettext('Shared by: ')}{sharedBy}</p>
<div className="d-flex justify-content-between align-items-center op-bar">
<p className="m-0 mr-4 ellipsis d-flex align-items-center">{gettext('Current path: ')}{this.renderPath()}</p>
<div className="flex-none">
{isDesktop &&
<div className="view-mode btn-group">
<button
className={`${modeBaseClass} sf2-icon-list-view ${mode == LIST_MODE ? 'current-mode' : ''}`}
title={gettext('List')}
aria-label={gettext('List')}
onClick={this.switchMode.bind(this, LIST_MODE)}
>
</button>
<button
className={`${modeBaseClass} sf2-icon-grid-view ${mode == GRID_MODE ? 'current-mode' : ''}`}
title={gettext('Grid')}
aria-label={gettext('Grid')}
onClick={this.switchMode.bind(this, GRID_MODE)}
>
</button>
</div>
<div
className="flex-fill d-flex o-hidden position-relative"
onMouseMove={inResizing ? this.onResizeMouseMove : null}
onMouseUp={this.onResizeMouseUp}
>
<div className="side-panel" style={sidePanelStyle}>
<div className="meta-info py-4 mx-4">
<h2 className="h3 text-truncate mb-4" title={dirName}>{dirName}</h2>
<p className="m-0">{gettext('Shared by: ')}{sharedBy}</p>
</div>
<div className="p-4 flex-fill o-auto">
{isTreeDataLoading ? <Loading /> : (
<TreeView
currentPath={path}
treeData={treeData}
onNodeExpanded={this.onTreeNodeExpanded}
onNodeCollapse={this.onTreeNodeCollapse}
onNodeClick={this.onTreeNodeClick}
/>
)}
</div>
</div>
{isDesktop &&
<ResizeBar
resizeBarRef={this.resizeBarRef}
dragHandlerRef={this.dragHandlerRef}
resizeBarStyle={{ left: `calc(${sidePanelRate ? sidePanelRate * 100 + '%' : `${INIT_SIDE_PANEL_RATE * 100}%`} - 1px)` }}
dragHandlerStyle={{ height: DRAG_HANDLER_HEIGHT }}
onResizeMouseDown={this.onResizeMouseDown}
onResizeMouseOver={this.onResizeMouseOver}
/>
}
<div className="main-panel cur-view-container" style={mainPanelStyle}>
<div className="cur-view-path d-flex justify-content-between align-items-center">
<div className="cur-view-path-left flex-fill o-hidden">
{(showDownloadIcon && this.state.items.some(item => item.isSelected))
? (
<div className="selected-items-toolbar">
<span className="cur-view-path-btn px-1" onClick={this.unselectItems}>
<span className="sf3-font-x-01 sf3-font mr-2" aria-label={gettext('Unselect')} title={gettext('Unselect')}></span>
<span>{`${selectedItemsLength} ${gettext('selected')}`}</span>
</span>
<span className="cur-view-path-btn ml-4" onClick={this.zipDownloadSelectedItems}>
<span className="sf3-font-download1 sf3-font" aria-label={gettext('Download')} title={gettext('Download')}></span>
</span>
{(canDownload && loginUser && (loginUser !== sharedBy)) &&
<span className="cur-view-path-btn ml-4" onClick={this.saveSelectedItems}>
<span className="sf3-font-save sf3-font" aria-label={gettext('Save')} title={gettext('Save')}></span>
</span>
}
</div>
)
: (
<div className="path-container">
<span className="mr-2">{gettext('Current path: ')}</span>
{this.renderPath()}
</div>
)
}
{canUpload && (
<Button disabled={noQuota}
title={noQuota ? gettext('The owner of this library has run out of space.') : ''}
onClick={this.onUploadFile} className="ml-2 shared-dir-op-btn shared-dir-upload-btn"
>{gettext('Upload')}
</Button>
</div>
<div className="cur-view-path-right">
{isDesktop && (
<>
<ViewModes currentViewMode={mode} switchViewMode={this.switchMode} />
<SortMenu sortBy={sortBy} sortOrder={sortOrder} onSelectSortOption={this.onSelectSortOption} />
</>
)}
{showDownloadIcon &&
<Fragment>
{this.state.items.some(item => item.isSelected) ?
<Fragment>
<Button color="success" onClick={this.zipDownloadSelectedItems} className="ml-2 shared-dir-op-btn">{gettext('ZIP Selected Items')}</Button>
{(canDownload && loginUser && (loginUser !== sharedBy)) &&
<Button color="success" onClick={this.saveSelectedItems} className="ml-2 shared-dir-op-btn">{gettext('Save Selected Items')}</Button>
}
</Fragment>
:
<Fragment>
<Button color="success" onClick={this.zipDownloadFolder.bind(this, relativePath)} className="ml-2 shared-dir-op-btn">{gettext('ZIP')}</Button>
{(canDownload && loginUser && (loginUser !== sharedBy)) &&
<Button color="success" onClick={this.saveAllItems} className="ml-2 shared-dir-op-btn">{gettext('Save')}</Button>
}
</Fragment>
}
</Fragment>
}
</div>
</div>
{!noQuota && canUpload && (
@@ -530,38 +855,40 @@ class SharedDirView extends React.Component {
dragAndDrop={false}
token={token}
path={dirPath === '/' ? dirPath : dirPath.replace(/\/+$/, '')}
relativePath={relativePath === '/' ? relativePath : relativePath.replace(/\/+$/, '')}
relativePath={path === '/' ? path : path.replace(/\/+$/, '')}
repoID={repoID}
onFileUploadSuccess={this.onFileUploadSuccess}
/>
)}
{this.state.isRepoInfoBarShow && (
<RepoInfoBar
repoID={repoID}
currentPath={'/'}
usedRepoTags={this.state.usedRepoTags}
shareLinkToken={token}
enableFileDownload={showDownloadIcon}
className="mx-0"
/>
)}
<div className="cur-view-content p-0">
{isRepoInfoBarShown && (
<RepoInfoBar
repoID={repoID}
currentPath={'/'}
usedRepoTags={this.state.usedRepoTags}
shareLinkToken={token}
enableFileDownload={showDownloadIcon}
/>
)}
<Content
isDesktop={isDesktop}
isLoading={this.state.isLoading}
errorMsg={this.state.errorMsg}
mode={mode}
items={this.state.items}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
sortItems={this.sortItems}
isAllItemsSelected={this.state.isAllItemsSelected}
toggleAllSelected={this.toggleAllSelected}
toggleItemSelected={this.toggleItemSelected}
zipDownloadFolder={this.zipDownloadFolder}
showImagePopup={this.showImagePopup}
/>
<Content
isDesktop={isDesktop}
isLoading={this.state.isLoading}
errorMsg={this.state.errorMsg}
mode={mode}
items={this.state.items}
sortBy={this.state.sortBy}
sortOrder={this.state.sortOrder}
sortItems={this.sortItems}
isAllItemsSelected={this.state.isAllItemsSelected}
toggleAllSelected={this.toggleAllSelected}
toggleItemSelected={this.toggleItemSelected}
visitFolder={this.visitFolder}
zipDownloadFolder={this.zipDownloadFolder}
showImagePopup={this.showImagePopup}
/>
</div>
</div>
</div>
</div>
@@ -578,7 +905,7 @@ class SharedDirView extends React.Component {
{this.state.isSaveSharedDirDialogShow &&
<SaveSharedDirDialog
sharedToken={token}
parentDir={relativePath}
parentDir={path}
items={this.state.itemsForSave}
toggleCancel={this.toggleSaveSharedDirCancel}
handleSaveSharedDir={this.handleSaveSharedDir}
@@ -660,6 +987,7 @@ class Content extends React.Component {
isDesktop={isDesktop}
mode={mode}
item={item}
visitFolder={this.props.visitFolder}
zipDownloadFolder={this.props.zipDownloadFolder}
showImagePopup={this.props.showImagePopup}
toggleItemSelected={this.props.toggleItemSelected}
@@ -685,24 +1013,26 @@ class Content extends React.Component {
const sortIcon = <span className={`sf3-font ${sortOrder == 'asc' ? 'sf3-font-down rotate-180 d-inline-block' : 'sf3-font-down'}`}></span>;
return mode == LIST_MODE ? (
<table className="table-hover">
<thead>
<tr>
{showDownloadIcon &&
<th width="3%" className="text-center">
<input type="checkbox" checked={isAllItemsSelected} onChange={this.props.toggleAllSelected} />
</th>
}
<th width="5%"></th>
<th width={showDownloadIcon ? '50%' : '53%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortBy == 'name' && sortIcon}</a></th>
<th width="8%"></th>
<th width="14%"><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {sortBy == 'size' && sortIcon}</a></th>
<th width="13%"><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {sortBy == 'time' && sortIcon}</a></th>
<th width="7%"></th>
</tr>
</thead>
{tbody}
</table>
<div className="table-container">
<table className="table-hover">
<thead>
<tr>
{showDownloadIcon &&
<th width="3%" className="text-center">
<input type="checkbox" checked={isAllItemsSelected} onChange={this.props.toggleAllSelected} />
</th>
}
<th width="5%"></th>
<th width={showDownloadIcon ? '50%' : '53%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortBy == 'name' && sortIcon}</a></th>
<th width="8%"></th>
<th width="14%"><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {sortBy == 'size' && sortIcon}</a></th>
<th width="13%"><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {sortBy == 'time' && sortIcon}</a></th>
<th width="7%"></th>
</tr>
</thead>
{tbody}
</table>
</div>
) : (
<ul className="grid-view">
{items.map((item, index) => {
@@ -710,6 +1040,7 @@ class Content extends React.Component {
key={index}
mode={mode}
item={item}
visitFolder={this.props.visitFolder}
zipDownloadFolder={this.props.zipDownloadFolder}
showImagePopup={this.props.showImagePopup}
/>;
@@ -733,6 +1064,7 @@ Content.propTypes = {
toggleItemSelected: PropTypes.func,
zipDownloadFolder: PropTypes.func,
showImagePopup: PropTypes.func,
visitFolder: PropTypes.func.isRequired
};
class Item extends React.Component {
@@ -776,6 +1108,13 @@ class Item extends React.Component {
this.props.toggleItemSelected(this.props.item, e.target.checked);
};
onFolderItemClick = (e) => {
e.preventDefault();
const { item } = this.props;
const { folder_path } = item;
this.props.visitFolder(folder_path);
};
render() {
const { item, isDesktop, mode } = this.props;
const { isIconShown } = this.state;
@@ -797,14 +1136,14 @@ class Item extends React.Component {
}
<td className="text-center"><img src={Utils.getFolderIconUrl()} alt="" width="24" /></td>
<td>
<a href={`?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`}>{item.folder_name}</a>
<a href={`?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`} onClick={this.onFolderItemClick}>{item.folder_name}</a>
</td>
<td></td>
<td></td>
<td title={dayjs(item.last_modified).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(item.last_modified).fromNow()}</td>
<td>
{showDownloadIcon &&
<a role="button" className={`action-icon sf2-icon-download${isIconShown ? '' : ' invisible'}`} href="#" onClick={this.zipDownloadFolder} title={gettext('Download')} aria-label={gettext('Download')}>
<a role="button" className={`op-icon sf3-font sf3-font-download1${isIconShown ? '' : ' invisible'}`} href="#" onClick={this.zipDownloadFolder} title={gettext('Download')} aria-label={gettext('Download')}>
</a>
}
</td>
@@ -813,7 +1152,7 @@ class Item extends React.Component {
<tr>
<td className="text-center"><img src={Utils.getFolderIconUrl()} alt="" width="24" /></td>
<td>
<a href={`?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`}>{item.folder_name}</a>
<a href={`?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`} onClick={this.onFolderItemClick}>{item.folder_name}</a>
<br />
<span className="item-meta-info">{dayjs(item.last_modified).fromNow()}</span>
</td>
@@ -879,7 +1218,7 @@ class Item extends React.Component {
<td title={dayjs(item.last_modified).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(item.last_modified).fromNow()}</td>
<td>
{showDownloadIcon &&
<a className={`action-icon sf2-icon-download${isIconShown ? '' : ' invisible'}`} href={`${fileURL}&dl=1`} title={gettext('Download')} aria-label={gettext('Download')}></a>
<a className={`op-icon sf3-font sf3-font-download1${isIconShown ? '' : ' invisible'}`} href={`${fileURL}&dl=1`} title={gettext('Download')} aria-label={gettext('Download')}></a>
}
</td>
</tr>
@@ -934,6 +1273,7 @@ Item.propTypes = {
toggleItemSelected: PropTypes.func,
zipDownloadFolder: PropTypes.func,
showImagePopup: PropTypes.func,
visitFolder: PropTypes.func.isRequired
};
class GridItem extends React.Component {
@@ -968,6 +1308,13 @@ class GridItem extends React.Component {
this.props.showImagePopup(item);
};
onFolderItemClick = (e) => {
e.preventDefault();
const { item } = this.props;
const { folder_path } = item;
this.props.visitFolder(folder_path);
};
render() {
const { item, mode } = this.props;
const { isIconShown } = this.state;
@@ -976,12 +1323,12 @@ class GridItem extends React.Component {
const folderURL = `?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`;
return (
<li className="grid-item" onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
<a href={folderURL} className="grid-file-img-link d-block">
<a href={folderURL} className="grid-file-img-link d-block" onClick={this.onFolderItemClick}>
<img src={Utils.getFolderIconUrl(false, 192)} alt="" width="80" height="80" />
</a>
<a href={folderURL} className="grid-file-name grid-file-name-link">{item.folder_name}</a>
<a href={folderURL} className="grid-file-name grid-file-name-link" onClick={this.onFolderItemClick}>{item.folder_name}</a>
{showDownloadIcon &&
<a role="button" className={`action-icon sf2-icon-download${isIconShown ? '' : ' invisible'}`} href="#" onClick={this.zipDownloadFolder} title={gettext('Download')} aria-label={gettext('Download')}>
<a role="button" className={`action-icon sf3-font sf3-font-download1${isIconShown ? '' : ' invisible'}`} href="#" onClick={this.zipDownloadFolder} title={gettext('Download')} aria-label={gettext('Download')}>
</a>
}
</li>
@@ -999,7 +1346,7 @@ class GridItem extends React.Component {
</a>
<a href={fileURL} className="grid-file-name grid-file-name-link" onClick={this.handleFileClick}>{item.file_name}</a>
{showDownloadIcon &&
<a className={`action-icon sf2-icon-download${isIconShown ? '' : ' invisible'}`} href={`${fileURL}&dl=1`} title={gettext('Download')} aria-label={gettext('Download')}>
<a className={`action-icon sf3-font sf3-font-download1${isIconShown ? '' : ' invisible'}`} href={`${fileURL}&dl=1`} title={gettext('Download')} aria-label={gettext('Download')}>
</a>
}
</li>
@@ -1013,6 +1360,7 @@ GridItem.propTypes = {
item: PropTypes.object,
zipDownloadFolder: PropTypes.func,
showImagePopup: PropTypes.func,
visitFolder: PropTypes.func.isRequired
};
const root = createRoot(document.getElementById('wrapper'));