mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-12 13:24:52 +00:00
Feature/tags view file operations (#7464)
* add basic file operations * add tag view toolbar operations * add share and rename * optimize * optimize * update codes --------- Co-authored-by: zhouwenxuan <aries@Mac.local> Co-authored-by: renjie-run <rj.aiyayao@gmail.com>
This commit is contained in:
@@ -84,6 +84,7 @@ const propTypes = {
|
||||
eventBus: PropTypes.object,
|
||||
updateCurrentDirent: PropTypes.func.isRequired,
|
||||
updateCurrentPath: PropTypes.func,
|
||||
toggleShowDirentToolbar: PropTypes.func,
|
||||
};
|
||||
|
||||
class DirColumnView extends React.Component {
|
||||
@@ -225,6 +226,11 @@ class DirColumnView extends React.Component {
|
||||
renameFileCallback={this.props.renameFileCallback}
|
||||
updateCurrentDirent={this.props.updateCurrentDirent}
|
||||
updateCurrentPath={this.props.updateCurrentPath}
|
||||
moveFileCallback={this.props.moveFileCallback}
|
||||
copyFileCallback={this.props.copyFileCallback}
|
||||
convertFileCallback={this.props.convertFileCallback}
|
||||
addFolderCallback={this.props.onAddFolder}
|
||||
toggleShowDirentToolbar={this.props.toggleShowDirentToolbar}
|
||||
/>
|
||||
)}
|
||||
{currentMode === LIST_MODE && (
|
||||
|
143
frontend/src/components/toolbar/tag-files-toolbar.js
Normal file
143
frontend/src/components/toolbar/tag-files-toolbar.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { EVENT_BUS_TYPE } from '../../metadata/constants';
|
||||
import TextTranslation from '../../utils/text-translation';
|
||||
import { getFileById, getFileName, getTagFileOperationList } from '../../tag/utils/file';
|
||||
|
||||
const TagFilesToolbar = ({ currentRepoInfo }) => {
|
||||
const [selectedFileIds, setSelectedFileIds] = useState([]);
|
||||
const tagFilesRef = useRef([]);
|
||||
|
||||
const canModify = window.sfTagsDataContext && window.sfTagsDataContext.canModify();
|
||||
const eventBus = window.sfTagsDataContext && window.sfTagsDataContext.eventBus;
|
||||
|
||||
const selectedFilesLen = useMemo(() => {
|
||||
return selectedFileIds.length;
|
||||
}, [selectedFileIds]);
|
||||
|
||||
const unSelect = useCallback(() => {
|
||||
setSelectedFileIds([]);
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UNSELECT_TAG_FILES);
|
||||
}, [eventBus]);
|
||||
|
||||
const moveTagFile = useCallback(() => {
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.MOVE_TAG_FILE);
|
||||
}, [eventBus]);
|
||||
|
||||
const copyTagFile = useCallback(() => {
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.COPY_TAG_FILE);
|
||||
}, [eventBus]);
|
||||
|
||||
const deleteTagFiles = useCallback(() => {
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DELETE_TAG_FILES);
|
||||
}, [eventBus]);
|
||||
|
||||
const downloadTagFiles = useCallback(() => {
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DOWNLOAD_TAG_FILES);
|
||||
}, [eventBus]);
|
||||
|
||||
const getMenuList = useCallback(() => {
|
||||
if (selectedFilesLen > 1) return [];
|
||||
const fileId = selectedFileIds[0];
|
||||
const file = getFileById(tagFilesRef.current, fileId);
|
||||
const fileName = getFileName(file);
|
||||
const allOperations = getTagFileOperationList(fileName, currentRepoInfo, canModify);
|
||||
const excludesOperations = ['Move', 'Copy', 'Delete', 'Download'];
|
||||
const validOperations = allOperations.filter((item) => {
|
||||
return excludesOperations.indexOf(item.key) == -1;
|
||||
});
|
||||
return validOperations;
|
||||
}, [canModify, currentRepoInfo, selectedFileIds, selectedFilesLen]);
|
||||
|
||||
const onMenuItemClick = useCallback((operation) => {
|
||||
switch (operation) {
|
||||
case TextTranslation.SHARE.key:
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.SHARE_TAG_FILE);
|
||||
break;
|
||||
case TextTranslation.RENAME.key:
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_RENAME_DIALOG);
|
||||
break;
|
||||
case TextTranslation.HISTORY.key:
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.FILE_HISTORY);
|
||||
break;
|
||||
case TextTranslation.ACCESS_LOG.key:
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.FILE_ACCESS_LOG);
|
||||
break;
|
||||
case TextTranslation.OPEN_VIA_CLIENT.key:
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.OPEN_VIA_CLIENT);
|
||||
break;
|
||||
case TextTranslation.CONVERT_TO_SDOC.key:
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.CONVERT_FILE, 'sdoc');
|
||||
break;
|
||||
case TextTranslation.CONVERT_TO_MARKDOWN.key: {
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.CONVERT_FILE, 'markdown');
|
||||
break;
|
||||
}
|
||||
case TextTranslation.CONVERT_TO_DOCX.key: {
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.CONVERT_FILE, 'docx');
|
||||
break;
|
||||
}
|
||||
case TextTranslation.EXPORT_DOCX.key: {
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.EXPORT_DOCX);
|
||||
break;
|
||||
}
|
||||
case TextTranslation.EXPORT_SDOC.key: {
|
||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.EXPORT_SDOC);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [eventBus]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeSelectedFileIds = eventBus && eventBus.subscribe(EVENT_BUS_TYPE.SELECT_TAG_FILES, (ids, tagFiles) => {
|
||||
tagFilesRef.current = tagFiles || [];
|
||||
setSelectedFileIds(ids);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeSelectedFileIds && unsubscribeSelectedFileIds();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="selected-dirents-toolbar">
|
||||
<span className="cur-view-path-btn px-2" onClick={unSelect}>
|
||||
<span className="sf3-font-x-01 sf3-font mr-2" aria-label={gettext('Unselect')} title={gettext('Unselect')}></span>
|
||||
<span>{selectedFilesLen}{' '}{gettext('selected')}</span>
|
||||
</span>
|
||||
{(selectedFilesLen === 1 && canModify) &&
|
||||
<>
|
||||
<span className="cur-view-path-btn" onClick={moveTagFile}>
|
||||
<span className="sf3-font-move1 sf3-font" aria-label={gettext('Move')} title={gettext('Move')}></span>
|
||||
</span>
|
||||
<span className="cur-view-path-btn" onClick={copyTagFile}>
|
||||
<span className="sf3-font-copy1 sf3-font" aria-label={gettext('Copy')} title={gettext('Copy')}></span>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
{canModify &&
|
||||
<>
|
||||
<span className="cur-view-path-btn" onClick={deleteTagFiles}>
|
||||
<span className="sf3-font-delete1 sf3-font" aria-label={gettext('Delete')} title={gettext('Delete')}></span>
|
||||
</span>
|
||||
<span className="cur-view-path-btn" onClick={downloadTagFiles}>
|
||||
<span className="sf3-font-download1 sf3-font" aria-label={gettext('Download')} title={gettext('Download')}></span>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
{selectedFilesLen === 1 &&
|
||||
<ItemDropdownMenu
|
||||
item={{}}
|
||||
toggleClass={'cur-view-path-btn sf3-font-more-vertical sf3-font'}
|
||||
onMenuItemClick={onMenuItemClick}
|
||||
getMenuList={getMenuList}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagFilesToolbar;
|
@@ -39,10 +39,11 @@ const FileNameEditor = React.forwardRef((props, ref) => {
|
||||
const fileType = getFileType();
|
||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
|
||||
const canDelete = window.sfMetadataContext.checkCanDeleteRow();
|
||||
|
||||
if (fileType === 'image') {
|
||||
return (
|
||||
<ImagePreviewer {...props} repoID={repoID} repoInfo={repoInfo} closeImagePopup={props.onCommitCancel} />
|
||||
<ImagePreviewer {...props} repoID={repoID} repoInfo={repoInfo} closeImagePopup={props.onCommitCancel} canDelete={canDelete} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import { Utils } from '../../../utils/utils';
|
||||
import { siteRoot, thumbnailSizeForOriginal, fileServerRoot, thumbnailDefaultSize } from '../../../utils/constants';
|
||||
import { getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell';
|
||||
|
||||
const ImagePreviewer = ({ record, table, repoID, repoInfo, closeImagePopup, deleteRecords }) => {
|
||||
const ImagePreviewer = ({ record, table, repoID, repoInfo, closeImagePopup, deleteRecords, canDelete }) => {
|
||||
const [imageIndex, setImageIndex] = useState(0);
|
||||
const [imageItems, setImageItems] = useState([]);
|
||||
|
||||
@@ -95,8 +95,6 @@ const ImagePreviewer = ({ record, table, repoID, repoInfo, closeImagePopup, dele
|
||||
}
|
||||
};
|
||||
|
||||
const canDelete = window.sfMetadataContext.checkCanDeleteRow();
|
||||
|
||||
return (
|
||||
<ModalPortal>
|
||||
<ImageDialog
|
||||
@@ -119,6 +117,7 @@ ImagePreviewer.propTypes = {
|
||||
repoInfo: PropTypes.object,
|
||||
closeImagePopup: PropTypes.func,
|
||||
deleteRecords: PropTypes.func,
|
||||
canDelete: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ImagePreviewer;
|
||||
|
@@ -78,4 +78,24 @@ export const EVENT_BUS_TYPE = {
|
||||
// map
|
||||
MODIFY_MAP_TYPE: 'modify_map_type',
|
||||
MAP_VIEW: 'map_view',
|
||||
|
||||
// tag file
|
||||
MOVE_TAG_FILE: 'move_tag_file',
|
||||
COPY_TAG_FILE: 'copy_tag_file',
|
||||
RENAME_TAG_FILE: 'rename_tag_file',
|
||||
TOGGLE_RENAME_DIALOG: 'toggle_rename_dialog',
|
||||
SHARE_TAG_FILE: 'share_tag_file',
|
||||
TOGGLE_ZIP_DIALOG: 'toggle_zip_dialog',
|
||||
DOWNLOAD_TAG_FILES: 'download_tag_files',
|
||||
DELETE_TAG_FILES: 'delete_tag_files',
|
||||
SELECT_TAG_FILES: 'select_tag_files',
|
||||
UNSELECT_TAG_FILES: 'unselect_tag_files',
|
||||
|
||||
// file
|
||||
FILE_HISTORY: 'file_history',
|
||||
FILE_ACCESS_LOG: 'file_access_log',
|
||||
OPEN_VIA_CLIENT: 'open_via_client',
|
||||
CONVERT_FILE: 'convert_file',
|
||||
EXPORT_DOCX: 'export_docx',
|
||||
EXPORT_SDOC: 'export_sdoc',
|
||||
};
|
||||
|
@@ -30,6 +30,7 @@ import DirTool from '../../components/cur-dir-path/dir-tool';
|
||||
import Detail from '../../components/dirent-detail';
|
||||
import DirColumnView from '../../components/dir-view-mode/dir-column-view';
|
||||
import SelectedDirentsToolbar from '../../components/toolbar/selected-dirents-toolbar';
|
||||
import TagFilesToolbar from '../../components/toolbar/tag-files-toolbar';
|
||||
|
||||
import '../../css/lib-content-view.css';
|
||||
|
||||
@@ -1337,7 +1338,7 @@ class LibContentView extends React.Component {
|
||||
this.updateMoveCopyTreeNode(copyToDirentPath);
|
||||
}
|
||||
|
||||
if (copyToDirentPath === nodeParentPath && this.state.currentMode !== METADATA_MODE) {
|
||||
if (copyToDirentPath === nodeParentPath && this.state.currentMode !== METADATA_MODE && this.state.currentMode !== TAGS_MODE) {
|
||||
this.loadDirentList(this.state.path);
|
||||
}
|
||||
|
||||
@@ -1382,31 +1383,36 @@ class LibContentView extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
convertFileAjaxCallback = ({ newName, parentDir, size, path, error }) => {
|
||||
if (error) {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
if (errMessage === gettext('Error')) {
|
||||
const name = Utils.getFileName(path);
|
||||
errMessage = gettext('Failed to convert {name}.').replace('{name}', name);
|
||||
}
|
||||
toaster.danger(errMessage, { 'id': 'conversion' });
|
||||
return;
|
||||
}
|
||||
const new_path = parentDir + '/' + newName;
|
||||
const parentPath = Utils.getDirName(new_path);
|
||||
if (this.state.isTreePanelShown) {
|
||||
this.addNodeToTree(newName, parentPath, 'file');
|
||||
}
|
||||
this.addDirent(newName, 'file', size);
|
||||
const message = gettext('Successfully converted the file.');
|
||||
toaster.success(message, { 'id': 'conversion' });
|
||||
};
|
||||
|
||||
onConvertItem = (dirent, dstType) => {
|
||||
let path = Utils.joinPath(this.state.path, dirent.name);
|
||||
let repoID = this.props.repoID;
|
||||
toaster.notifyInProgress(gettext('Converting, please wait...'), { 'id': 'conversion' });
|
||||
seafileAPI.convertFile(repoID, path, dstType).then((res) => {
|
||||
let newFileName = res.data.obj_name;
|
||||
let parentDir = res.data.parent_dir;
|
||||
let new_path = parentDir + '/' + newFileName;
|
||||
let parentPath = Utils.getDirName(new_path);
|
||||
|
||||
if (this.state.isTreePanelShown) {
|
||||
this.addNodeToTree(newFileName, parentPath, 'file');
|
||||
}
|
||||
|
||||
this.addDirent(newFileName, 'file', res.data.size);
|
||||
let message = gettext('Successfully converted the file.');
|
||||
toaster.success(message, { 'id': 'conversion' });
|
||||
|
||||
const newFileName = res.data.obj_name;
|
||||
const parentDir = res.data.parent_dir;
|
||||
this.convertFileAjaxCallback({ newName: newFileName, parentDir, size: res.data.size });
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
if (errMessage === gettext('Error')) {
|
||||
let name = Utils.getFileName(path);
|
||||
errMessage = gettext('Failed to convert {name}.').replace('{name}', name);
|
||||
}
|
||||
toaster.danger(errMessage, { 'id': 'conversion' });
|
||||
this.convertFileAjaxCallback({ path, error });
|
||||
});
|
||||
|
||||
};
|
||||
@@ -2164,6 +2170,10 @@ class LibContentView extends React.Component {
|
||||
this.setState({ path });
|
||||
};
|
||||
|
||||
toggleShowDirentToolbar = (isDirentSelected) => {
|
||||
this.setState({ isDirentSelected });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repoID } = this.props;
|
||||
let { currentRepoInfo, userPerm, isCopyMoveProgressDialogShow, isDeleteFolderDialogOpen, errorMsg,
|
||||
@@ -2251,7 +2261,10 @@ class LibContentView extends React.Component {
|
||||
'w-100': !isDesktop,
|
||||
'animation-children': isDirentSelected
|
||||
})}>
|
||||
{isDirentSelected ?
|
||||
{isDirentSelected ? (
|
||||
this.state.currentMode === TAGS_MODE ?
|
||||
<TagFilesToolbar currentRepoInfo={this.state.currentRepoInfo} />
|
||||
:
|
||||
<SelectedDirentsToolbar
|
||||
repoID={this.props.repoID}
|
||||
path={this.state.path}
|
||||
@@ -2277,7 +2290,7 @@ class LibContentView extends React.Component {
|
||||
onItemConvert={this.onConvertItem}
|
||||
onAddFolder={this.onAddFolder}
|
||||
/>
|
||||
:
|
||||
) : (
|
||||
<CurDirPath
|
||||
currentRepoInfo={this.state.currentRepoInfo}
|
||||
repoID={this.props.repoID}
|
||||
@@ -2310,7 +2323,7 @@ class LibContentView extends React.Component {
|
||||
loadDirentList={this.loadDirentList}
|
||||
onAddFolderNode={this.onAddFolder}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{isDesktop &&
|
||||
<div className="cur-view-path-right py-1">
|
||||
@@ -2389,6 +2402,7 @@ class LibContentView extends React.Component {
|
||||
moveFileCallback={this.moveItemsAjaxCallback}
|
||||
onItemCopy={this.onCopyItem}
|
||||
copyFileCallback={this.copyItemsAjaxCallback}
|
||||
convertFileCallback={this.convertFileAjaxCallback}
|
||||
onItemConvert={this.onConvertItem}
|
||||
onDirentClick={this.onDirentClick}
|
||||
updateDirent={this.updateDirent}
|
||||
@@ -2405,6 +2419,7 @@ class LibContentView extends React.Component {
|
||||
eventBus={this.props.eventBus}
|
||||
updateCurrentDirent={this.updateCurrentDirent}
|
||||
updateCurrentPath={this.updatePath}
|
||||
toggleShowDirentToolbar={this.toggleShowDirentToolbar}
|
||||
/>
|
||||
:
|
||||
<div className="message err-tip">{gettext('Folder does not exist.')}</div>
|
||||
|
8
frontend/src/tag/constants/file.js
Normal file
8
frontend/src/tag/constants/file.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export const TAG_FILE_KEY = {
|
||||
ID: '_id',
|
||||
NAME: '_name',
|
||||
SIZE: '_size',
|
||||
PARENT_DIR: '_parent_dir',
|
||||
FILE_MTIME: '_file_mtime',
|
||||
TAGS: '_tags',
|
||||
};
|
@@ -1,19 +1,34 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import toaster from '../../components/toast';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { metadataAPI } from '../../metadata';
|
||||
import tagsAPI from '../api';
|
||||
import { useTags } from './tags';
|
||||
import { getTreeNodeById, getTreeNodeByKey } from '../../components/sf-table/utils/tree';
|
||||
import { getAllChildTagsIdsFromNode } from '../utils/tree';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { TAG_FILE_KEY } from '../constants/file';
|
||||
import { EVENT_BUS_TYPE } from '../../metadata/constants';
|
||||
import { getFileById } from '../utils/file';
|
||||
import { getRowById } from '../../metadata/utils/table';
|
||||
import { getTagFilesLinks } from '../utils/cell';
|
||||
import { PRIVATE_COLUMN_KEY } from '../constants';
|
||||
import URLDecorator from '../../utils/url-decorator';
|
||||
import { fileServerRoot, useGoFileserver } from '../../utils/constants';
|
||||
|
||||
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
|
||||
const TagViewContext = React.createContext(null);
|
||||
|
||||
export const TagViewProvider = ({ repoID, tagID, nodeKey, children, ...params }) => {
|
||||
export const TagViewProvider = ({
|
||||
repoID, tagID, nodeKey, children, moveFileCallback, copyFileCallback, addFolderCallback, deleteFilesCallback, renameFileCallback, convertFileCallback,
|
||||
toggleShowDirentToolbar, ...params
|
||||
}) => {
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [tagFiles, setTagFiles] = useState(null);
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const [selectedFileIds, setSelectedFileIds] = useState([]);
|
||||
|
||||
const { tagsData } = useTags();
|
||||
const { tagsData, updateLocalTags } = useTags();
|
||||
|
||||
const getChildTagsIds = useCallback((tagID, nodeKey) => {
|
||||
let displayNode = null;
|
||||
@@ -26,6 +41,121 @@ export const TagViewProvider = ({ repoID, tagID, nodeKey, children, ...params })
|
||||
return getAllChildTagsIdsFromNode(displayNode);
|
||||
}, [tagsData]);
|
||||
|
||||
const updateSelectedFileIds = useCallback((ids) => {
|
||||
toggleShowDirentToolbar(ids.length > 0);
|
||||
setSelectedFileIds(ids);
|
||||
setTimeout(() => {
|
||||
window.sfTagsDataContext && window.sfTagsDataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_TAG_FILES, ids, tagFiles);
|
||||
}, 0);
|
||||
}, [setSelectedFileIds, tagFiles, toggleShowDirentToolbar]);
|
||||
|
||||
const moveTagFile = useCallback((targetRepo, dirent, targetParentPath, sourceParentPath, isByDialog) => {
|
||||
seafileAPI.moveDir(repoID, targetRepo.repo_id, targetParentPath, sourceParentPath, dirent.name).then(res => {
|
||||
moveFileCallback && moveFileCallback(repoID, targetRepo, dirent, targetParentPath, sourceParentPath, res.data.task_id || null, isByDialog);
|
||||
updateSelectedFileIds([]);
|
||||
});
|
||||
}, [repoID, moveFileCallback, updateSelectedFileIds]);
|
||||
|
||||
const copyTagFile = useCallback((targetRepo, dirent, targetParentPath, sourceParentPath, isByDialog) => {
|
||||
seafileAPI.copyDir(repoID, targetRepo.repo_id, targetParentPath, sourceParentPath, dirent.name).then(res => {
|
||||
copyFileCallback && copyFileCallback(repoID, targetRepo, dirent, targetParentPath, sourceParentPath, res.data.task_id || null, isByDialog);
|
||||
updateSelectedFileIds([]);
|
||||
});
|
||||
}, [repoID, copyFileCallback, updateSelectedFileIds]);
|
||||
|
||||
const deleteTagFiles = useCallback(() => {
|
||||
const files = selectedFileIds.map(id => getFileById(tagFiles, id));
|
||||
const paths = files.map(f => Utils.joinPath(f[TAG_FILE_KEY.PARENT_DIR], f[TAG_FILE_KEY.NAME]));
|
||||
const fileNames = files.map(f => f[TAG_FILE_KEY.NAME]);
|
||||
metadataAPI.batchDeleteFiles(repoID, paths).then(() => {
|
||||
const updatedTags = new Set();
|
||||
files.forEach(file => {
|
||||
file._tags.forEach(tag => updatedTags.add(tag.row_id));
|
||||
});
|
||||
|
||||
let idTagUpdates = {};
|
||||
updatedTags.forEach(tagID => {
|
||||
const row = getRowById(tagsData, tagID);
|
||||
const oldTagFileLinks = getTagFilesLinks(row);
|
||||
if (Array.isArray(oldTagFileLinks) && oldTagFileLinks.length > 0) {
|
||||
const newTagFileLinks = oldTagFileLinks.filter(link => !selectedFileIds.includes(link.row_id));
|
||||
const update = { [PRIVATE_COLUMN_KEY.TAG_FILE_LINKS]: newTagFileLinks };
|
||||
idTagUpdates[tagID] = update;
|
||||
}
|
||||
});
|
||||
updateLocalTags([...updatedTags], idTagUpdates);
|
||||
|
||||
setTagFiles(prevTagFiles => ({
|
||||
...prevTagFiles,
|
||||
rows: prevTagFiles.rows.filter(row => !selectedFileIds.includes(row[TAG_FILE_KEY.ID])),
|
||||
}));
|
||||
|
||||
deleteFilesCallback && deleteFilesCallback(paths, fileNames);
|
||||
updateSelectedFileIds([]);
|
||||
});
|
||||
}, [repoID, tagsData, tagFiles, selectedFileIds, updateLocalTags, deleteFilesCallback, updateSelectedFileIds]);
|
||||
|
||||
const getDownloadTarget = useCallback(() => {
|
||||
if (!selectedFileIds.length) return [];
|
||||
return selectedFileIds.map(id => {
|
||||
const file = getFileById(tagFiles, id);
|
||||
const path = file[TAG_FILE_KEY.PARENT_DIR] === '/' ? file[TAG_FILE_KEY.NAME] : `${file[TAG_FILE_KEY.PARENT_DIR]}/${file[TAG_FILE_KEY.NAME]}`;
|
||||
return path;
|
||||
});
|
||||
}, [tagFiles, selectedFileIds]);
|
||||
|
||||
const downloadTagFiles = useCallback(() => {
|
||||
if (!selectedFileIds.length) return;
|
||||
if (selectedFileIds.length === 1) {
|
||||
const file = getFileById(tagFiles, selectedFileIds[0]);
|
||||
const filePath = Utils.joinPath(file[TAG_FILE_KEY.PARENT_DIR], file[TAG_FILE_KEY.NAME]);
|
||||
const url = URLDecorator.getUrl({ type: 'download_file_url', repoID, filePath });
|
||||
location.href = url;
|
||||
return;
|
||||
}
|
||||
if (!useGoFileserver) {
|
||||
window.sfTagsDataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_ZIP_DIALOG);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = getDownloadTarget();
|
||||
metadataAPI.zipDownload(repoID, '/', target).then(res => {
|
||||
const zipToken = res.data['zip_token'];
|
||||
location.href = `${fileServerRoot}zip/${zipToken}`;
|
||||
}).catch(error => {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}, [repoID, tagFiles, selectedFileIds, getDownloadTarget]);
|
||||
|
||||
const renameTagFile = useCallback((id, path, newName) => {
|
||||
seafileAPI.renameFile(repoID, path, newName).then(res => {
|
||||
renameFileCallback && renameFileCallback(path, newName);
|
||||
setTagFiles(prevTagFiles => ({
|
||||
...prevTagFiles,
|
||||
rows: prevTagFiles.rows.map(row => {
|
||||
if (row[TAG_FILE_KEY.ID] === id) {
|
||||
return { ...row, [TAG_FILE_KEY.NAME]: newName };
|
||||
}
|
||||
return row;
|
||||
})
|
||||
}));
|
||||
}).catch(error => {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}, [repoID, renameFileCallback]);
|
||||
|
||||
const convertFile = useCallback((path, dstType) => {
|
||||
seafileAPI.convertFile(repoID, path, dstType).then((res) => {
|
||||
const newFileName = res.data.obj_name;
|
||||
const parentDir = res.data.parent_dir;
|
||||
convertFileCallback({ newName: newFileName, parentDir, size: res.data.size });
|
||||
}).catch((error) => {
|
||||
convertFileCallback({ path, error });
|
||||
});
|
||||
}, [repoID, convertFileCallback]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const childTagsIds = getChildTagsIds(tagID, nodeKey);
|
||||
@@ -53,9 +183,17 @@ export const TagViewProvider = ({ repoID, tagID, nodeKey, children, ...params })
|
||||
repoID,
|
||||
tagID,
|
||||
repoInfo: params.repoInfo,
|
||||
deleteFilesCallback: params.deleteFilesCallback,
|
||||
renameFileCallback: params.renameFileCallback,
|
||||
updateCurrentDirent: params.updateCurrentDirent,
|
||||
selectedFileIds,
|
||||
updateSelectedFileIds,
|
||||
moveTagFile,
|
||||
copyTagFile,
|
||||
addFolder: addFolderCallback,
|
||||
deleteTagFiles,
|
||||
getDownloadTarget,
|
||||
downloadTagFiles,
|
||||
renameTagFile,
|
||||
convertFile,
|
||||
}}>
|
||||
{children}
|
||||
</TagViewContext.Provider>
|
||||
|
@@ -192,6 +192,29 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
||||
modifyLocalTags(tagIds, idTagUpdates, { [tagId]: originalRowUpdates }, { [tagId]: oldRowData }, { [tagId]: originalOldRowData }, { success_callback, fail_callback });
|
||||
}, [tagsData, modifyLocalTags]);
|
||||
|
||||
const updateLocalTags = useCallback((tagIds, idTagUpdates, { success_callback, fail_callback } = { }) => {
|
||||
if (!Array.isArray(tagIds) || tagIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
let idOriginalRowUpdates = {};
|
||||
let idOldRowData = {};
|
||||
let idOriginalOldRowData = {};
|
||||
tagIds.forEach((tagId) => {
|
||||
const tag = getRowById(tagsData, tagId);
|
||||
const tagUpdates = idTagUpdates[tagId];
|
||||
if (tagUpdates) {
|
||||
Object.keys(tagUpdates).forEach((key) => {
|
||||
const column = tagsData.key_column_map[key];
|
||||
const columnName = getColumnOriginName(column);
|
||||
idOriginalRowUpdates[tagId] = Object.assign({}, idOriginalRowUpdates[tagId], { [key]: tagUpdates[key] });
|
||||
idOldRowData[tagId] = Object.assign({}, idOldRowData[tagId], { [key]: getCellValueByColumn(tag, column) });
|
||||
idOriginalOldRowData[tagId] = Object.assign({}, idOriginalOldRowData[tagId], { [columnName]: getCellValueByColumn(tag, column) });
|
||||
});
|
||||
}
|
||||
});
|
||||
modifyLocalTags(tagIds, idTagUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, { success_callback, fail_callback });
|
||||
}, [tagsData, modifyLocalTags]);
|
||||
|
||||
const addTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
|
||||
storeRef.current.addTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
||||
}, [storeRef]);
|
||||
@@ -291,9 +314,10 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
||||
deleteTagsLinks,
|
||||
mergeTags,
|
||||
updateLocalTag,
|
||||
updateLocalTags,
|
||||
selectTag: handleSelectTag,
|
||||
modifyColumnWidth,
|
||||
modifyLocalFileTags,
|
||||
modifyLocalFileTags
|
||||
}}>
|
||||
{children}
|
||||
</TagsContext.Provider>
|
||||
|
@@ -8,6 +8,7 @@ import { PRIVATE_COLUMN_KEY, ALL_TAGS_ID } from '../constants';
|
||||
import { checkTreeNodeHasChildNodes, getTreeChildNodes, getTreeNodeDepth, getTreeNodeId, getTreeNodeKey } from '../../components/sf-table/utils/tree';
|
||||
import { getRowById } from '../../metadata/utils/table';
|
||||
import { SIDEBAR_INIT_LEFT_INDENT } from '../constants/sidebar-tree';
|
||||
import { EVENT_BUS_TYPE } from '../../metadata/constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -77,6 +78,7 @@ const TagsTreeView = ({ currentPath }) => {
|
||||
const nodeKey = getTreeNodeKey(node);
|
||||
selectTag(tag, nodeKey);
|
||||
setCurrSelectedNodeKey(nodeKey);
|
||||
window.sfTagsDataContext && window.sfTagsDataContext.eventBus.dispatch(EVENT_BUS_TYPE.UNSELECT_TAG_FILES);
|
||||
}, [tagsData, selectTag]);
|
||||
|
||||
const selectAllTags = useCallback((isSelected) => {
|
||||
|
62
frontend/src/tag/utils/file.js
Normal file
62
frontend/src/tag/utils/file.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { enableSeadoc, fileAuditEnabled, isPro } from '../../utils/constants';
|
||||
import TextTranslation from '../../utils/text-translation';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { TAG_FILE_KEY } from '../constants/file';
|
||||
|
||||
export const getFileById = (tagFiles, fileId) => {
|
||||
return fileId ? tagFiles.rows.find(file => file._id === fileId) : '';
|
||||
};
|
||||
|
||||
export const getFileName = (file) => {
|
||||
return file ? file[TAG_FILE_KEY.NAME] : '';
|
||||
};
|
||||
|
||||
export const getFileParentDir = (file) => {
|
||||
return file ? file[TAG_FILE_KEY.PARENT_DIR] : '';
|
||||
};
|
||||
|
||||
export const getTagFileOperationList = (fileName, repo, canModify) => {
|
||||
const {
|
||||
SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT, CONVERT_AND_EXPORT, CONVERT_TO_MARKDOWN, CONVERT_TO_DOCX, EXPORT_DOCX, CONVERT_TO_SDOC, EXPORT_SDOC
|
||||
} = TextTranslation;
|
||||
let menuList = [DOWNLOAD, SHARE];
|
||||
if (canModify) {
|
||||
menuList.push(DELETE, 'Divider', RENAME, MOVE, COPY);
|
||||
if (enableSeadoc && !repo.encrypted) {
|
||||
menuList.push('Divider');
|
||||
if (fileName.endsWith('.md') || fileName.endsWith('.docx')) {
|
||||
menuList.push(CONVERT_TO_SDOC);
|
||||
}
|
||||
if (fileName.endsWith('.sdoc')) {
|
||||
if (Utils.isDesktop()) {
|
||||
let subOpList = [CONVERT_TO_MARKDOWN, CONVERT_TO_DOCX, EXPORT_DOCX, EXPORT_SDOC];
|
||||
menuList.push({ ...CONVERT_AND_EXPORT, subOpList });
|
||||
} else {
|
||||
menuList.push(CONVERT_TO_MARKDOWN);
|
||||
menuList.push(CONVERT_TO_DOCX);
|
||||
menuList.push(EXPORT_DOCX);
|
||||
menuList.push(EXPORT_SDOC);
|
||||
}
|
||||
}
|
||||
}
|
||||
menuList.push('Divider', HISTORY);
|
||||
if (isPro && fileAuditEnabled) {
|
||||
menuList.push(ACCESS_LOG);
|
||||
}
|
||||
menuList.push('Divider', OPEN_VIA_CLIENT);
|
||||
}
|
||||
|
||||
// if the last item of menuList is ‘Divider’, delete the last item
|
||||
if (menuList[menuList.length - 1] === 'Divider') {
|
||||
menuList.pop();
|
||||
}
|
||||
|
||||
// Remove adjacent excess 'Divider'
|
||||
for (let i = 0; i < menuList.length; i++) {
|
||||
if (menuList[i] === 'Divider' && menuList[i + 1] === 'Divider') {
|
||||
menuList.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
return menuList;
|
||||
};
|
@@ -1,3 +1,5 @@
|
||||
.sf-metadata-tag-files-wrapper .sf-metadata-tags-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
@@ -1,25 +1,67 @@
|
||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
|
||||
import { useTagView, useTags } from '../../hooks';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import TagFile from './tag-file';
|
||||
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
||||
import { gettext, username } from '../../../utils/constants';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
import ImagePreviewer from '../../../metadata/components/cell-formatter/image-previewer';
|
||||
import toaster from '../../../components/toast';
|
||||
import ContextMenu from '../../../components/context-menu/context-menu';
|
||||
import MoveDirent from '../../../components/dialog/move-dirent-dialog';
|
||||
import CopyDirent from '../../../components/dialog/copy-dirent-dialog';
|
||||
import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog';
|
||||
import ShareDialog from '../../../components/dialog/share-dialog';
|
||||
import FileAccessLog from '../../../components/dialog/file-access-log';
|
||||
import Rename from '../../../components/dialog/rename-dirent';
|
||||
import FixedWidthTable from '../../../components/common/fixed-width-table';
|
||||
import ImagePreviewer from '../../../metadata/components/cell-formatter/image-previewer';
|
||||
import TagFile from './tag-file';
|
||||
import TextTranslation from '../../../utils/text-translation';
|
||||
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { TAG_FILE_KEY } from '../../constants/file';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { getFileById, getFileName, getFileParentDir, getTagFileOperationList } from '../../utils/file';
|
||||
import { EVENT_BUS_TYPE } from '../../../metadata/constants';
|
||||
import { hideMenu, showMenu } from '../../../components/context-menu/actions';
|
||||
import URLDecorator from '../../../utils/url-decorator';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const TAG_FILE_CONTEXT_MENU_ID = 'tag-files-context-menu';
|
||||
|
||||
const TagFiles = () => {
|
||||
const { tagFiles, repoID, repoInfo } = useTagView();
|
||||
const { tagsData } = useTags();
|
||||
const [selectedFiles, setSelectedFiles] = useState(null);
|
||||
const {
|
||||
tagFiles, repoID, repoInfo, selectedFileIds, updateSelectedFileIds,
|
||||
moveTagFile, copyTagFile, addFolder, deleteTagFiles, renameTagFile, getDownloadTarget, downloadTagFiles, convertFile,
|
||||
} = useTagView();
|
||||
|
||||
const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);
|
||||
const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false);
|
||||
const [isZipDialogOpen, setIsZipDialogOpen] = useState(false);
|
||||
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
|
||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||
const [isImagePreviewerVisible, setImagePreviewerVisible] = useState(false);
|
||||
const [isFileAccessLogDialogOpen, setIsFileAccessLogDialogOpen] = useState(false);
|
||||
|
||||
const currentImageRef = useRef(null);
|
||||
|
||||
const isSelectedAll = useMemo(() => {
|
||||
return selectedFiles ? selectedFiles.length === tagFiles.rows.length : false;
|
||||
}, [selectedFiles, tagFiles]);
|
||||
return selectedFileIds ? selectedFileIds.length === tagFiles.rows.length : false;
|
||||
}, [selectedFileIds, tagFiles]);
|
||||
|
||||
// selectedFile
|
||||
const selectedFile = useMemo(() => {
|
||||
if (!selectedFileIds || selectedFileIds.length === 0) return null;
|
||||
return getFileById(tagFiles, selectedFileIds[0]);
|
||||
}, [selectedFileIds, tagFiles]);
|
||||
const selectedFileParentDir = useMemo(() => {
|
||||
return getFileParentDir(selectedFile);
|
||||
}, [selectedFile]);
|
||||
const selectedFileName = useMemo(() => {
|
||||
return getFileName(selectedFile);
|
||||
}, [selectedFile]);
|
||||
const selectedFilePath = useMemo(() => {
|
||||
return selectedFileParentDir && selectedFileName ? Utils.joinPath(selectedFileParentDir, selectedFileName) : '';
|
||||
}, [selectedFileParentDir, selectedFileName]);
|
||||
|
||||
const onMouseDown = useCallback((event) => {
|
||||
if (event.button === 2) {
|
||||
@@ -38,30 +80,48 @@ const TagFiles = () => {
|
||||
|
||||
const onSelectedAll = useCallback(() => {
|
||||
if (isSelectedAll) {
|
||||
setSelectedFiles([]);
|
||||
updateSelectedFileIds([]);
|
||||
} else {
|
||||
const allIds = tagFiles.rows.map(record => getRecordIdFromRecord(record));
|
||||
setSelectedFiles(allIds);
|
||||
updateSelectedFileIds(allIds);
|
||||
}
|
||||
}, [tagFiles, isSelectedAll]);
|
||||
}, [tagFiles, isSelectedAll, updateSelectedFileIds]);
|
||||
|
||||
const onSelectFile = useCallback((fileId) => {
|
||||
let newSelectedFiles = selectedFiles ? selectedFiles.slice(0) : [];
|
||||
if (newSelectedFiles.includes(fileId)) {
|
||||
newSelectedFiles = newSelectedFiles.filter(item => item !== fileId);
|
||||
} else {
|
||||
newSelectedFiles.push(fileId);
|
||||
}
|
||||
if (newSelectedFiles.length > 0) {
|
||||
setSelectedFiles(newSelectedFiles);
|
||||
} else {
|
||||
setSelectedFiles(null);
|
||||
}
|
||||
}, [selectedFiles]);
|
||||
const onContainerClick = (event) => {
|
||||
hideMenu();
|
||||
if (selectedFileIds.length > 0) updateSelectedFileIds([]);
|
||||
};
|
||||
|
||||
const reSelectFiles = useCallback((fileId) => {
|
||||
setSelectedFiles([fileId]);
|
||||
}, []);
|
||||
const onSelectFile = useCallback((event, fileId) => {
|
||||
if (event.target.tagName === 'TD') {
|
||||
updateSelectedFileIds([fileId]);
|
||||
return;
|
||||
}
|
||||
const newSelectedFileIds = selectedFileIds.includes(fileId)
|
||||
? selectedFileIds.filter(id => id !== fileId)
|
||||
: [...selectedFileIds, fileId];
|
||||
updateSelectedFileIds(newSelectedFileIds);
|
||||
}, [selectedFileIds, updateSelectedFileIds]);
|
||||
|
||||
const toggleMoveDialog = useCallback(() => {
|
||||
setIsMoveDialogOpen(!isMoveDialogOpen);
|
||||
}, [isMoveDialogOpen]);
|
||||
|
||||
const toggleCopyDialog = useCallback(() => {
|
||||
setIsCopyDialogOpen(!isCopyDialogOpen);
|
||||
}, [isCopyDialogOpen]);
|
||||
|
||||
const toggleZipDialog = useCallback(() => {
|
||||
setIsZipDialogOpen(!isZipDialogOpen);
|
||||
}, [isZipDialogOpen]);
|
||||
|
||||
const toggleShareDialog = useCallback(() => {
|
||||
setIsShareDialogOpen(!isShareDialogOpen);
|
||||
}, [isShareDialogOpen]);
|
||||
|
||||
const toggleRenameDialog = useCallback(() => {
|
||||
setIsRenameDialogOpen(!isRenameDialogOpen);
|
||||
}, [isRenameDialogOpen]);
|
||||
|
||||
const openImagePreview = useCallback((record) => {
|
||||
currentImageRef.current = record;
|
||||
@@ -73,6 +133,197 @@ const TagFiles = () => {
|
||||
setImagePreviewerVisible(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteTagFiles = useCallback(() => {
|
||||
deleteTagFiles();
|
||||
updateSelectedFileIds([]);
|
||||
}, [deleteTagFiles, updateSelectedFileIds]);
|
||||
|
||||
const handleRenameTagFile = useCallback((newName) => {
|
||||
const path = selectedFile[TAG_FILE_KEY.PARENT_DIR];
|
||||
const newPath = Utils.joinPath(path, newName);
|
||||
seafileAPI.getFileInfo(repoID, newPath).then(() => {
|
||||
let errMessage = gettext('The name "{name}" is already taken. Please choose a different name.');
|
||||
errMessage = errMessage.replace('{name}', Utils.HTMLescape(newName));
|
||||
toaster.danger(errMessage);
|
||||
}).catch(error => {
|
||||
if (error && error.response && error.response.status === 404) {
|
||||
const fullPath = Utils.joinPath(path, selectedFile[TAG_FILE_KEY.NAME]);
|
||||
renameTagFile(selectedFile[TAG_FILE_KEY.ID], fullPath, newName);
|
||||
} else {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
}
|
||||
});
|
||||
}, [repoID, selectedFile, renameTagFile]);
|
||||
|
||||
const openViaClient = useCallback(() => {
|
||||
let url = URLDecorator.getUrl({ type: 'open_via_client', repoID, selectedFilePath });
|
||||
location.href = url;
|
||||
}, [repoID, selectedFilePath]);
|
||||
|
||||
const onHistory = useCallback(() => {
|
||||
let url = URLDecorator.getUrl({
|
||||
type: 'file_revisions',
|
||||
repoID: repoID,
|
||||
filePath: selectedFilePath,
|
||||
});
|
||||
location.href = url;
|
||||
}, [repoID, selectedFilePath]);
|
||||
|
||||
const onConvertFile = useCallback((dstType) => {
|
||||
toaster.notifyInProgress(gettext('Converting, please wait...'), { 'id': 'conversion' });
|
||||
convertFile(selectedFilePath, dstType);
|
||||
}, [selectedFilePath, convertFile]);
|
||||
|
||||
const exportDocx = useCallback((dirent) => {
|
||||
const serviceUrl = window.app.config.serviceURL;
|
||||
const exportToDocxUrl = serviceUrl + '/repo/sdoc_export_to_docx/' + repoID + '/?file_path=' + selectedFilePath;
|
||||
window.location.href = exportToDocxUrl;
|
||||
}, [repoID, selectedFilePath]);
|
||||
|
||||
const exportSdoc = useCallback((dirent) => {
|
||||
const serviceUrl = window.app.config.serviceURL;
|
||||
const exportToSdocUrl = serviceUrl + '/lib/' + repoID + '/file/' + selectedFilePath + '?dl=1';
|
||||
window.location.href = exportToSdocUrl;
|
||||
}, [repoID, selectedFilePath]);
|
||||
|
||||
const getMenuContainerSize = useCallback(() => {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getMenuList = useCallback((file) => {
|
||||
const { DOWNLOAD, DELETE } = TextTranslation;
|
||||
if (selectedFileIds.length > 1) {
|
||||
return [DOWNLOAD, DELETE];
|
||||
}
|
||||
const canModify = window.sfTagsDataContext && window.sfTagsDataContext.canModify();
|
||||
const fileName = file ? getFileName(file) : selectedFileName;
|
||||
return getTagFileOperationList(fileName, repoInfo, canModify);
|
||||
}, [selectedFileIds, selectedFileName, repoInfo]);
|
||||
|
||||
const onMenuItemClick = useCallback((option) => {
|
||||
if (!option) return;
|
||||
switch (option) {
|
||||
case TextTranslation.MOVE.key:
|
||||
toggleMoveDialog();
|
||||
break;
|
||||
case TextTranslation.COPY.key:
|
||||
toggleCopyDialog();
|
||||
break;
|
||||
case TextTranslation.DELETE.key:
|
||||
handleDeleteTagFiles();
|
||||
break;
|
||||
case TextTranslation.SHARE.key:
|
||||
toggleShareDialog();
|
||||
break;
|
||||
case TextTranslation.DOWNLOAD.key:
|
||||
downloadTagFiles();
|
||||
break;
|
||||
case TextTranslation.RENAME.key:
|
||||
window.sfTagsDataContext && window.sfTagsDataContext.eventBus.dispatch(EVENT_BUS_TYPE.RENAME_TAG_FILE, selectedFileIds[0]);
|
||||
break;
|
||||
case TextTranslation.CONVERT_TO_SDOC.key:
|
||||
onConvertFile('sdoc');
|
||||
break;
|
||||
case TextTranslation.CONVERT_TO_MARKDOWN.key: {
|
||||
onConvertFile('markdown');
|
||||
break;
|
||||
}
|
||||
case TextTranslation.CONVERT_TO_DOCX.key: {
|
||||
onConvertFile('docx');
|
||||
break;
|
||||
}
|
||||
case TextTranslation.EXPORT_DOCX.key: {
|
||||
exportDocx();
|
||||
break;
|
||||
}
|
||||
case TextTranslation.EXPORT_SDOC.key: {
|
||||
exportSdoc();
|
||||
break;
|
||||
}
|
||||
case TextTranslation.HISTORY.key:
|
||||
onHistory();
|
||||
break;
|
||||
case TextTranslation.ACCESS_LOG.key:
|
||||
setIsFileAccessLogDialogOpen(true);
|
||||
break;
|
||||
case TextTranslation.OPEN_VIA_CLIENT.key:
|
||||
openViaClient();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
hideMenu();
|
||||
}, [toggleMoveDialog, toggleCopyDialog, handleDeleteTagFiles, downloadTagFiles, selectedFileIds, onConvertFile, exportDocx, exportSdoc, toggleShareDialog, openViaClient, onHistory]);
|
||||
|
||||
const onTagFileContextMenu = useCallback((event, file) => {
|
||||
let menuList = [];
|
||||
if (selectedFileIds.length <= 1) {
|
||||
const fileId = getRecordIdFromRecord(file);
|
||||
updateSelectedFileIds([fileId]);
|
||||
menuList = getMenuList(file);
|
||||
} else {
|
||||
menuList = getMenuList();
|
||||
}
|
||||
const id = TAG_FILE_CONTEXT_MENU_ID;
|
||||
let x = event.clientX || (event.touches && event.touches[0].pageX);
|
||||
let y = event.clientY || (event.touches && event.touches[0].pageY);
|
||||
|
||||
hideMenu();
|
||||
|
||||
let showMenuConfig = {
|
||||
id: id,
|
||||
position: { x, y },
|
||||
target: event.target,
|
||||
currentObject: file,
|
||||
menuList: menuList,
|
||||
};
|
||||
|
||||
if (menuList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
showMenu(showMenuConfig);
|
||||
}, [selectedFileIds, updateSelectedFileIds, getMenuList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.sfTagsDataContext) return;
|
||||
const unsubscribeUnselectFiles = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.UNSELECT_TAG_FILES, () => updateSelectedFileIds([]));
|
||||
const unsubscribeDeleteTagFiles = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.DELETE_TAG_FILES, deleteTagFiles);
|
||||
const unsubScribeMoveTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.MOVE_TAG_FILE, toggleMoveDialog);
|
||||
const unsubScribeCopyTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.COPY_TAG_FILE, toggleCopyDialog);
|
||||
const unsubscribeShareTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.SHARE_TAG_FILE, toggleShareDialog);
|
||||
const unsubscribeRenameTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_RENAME_DIALOG, toggleRenameDialog);
|
||||
const unsubscribeDownloadTagFiles = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.DOWNLOAD_TAG_FILES, downloadTagFiles);
|
||||
const unsubscribeZipDownload = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_ZIP_DIALOG, toggleZipDialog);
|
||||
const unsubscribeFileHistory = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.FILE_HISTORY, onHistory);
|
||||
const unsubscribeFileAccessLog = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.FILE_ACCESS_LOG, () => setIsFileAccessLogDialogOpen(true));
|
||||
const unsubscribeOpenViaClient = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.OPEN_VIA_CLIENT, openViaClient);
|
||||
const unsubscribeConvertFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.CONVERT_FILE, onConvertFile);
|
||||
const unsubscribeExportDocx = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.EXPORT_DOCX, exportDocx);
|
||||
const unsubscribeExportSDoc = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.EXPORT_SDOC, exportSdoc);
|
||||
|
||||
return () => {
|
||||
unsubscribeUnselectFiles();
|
||||
unsubscribeDeleteTagFiles();
|
||||
unsubScribeMoveTagFile();
|
||||
unsubScribeCopyTagFile();
|
||||
unsubscribeShareTagFile();
|
||||
unsubscribeRenameTagFile();
|
||||
unsubscribeDownloadTagFiles();
|
||||
unsubscribeZipDownload();
|
||||
unsubscribeFileHistory();
|
||||
unsubscribeFileAccessLog();
|
||||
unsubscribeOpenViaClient();
|
||||
unsubscribeConvertFile();
|
||||
unsubscribeExportDocx();
|
||||
unsubscribeExportSDoc();
|
||||
};
|
||||
});
|
||||
|
||||
if (tagFiles.rows.length === 0) {
|
||||
return (<EmptyTip text={gettext('No files')} />);
|
||||
}
|
||||
@@ -117,9 +368,18 @@ const TagFiles = () => {
|
||||
}
|
||||
];
|
||||
|
||||
let enableDirPrivateShare = false;
|
||||
let isRepoOwner = repoInfo.owner_email === username;
|
||||
let isVirtual = repoInfo.is_virtual;
|
||||
let isAdmin = repoInfo.is_admin;
|
||||
if (!isVirtual && (isRepoOwner || isAdmin)) {
|
||||
enableDirPrivateShare = true;
|
||||
}
|
||||
const isGroupOwnedRepo = repoInfo.owner_email.includes('@seafile_group');
|
||||
const canDelete = window.sfTagsDataContext && window.sfTagsDataContext.canModifyTag();
|
||||
return (
|
||||
<>
|
||||
<div className="table-container">
|
||||
<div className="table-container" onClick={onContainerClick}>
|
||||
<FixedWidthTable
|
||||
headers={headers}
|
||||
className="table-hover"
|
||||
@@ -134,12 +394,13 @@ const TagFiles = () => {
|
||||
<TagFile
|
||||
key={fileId}
|
||||
repoID={repoID}
|
||||
isSelected={selectedFiles ? selectedFiles.includes(fileId) : false}
|
||||
isSelected={selectedFileIds ? selectedFileIds.includes(fileId) : false}
|
||||
file={file}
|
||||
tagsData={tagsData}
|
||||
onSelectFile={onSelectFile}
|
||||
reSelectFiles={reSelectFiles}
|
||||
openImagePreview={openImagePreview}
|
||||
onRenameFile={handleRenameTagFile}
|
||||
onContextMenu={onTagFileContextMenu}
|
||||
/>);
|
||||
})}
|
||||
</FixedWidthTable>
|
||||
@@ -151,8 +412,75 @@ const TagFiles = () => {
|
||||
record={currentImageRef.current}
|
||||
table={tagFiles}
|
||||
closeImagePopup={closeImagePreviewer}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
)}
|
||||
<ContextMenu
|
||||
id={TAG_FILE_CONTEXT_MENU_ID}
|
||||
onMenuItemClick={onMenuItemClick}
|
||||
getMenuContainerSize={getMenuContainerSize}
|
||||
/>
|
||||
{isMoveDialogOpen && (
|
||||
<MoveDirent
|
||||
path={selectedFile[TAG_FILE_KEY.PARENT_DIR]}
|
||||
repoID={repoID}
|
||||
repoEncrypted={repoInfo.encrypted}
|
||||
isMultipleOperation={false}
|
||||
dirent={{ name: selectedFile[TAG_FILE_KEY.NAME] }}
|
||||
onItemMove={moveTagFile}
|
||||
onCancelMove={toggleMoveDialog}
|
||||
onAddFolder={addFolder}
|
||||
/>
|
||||
)}
|
||||
{isCopyDialogOpen && (
|
||||
<CopyDirent
|
||||
path={selectedFile[TAG_FILE_KEY.PARENT_DIR]}
|
||||
repoID={repoID}
|
||||
repoEncrypted={repoInfo.encrypted}
|
||||
isMultipleOperation={false}
|
||||
dirent={{ name: selectedFile[TAG_FILE_KEY.NAME] }}
|
||||
onItemCopy={copyTagFile}
|
||||
onCancelCopy={toggleCopyDialog}
|
||||
onAddFolder={addFolder}
|
||||
/>
|
||||
)}
|
||||
{isZipDialogOpen && (
|
||||
<ZipDownloadDialog
|
||||
repoID={repoID}
|
||||
path="/"
|
||||
target={getDownloadTarget()}
|
||||
toggleDialog={toggleZipDialog}
|
||||
/>
|
||||
)}
|
||||
{isShareDialogOpen &&
|
||||
<ShareDialog
|
||||
itemType='file'
|
||||
itemName={selectedFile[TAG_FILE_KEY.NAME]}
|
||||
itemPath={Utils.joinPath(selectedFile[TAG_FILE_KEY.PARENT_DIR], selectedFile[TAG_FILE_KEY.NAME])}
|
||||
userPerm={repoInfo.permission}
|
||||
repoID={repoID}
|
||||
repoEncrypted={repoInfo.repoEncrypted}
|
||||
enableDirPrivateShare={enableDirPrivateShare}
|
||||
isGroupOwnedRepo={isGroupOwnedRepo}
|
||||
toggleDialog={toggleShareDialog}
|
||||
/>
|
||||
}
|
||||
{isRenameDialogOpen &&
|
||||
<Rename
|
||||
dirent={{ name: selectedFile[TAG_FILE_KEY.NAME], type: 'file' }}
|
||||
onRename={handleRenameTagFile}
|
||||
checkDuplicatedName={() => {}}
|
||||
toggleCancel={toggleRenameDialog}
|
||||
/>
|
||||
}
|
||||
{isFileAccessLogDialogOpen &&
|
||||
<FileAccessLog
|
||||
repoID={repoID}
|
||||
filePath={Utils.joinPath(selectedFile[TAG_FILE_KEY.PARENT_DIR], selectedFile[TAG_FILE_KEY.NAME])}
|
||||
fileName={selectedFile[TAG_FILE_KEY.NAME]}
|
||||
toggleDialog={() => setIsFileAccessLogDialogOpen(false)}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -10,14 +10,18 @@ import { getParentDirFromRecord, getRecordIdFromRecord, getFileNameFromRecord, g
|
||||
} from '../../../../metadata/utils/cell';
|
||||
import { Utils } from '../../../../utils/utils';
|
||||
import { openFile } from '../../../../metadata/utils/file';
|
||||
import Rename from '../../../../components/rename';
|
||||
import { TAG_FILE_KEY } from '../../../constants/file';
|
||||
import { EVENT_BUS_TYPE } from '../../../../metadata/constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFiles, openImagePreview }) => {
|
||||
const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, openImagePreview, onRenameFile, onContextMenu }) => {
|
||||
const [highlight, setHighlight] = useState(false);
|
||||
const [isIconLoadError, setIconLoadError] = useState(false);
|
||||
const [isRenameing, setIsRenaming] = useState(false);
|
||||
|
||||
const fileId = useMemo(() => getRecordIdFromRecord(file), [file]);
|
||||
const parentDir = useMemo(() => getParentDirFromRecord(file), [file]);
|
||||
@@ -62,7 +66,7 @@ const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFil
|
||||
|
||||
const handleSelected = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
onSelectFile(fileId);
|
||||
onSelectFile(event, fileId);
|
||||
}, [fileId, onSelectFile]);
|
||||
|
||||
const onIconLoadError = useCallback(() => {
|
||||
@@ -71,16 +75,45 @@ const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFil
|
||||
|
||||
const handelClickFileName = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
if (isRenameing) return;
|
||||
openFile(repoID, file, () => {
|
||||
openImagePreview(file);
|
||||
});
|
||||
}, [repoID, file, openImagePreview]);
|
||||
}, [repoID, file, openImagePreview, isRenameing]);
|
||||
|
||||
const handelClick = useCallback((event) => {
|
||||
if (event.target.tagName == 'TD') {
|
||||
reSelectFiles(fileId);
|
||||
}
|
||||
}, [fileId, reSelectFiles]);
|
||||
event.stopPropagation();
|
||||
if (isRenameing) return;
|
||||
onSelectFile(event, fileId);
|
||||
}, [fileId, onSelectFile, isRenameing]);
|
||||
|
||||
const onRenameCancel = useCallback(() => {
|
||||
setIsRenaming(false);
|
||||
}, []);
|
||||
|
||||
const onRenameConfirm = useCallback((newName) => {
|
||||
onRenameFile(newName);
|
||||
onRenameCancel();
|
||||
}, [onRenameFile, onRenameCancel]);
|
||||
|
||||
const toggleRename = useCallback((id) => {
|
||||
if (id === file[TAG_FILE_KEY.ID]) setIsRenaming(true);
|
||||
}, [file]);
|
||||
|
||||
const handleContextMenu = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onContextMenu(event, file);
|
||||
}, [file, onContextMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.sfTagsDataContext) return;
|
||||
const unsubscribeRenameTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.RENAME_TAG_FILE, (id) => toggleRename(id));
|
||||
|
||||
return () => {
|
||||
unsubscribeRenameTagFile && unsubscribeRenameTagFile();
|
||||
};
|
||||
}, [toggleRename]);
|
||||
|
||||
return (
|
||||
<tr
|
||||
@@ -91,6 +124,7 @@ const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFil
|
||||
onClick={handelClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<td className="pl10 pr-2">
|
||||
<input
|
||||
@@ -109,7 +143,16 @@ const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFil
|
||||
</div>
|
||||
</td>
|
||||
<td className="name">
|
||||
{isRenameing ? (
|
||||
<Rename
|
||||
hasSuffix={true}
|
||||
name={file[TAG_FILE_KEY.NAME]}
|
||||
onRenameConfirm={onRenameConfirm}
|
||||
onRenameCancel={onRenameCancel}
|
||||
/>
|
||||
) : (
|
||||
<a href={path} onClick={handelClickFileName}>{name}</a>
|
||||
)}
|
||||
</td>
|
||||
<td className="tag-list-title">
|
||||
<FileTagsFormatter value={tags} tagsData={tagsData} className="sf-metadata-tags-formatter" />
|
||||
@@ -129,6 +172,7 @@ TagFile.propTypes = {
|
||||
onSelectFile: PropTypes.func,
|
||||
openImagePreview: PropTypes.func,
|
||||
reSelectFiles: PropTypes.func,
|
||||
onRenameFile: PropTypes.func,
|
||||
};
|
||||
|
||||
export default TagFile;
|
||||
|
Reference in New Issue
Block a user