@@ -2251,33 +2261,36 @@ class LibContentView extends React.Component {
'w-100': !isDesktop,
'animation-children': isDirentSelected
})}>
- {isDirentSelected ?
-
- :
+ {isDirentSelected ? (
+ this.state.currentMode === TAGS_MODE ?
+
+ :
+
+ ) : (
- }
+ )}
{isDesktop &&
@@ -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}
/>
:
{gettext('Folder does not exist.')}
diff --git a/frontend/src/tag/constants/file.js b/frontend/src/tag/constants/file.js
new file mode 100644
index 0000000000..782f03448d
--- /dev/null
+++ b/frontend/src/tag/constants/file.js
@@ -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',
+};
diff --git a/frontend/src/tag/hooks/tag-view.js b/frontend/src/tag/hooks/tag-view.js
index 62861c9ee1..121015d227 100644
--- a/frontend/src/tag/hooks/tag-view.js
+++ b/frontend/src/tag/hooks/tag-view.js
@@ -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}
diff --git a/frontend/src/tag/hooks/tags.js b/frontend/src/tag/hooks/tags.js
index f2c2a9de13..d70caece0c 100644
--- a/frontend/src/tag/hooks/tags.js
+++ b/frontend/src/tag/hooks/tags.js
@@ -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}
diff --git a/frontend/src/tag/tags-tree-view/index.js b/frontend/src/tag/tags-tree-view/index.js
index 88b55aecb2..9107a4351c 100644
--- a/frontend/src/tag/tags-tree-view/index.js
+++ b/frontend/src/tag/tags-tree-view/index.js
@@ -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) => {
diff --git a/frontend/src/tag/utils/file.js b/frontend/src/tag/utils/file.js
new file mode 100644
index 0000000000..64c1591e0e
--- /dev/null
+++ b/frontend/src/tag/utils/file.js
@@ -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;
+};
diff --git a/frontend/src/tag/views/tag-files/index.css b/frontend/src/tag/views/tag-files/index.css
index 169670e511..33b0ca563d 100644
--- a/frontend/src/tag/views/tag-files/index.css
+++ b/frontend/src/tag/views/tag-files/index.css
@@ -1,3 +1,5 @@
.sf-metadata-tag-files-wrapper .sf-metadata-tags-main {
+ display: flex;
+ flex-direction: column;
overflow: auto !important;
}
diff --git a/frontend/src/tag/views/tag-files/index.js b/frontend/src/tag/views/tag-files/index.js
index e08cf1f3a6..13525b7ae1 100644
--- a/frontend/src/tag/views/tag-files/index.js
+++ b/frontend/src/tag/views/tag-files/index.js
@@ -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 (
);
}
@@ -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 (
<>
-
+
{
);
})}
@@ -151,8 +412,75 @@ const TagFiles = () => {
record={currentImageRef.current}
table={tagFiles}
closeImagePopup={closeImagePreviewer}
+ canDelete={canDelete}
/>
)}
+
+ {isMoveDialogOpen && (
+
+ )}
+ {isCopyDialogOpen && (
+
+ )}
+ {isZipDialogOpen && (
+
+ )}
+ {isShareDialogOpen &&
+
+ }
+ {isRenameDialogOpen &&
+
{}}
+ toggleCancel={toggleRenameDialog}
+ />
+ }
+ {isFileAccessLogDialogOpen &&
+ setIsFileAccessLogDialogOpen(false)}
+ />
+ }
>
);
};
diff --git a/frontend/src/tag/views/tag-files/tag-file/index.js b/frontend/src/tag/views/tag-files/tag-file/index.js
index d4c15821d3..9c9b48461a 100644
--- a/frontend/src/tag/views/tag-files/tag-file/index.js
+++ b/frontend/src/tag/views/tag-files/tag-file/index.js
@@ -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 (
|
- {name}
+ {isRenameing ? (
+
+ ) : (
+ {name}
+ )}
|
@@ -129,6 +172,7 @@ TagFile.propTypes = {
onSelectFile: PropTypes.func,
openImagePreview: PropTypes.func,
reSelectFiles: PropTypes.func,
+ onRenameFile: PropTypes.func,
};
export default TagFile;
|