1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-07-23 03:13:40 +00:00
seahub/frontend/src/components/dirent-list-view/dirent-list-view.js

750 lines
23 KiB
JavaScript

import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { siteRoot, gettext, thumbnailSizeForOriginal, username, isPro, enableFileComment, fileAuditEnabled, folderPermEnabled, canGenerateShareLink } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import TextTranslation from '../../utils/text-translation';
import { seafileAPI } from '../../utils/seafile-api';
import URLDecorator from '../../utils/url-decorator';
import Loading from '../loading';
import toaster from '../toast';
import ModalPortal from '../modal-portal';
import CreateFile from '../dialog/create-file-dialog';
import CreateFolder from '../dialog/create-folder-dialog';
import ImageDialog from '../dialog/image-dialog';
import ZipDownloadDialog from '../dialog/zip-download-dialog';
import MoveDirentDialog from '../dialog/move-dirent-dialog';
import CopyDirentDialog from '../dialog/copy-dirent-dialog';
import DirentListItem from './dirent-list-item';
import ContextMenu from '../context-menu/context-menu';
import { hideMenu, showMenu } from '../context-menu/actions';
import DirentsDraggedPreview from '../draggable/dirents-dragged-preview';
const propTypes = {
path: PropTypes.string.isRequired,
repoID: PropTypes.string.isRequired,
currentRepoInfo: PropTypes.object,
isAllItemSelected: PropTypes.bool.isRequired,
isDirentListLoading: PropTypes.bool.isRequired,
direntList: PropTypes.array.isRequired,
sortBy: PropTypes.string.isRequired,
sortOrder: PropTypes.string.isRequired,
sortItems: PropTypes.func.isRequired,
onAddFile: PropTypes.func.isRequired,
onAddFolder: PropTypes.func.isRequired,
onItemDelete: PropTypes.func.isRequired,
onAllItemSelected: PropTypes.func.isRequired,
onItemSelected: PropTypes.func.isRequired,
onItemRename: PropTypes.func.isRequired,
onItemClick: PropTypes.func.isRequired,
onItemMove: PropTypes.func.isRequired,
onItemCopy: PropTypes.func.isRequired,
onDirentClick: PropTypes.func.isRequired,
updateDirent: PropTypes.func.isRequired,
selectedDirentList: PropTypes.array.isRequired,
onItemsMove: PropTypes.func.isRequired,
onItemsCopy: PropTypes.func.isRequired,
onItemsDelete: PropTypes.func.isRequired,
onFileTagChanged: PropTypes.func,
enableDirPrivateShare: PropTypes.bool.isRequired,
isGroupOwnedRepo: PropTypes.bool.isRequired,
userPerm: PropTypes.string,
showDirentDetail: PropTypes.func.isRequired,
loadDirentList: PropTypes.func.isRequired,
};
class DirentListView extends React.Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false,
isImagePopupOpen: false,
imageItems: [],
imageIndex: 0,
fileType: '',
isCreateFileDialogShow: false,
isCreateFolderDialogShow: false,
isMoveDialogShow: false,
isCopyDialogShow: false,
isProgressDialogShow: false,
downloadItems: [],
isMutipleOperation: true,
activeDirent: null,
isListDropTipShow: false,
isShowDirentsDraggablePreview: false,
};
this.enteredCounter = 0; // Determine whether to enter the child element to avoid dragging bubbling bugs。
this.isRepoOwner = props.currentRepoInfo.owner_email === username;
this.isAdmin = props.currentRepoInfo.is_admin;
this.repoEncrypted = props.currentRepoInfo.encrypted;
this.clickedDirent = null;
this.direntItems = [];
this.currentItemRef = null;
this.zipToken = null;
const { userPerm } = props;
this.canDrop = userPerm === 'rw';
const { isCustomPermission, customPermission } = Utils.getUserPermission(userPerm);
if (isCustomPermission) {
const { modify } = customPermission.permission;
this.canDrop = modify;
}
}
freezeItem = () => {
this.setState({isItemFreezed: true});
}
unfreezeItem = () => {
this.setState({isItemFreezed: false});
}
onItemRename = (dirent, newName) => {
let isDuplicated = this.props.direntList.some(item => {
return item.name === newName;
});
if (isDuplicated) {
let errMessage = gettext('The name "{name}" is already taken. Please choose a different name.');
errMessage = errMessage.replace('{name}', Utils.HTMLescape(newName));
toaster.danger(errMessage);
return false;
}
this.props.onItemRename(dirent, newName);
}
onItemRenameToggle = () => {
this.freezeItem();
}
onItemSelected = (dirent) => {
this.setState({activeDirent: null});
this.props.onItemSelected(dirent);
}
onDirentClick = (dirent) => {
if (this.props.selectedDirentList.length > 0 && !this.state.activeDirent ) {
return;
}
this.setState({activeDirent: dirent});
this.props.onDirentClick(dirent);
}
sortByName = (e) => {
e.preventDefault();
const sortBy = 'name';
const sortOrder = this.props.sortOrder == 'asc' ? 'desc' : 'asc';
this.props.sortItems(sortBy, sortOrder);
}
sortByTime = (e) => {
e.preventDefault();
const sortBy = 'time';
const sortOrder = this.props.sortOrder == 'asc' ? 'desc' : 'asc';
this.props.sortItems(sortBy, sortOrder);
}
sortBySize = (e) => {
e.preventDefault();
const sortBy = 'size';
const sortOrder = this.props.sortOrder == 'asc' ? 'desc' : 'asc';
this.props.sortItems(sortBy, sortOrder);
}
// for image popup
prepareImageItem = (item) => {
const useThumbnail = !this.repoEncrypted;
const name = item.name;
const fileExt = name.substr(name.lastIndexOf('.') + 1).toLowerCase();
const isGIF = fileExt == 'gif';
const path = Utils.encodePath(Utils.joinPath(this.props.path, name));
const repoID = this.props.repoID;
let src;
if (useThumbnail && !isGIF) {
src = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`;
} else {
src = `${siteRoot}repo/${repoID}/raw${path}`;
}
return {
'name': name,
'url': `${siteRoot}lib/${repoID}/file${path}`,
'src': src
};
}
showImagePopup = (curItem) => {
let items = this.props.fullDirentList.filter((item) => {
return Utils.imageCheck(item.name);
});
const imageItems = items.map((item) => {
return this.prepareImageItem(item);
});
this.setState({
isImagePopupOpen: true,
imageItems: imageItems,
imageIndex: items.indexOf(curItem)
});
}
moveToPrevImage = () => {
const imageItemsLength = this.state.imageItems.length;
this.setState((prevState) => ({
imageIndex: (prevState.imageIndex + imageItemsLength - 1) % imageItemsLength
}));
}
moveToNextImage = () => {
const imageItemsLength = this.state.imageItems.length;
this.setState((prevState) => ({
imageIndex: (prevState.imageIndex + 1) % imageItemsLength
}));
}
closeImagePopup = () => {
this.setState({isImagePopupOpen: false});
}
onCreateFileToggle = () => {
this.setState({isCreateFileDialogShow: !this.state.isCreateFileDialogShow});
}
onCreateFolderToggle = () => {
this.setState({isCreateFolderDialogShow: !this.state.isCreateFolderDialogShow});
}
onAddFile = (filePath, isDraft) => {
this.setState({isCreateFileDialogShow: false});
this.props.onAddFile(filePath, isDraft);
}
onAddFolder = (dirPath) => {
this.setState({isCreateFolderDialogShow: false});
this.props.onAddFolder(dirPath);
}
checkDuplicatedName = (newName) => {
let direntList = this.props.direntList;
let isDuplicated = direntList.some(object => {
return object.name === newName;
});
return isDuplicated;
}
onMoveToggle = () => {
this.setState({isMoveDialogShow: !this.state.isMoveDialogShow});
}
onCopyToggle = () => {
this.setState({isCopyDialogShow: !this.state.isCopyDialogShow});
}
onItemsDownload = () => {
let { path, repoID, selectedDirentList } = this.props;
if (selectedDirentList.length) {
if (selectedDirentList.length === 1 && !selectedDirentList[0].isDir()) {
let direntPath = Utils.joinPath(path, selectedDirentList[0].name);
let url = URLDecorator.getUrl({type: 'download_file_url', repoID: repoID, filePath: direntPath});
location.href= url;
return;
}
let selectedDirentNames = selectedDirentList.map(dirent => {
return dirent.name;
});
this.setState({
isProgressDialogShow: true,
downloadItems: selectedDirentNames
});
}
}
onCloseZipDownloadDialog = () => {
this.setState({isProgressDialogShow: false});
}
// common contextmenu handle
onMouseDown = (event) => {
event.stopPropagation();
if (event.button === 2) {
return;
}
}
handleContextClick = (event, id, menuList, currentObject = null) => {
event.preventDefault();
event.stopPropagation();
let x = event.clientX || (event.touches && event.touches[0].pageX);
let y = event.clientY || (event.touches && event.touches[0].pageY);
if (this.props.posX) {
x -= this.props.posX;
}
if (this.props.posY) {
y -= this.props.posY;
}
hideMenu();
let showMenuConfig = {
id: id,
position: { x, y },
target: event.target,
currentObject: currentObject,
menuList: menuList,
};
if (menuList.length === 0) {
return;
}
showMenu(showMenuConfig);
}
// table-container contextmenu handle
onContainerClick = () => {
if (this.state.activeDirent) {
this.onDirentClick(null);
}
}
onContainerMouseDown = (event) => {
this.onMouseDown(event);
}
onContainerContextMenu = (event) => {
event.preventDefault();
// Display menu items based on the permissions of the current path
let permission = this.props.userPerm;
const { isCustomPermission, customPermission } = Utils.getUserPermission(this.props.userPerm);
if (permission !== 'admin' && permission !== 'rw' && !isCustomPermission) {
return;
}
if (this.props.selectedDirentList.length === 0) {
let id = 'dirent-container-menu';
// custom permission judgement
if (isCustomPermission) {
const { modify } = customPermission.permission;
if (!modify) return;
}
let menuList = [TextTranslation.NEW_FOLDER, TextTranslation.NEW_FILE];
this.handleContextClick(event, id, menuList);
} else {
if (this.props.selectedDirentList.length === 1) {
if (!this.state.activeDirent) {
let id = 'dirent-item-menu';
let dirent = this.props.selectedDirentList[0];
let menuList = this.getDirentItemMenuList(dirent, true);
this.handleContextClick(event, id, menuList, dirent);
} else {
this.onDirentClick(null);
event.preventDefault();
event.persist();
// custom permission judgement
if (isCustomPermission) {
const { modify } = customPermission.permission;
if (!modify) return;
}
setTimeout(() => {
let id = 'dirent-container-menu';
let menuList = [TextTranslation.NEW_FOLDER, TextTranslation.NEW_FILE];
this.handleContextClick(event, id, menuList);
}, 0);
}
} else {
let id = 'dirents-menu';
let menuList = [];
if (isCustomPermission) {
const { modify: canModify, copy: canCopy, download: canDownload, delete: canDelete } = customPermission.permission;
canModify && menuList.push(TextTranslation.MOVE);
canCopy && menuList.push(TextTranslation.COPY);
canDownload && menuList.push(TextTranslation.DOWNLOAD);
canDelete && menuList.push(TextTranslation.DELETE);
} else {
menuList = [TextTranslation.MOVE, TextTranslation.COPY, TextTranslation.DOWNLOAD, TextTranslation.DELETE];
}
this.handleContextClick(event, id, menuList);
}
}
}
onContainerMenuItemClick = (operation) => {
switch(operation) {
case 'New Folder':
this.onCreateFolderToggle();
break;
case 'New File':
this.onCreateFileToggle();
break;
default:
break;
}
hideMenu();
}
onDirentsMenuItemClick = (operation) => {
switch(operation) {
case 'Move':
this.onMoveToggle();
break;
case 'Copy':
this.onCopyToggle();
break;
case 'Download':
this.onItemsDownload();
break;
case 'Delete':
this.props.onItemsDelete();
break;
default:
break;
}
hideMenu();
}
// table-thread contextmenu handle -- Shield event
onThreadMouseDown = (event) => {
this.onMouseDown(event);
}
onThreadContextMenu = (event) => {
event.stopPropagation();
}
// table-dirent-item contextmenu handle
onItemMouseDown = (event) => {
this.onMouseDown(event);
}
onItemContextMenu = (event, dirent) => {
// Display menu items according to the current dirent permission
if (this.props.selectedDirentList.length > 1) {
return;
}
this.onDirentClick(dirent);
let id = 'dirent-item-menu';
let menuList = this.getDirentItemMenuList(dirent, true);
this.handleContextClick(event, id, menuList, dirent);
}
setDirentItemRef = (index) => item => {
this.direntItems[index] = item;
}
onMenuItemClick = (operation, currentObject, event) => {
let index = this.getDirentIndex(currentObject);
this.direntItems[index].onMenuItemClick(operation, event);
hideMenu();
}
onShowMenu = (e) => {
this.freezeItem();
}
onHideMenu = (e) => {
this.unfreezeItem();
}
// contextmenu utils
getDirentIndex = (dirent) => {
let direntList = this.props.direntList;
let index = 0;
for (let i = 0; i < direntList.length; i++) {
if (direntList[i].name === dirent.name) {
index = i;
break;
}
}
return index;
}
getDirentItemMenuList = (dirent, isContextmenu) => {
const isRepoOwner = this.isRepoOwner;
const currentRepoInfo = this.props.currentRepoInfo;
return Utils.getDirentOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu);
}
onTableDragEnter = (e) => {
if (Utils.isIEBrower() || !this.canDrop) {
return false;
}
this.enteredCounter++;
if (this.enteredCounter !== 0) {
if (this.state.isListDropTipShow) {
return ;
}
this.setState({isListDropTipShow: true});
}
}
onTableDragOver = (e) => {
if (Utils.isIEBrower() || !this.canDrop) {
return false;
}
if (e.dataTransfer.dropEffect === 'copy') {
return;
}
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
onTableDragLeave = (e) => {
if (Utils.isIEBrower() || !this.canDrop) {
return false;
}
this.enteredCounter--;
if (this.enteredCounter === 0) {
this.setState({isListDropTipShow: false});
}
}
tableDrop = (e) => {
if (Utils.isIEBrower() || !this.canDrop) {
return false;
}
e.persist();
this.enteredCounter = 0;
this.setState({isListDropTipShow: false});
if (e.dataTransfer.files.length) { // uploaded files
return;
}
let dragStartItemData = e.dataTransfer.getData('applicaiton/drag-item-info');
dragStartItemData = JSON.parse(dragStartItemData);
let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartItemData;
if (Array.isArray(dragStartItemData)) { //selected items
return;
}
if (nodeRootPath === this.props.path || nodeParentPath === this.props.path) {
return;
}
if (this.props.path.indexOf(nodeRootPath) !== -1) {
return;
}
this.props.onItemMove(this.props.currentRepoInfo, nodeDirent, this.props.path, nodeParentPath);
}
onShowDirentsDraggablePreview = () => {
this.setState({
isShowDirentsDraggablePreview: true,
});
}
onHideDirentsDraggablePreview = () => {
this.setState({
isShowDirentsDraggablePreview: false
});
}
render() {
const { direntList, sortBy, sortOrder } = this.props;
if (this.props.isDirentListLoading) {
return (<Loading />);
}
// sort
const sortByName = sortBy == 'name';
const sortByTime = sortBy == 'time';
const sortBySize = sortBy == 'size';
const sortIcon = sortOrder == 'asc' ? <span className="fas fa-caret-up"></span> : <span className="fas fa-caret-down"></span>;
const isDesktop = Utils.isDesktop();
return (
<div
className={`table-container ${(this.state.isListDropTipShow && this.canDrop) ? 'table-drop-active' : ''}`}
onMouseDown={this.onContainerMouseDown}
onContextMenu={this.onContainerContextMenu}
onClick={this.onContainerClick}
onDragEnter={this.onTableDragEnter}
onDragOver={this.onTableDragOver}
onDragLeave={this.onTableDragLeave}
onDrop={this.tableDrop}
>
<table className={`table-hover ${isDesktop ? '': 'table-thead-hidden'}`}>
{isDesktop ? (
<thead onMouseDown={this.onThreadMouseDown} onContextMenu={this.onThreadContextMenu}>
<tr>
<th width="3%" className="pl10">
<input type="checkbox" className="vam" onChange={this.props.onAllItemSelected} checked={this.props.isAllItemSelected} />
</th>
<th width="3%" className="pl10">{/*icon */}</th>
<th width="5%" className="pl10">{/*star */}</th>
<th width="39%"><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a></th>
<th width="6%">{/*tag */}</th>
<th width="18%">{/*operation */}</th>
<th width="11%"><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {sortBySize && sortIcon}</a></th>
<th width="15%"><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {sortByTime && sortIcon}</a></th>
</tr>
</thead>
) : (
<thead>
<tr>
<th width="12%"></th>
<th width="80%"></th>
<th width="8%"></th>
</tr>
</thead>
)}
<tbody>
{direntList.map((dirent, index) => {
return (
<DirentListItem
ref={this.setDirentItemRef(index)}
key={index}
dirent={dirent}
path={this.props.path}
repoID={this.props.repoID}
currentRepoInfo={this.props.currentRepoInfo}
isAdmin={this.isAdmin}
isRepoOwner={this.isRepoOwner}
repoEncrypted={this.repoEncrypted}
enableDirPrivateShare={this.props.enableDirPrivateShare}
isGroupOwnedRepo={this.props.isGroupOwnedRepo}
onItemClick={this.props.onItemClick}
onItemRenameToggle={this.onItemRenameToggle}
onItemSelected={this.onItemSelected}
onItemDelete={this.props.onItemDelete}
onItemRename={this.onItemRename}
onItemMove={this.props.onItemMove}
onItemCopy={this.props.onItemCopy}
updateDirent={this.props.updateDirent}
isItemFreezed={this.state.isItemFreezed}
freezeItem={this.freezeItem}
unfreezeItem={this.unfreezeItem}
onDirentClick={this.onDirentClick}
showImagePopup={this.showImagePopup}
onItemMouseDown={this.onItemMouseDown}
onItemContextMenu={this.onItemContextMenu}
selectedDirentList={this.props.selectedDirentList}
activeDirent={this.state.activeDirent}
onFileTagChanged={this.props.onFileTagChanged}
getDirentItemMenuList={this.getDirentItemMenuList}
showDirentDetail={this.props.showDirentDetail}
onItemsMove={this.props.onItemsMove}
onShowDirentsDraggablePreview={this.onShowDirentsDraggablePreview}
loadDirentList={this.props.loadDirentList}
/>
);
})}
</tbody>
</table>
<Fragment>
<ContextMenu
id={'dirent-container-menu'}
onMenuItemClick={this.onContainerMenuItemClick}
/>
<ContextMenu
id={'dirent-item-menu'}
onMenuItemClick={this.onMenuItemClick}
onShowMenu={this.onShowMenu}
onHideMenu={this.onHideMenu}
/>
<ContextMenu
id={'dirents-menu'}
onMenuItemClick={this.onDirentsMenuItemClick}
/>
{this.state.isShowDirentsDraggablePreview &&
<ModalPortal>
<DirentsDraggedPreview
selectedDirentList={this.props.selectedDirentList}
onHideDirentsDraggablePreview={this.onHideDirentsDraggablePreview}
dragStartPosition={this.state.dragStartPosition}
/>
</ModalPortal>
}
{this.state.isImagePopupOpen && (
<ModalPortal>
<ImageDialog
imageItems={this.state.imageItems}
imageIndex={this.state.imageIndex}
closeImagePopup={this.closeImagePopup}
moveToPrevImage={this.moveToPrevImage}
moveToNextImage={this.moveToNextImage}
/>
</ModalPortal>
)}
{this.state.isCreateFolderDialogShow && (
<ModalPortal>
<CreateFolder
parentPath={this.props.path}
onAddFolder={this.onAddFolder}
checkDuplicatedName={this.checkDuplicatedName}
addFolderCancel={this.onCreateFolderToggle}
/>
</ModalPortal>
)}
{this.state.isCreateFileDialogShow && (
<ModalPortal>
<CreateFile
parentPath={this.props.path}
fileType={this.state.fileType}
onAddFile={this.onAddFile}
checkDuplicatedName={this.checkDuplicatedName}
addFileCancel={this.onCreateFileToggle}
/>
</ModalPortal>
)}
{this.state.isMoveDialogShow &&
<MoveDirentDialog
path={this.props.path}
repoID={this.props.repoID}
repoEncrypted={this.props.currentRepoInfo.encrypted}
isMutipleOperation={this.state.isMutipleOperation}
selectedDirentList={this.props.selectedDirentList}
onItemsMove={this.props.onItemsMove}
onCancelMove={this.onMoveToggle}
/>
}
{this.state.isCopyDialogShow &&
<CopyDirentDialog
path={this.props.path}
repoID={this.props.repoID}
repoEncrypted={this.props.currentRepoInfo.encrypted}
selectedDirentList={this.props.selectedDirentList}
isMutipleOperation={this.state.isMutipleOperation}
onItemsCopy={this.props.onItemsCopy}
onCancelCopy={this.onCopyToggle}
/>
}
{this.state.isProgressDialogShow &&
<ZipDownloadDialog
repoID={this.props.repoID}
path={this.props.path}
target={this.state.downloadItems}
toggleDialog={this.onCloseZipDownloadDialog}
/>
}
</Fragment>
</div>
);
}
}
DirentListView.propTypes = propTypes;
export default DirentListView;