-
- {isEmpty && (
)}
- {!isEmpty && (
- <>
- {boards.map((board, index) => {
- return (
-
- );
- })}
- >
- )}
- {!readonly && (
)}
+ <>
+
+
+ {isEmpty && (
)}
+ {!isEmpty && (
+ <>
+ {boards.map((board, index) => {
+ return (
+
+ );
+ })}
+ >
+ )}
+ {!readonly && (
)}
+
+
{isImagePreviewerVisible && (
{
closeImagePopup={closeImagePreviewer}
/>
)}
-
+ >
);
};
diff --git a/frontend/src/metadata/views/kanban/context-menu/index.js b/frontend/src/metadata/views/kanban/context-menu/index.js
new file mode 100644
index 0000000000..9e92ef450d
--- /dev/null
+++ b/frontend/src/metadata/views/kanban/context-menu/index.js
@@ -0,0 +1,163 @@
+import React, { useCallback, useMemo, useState } from 'react';
+import PropTypes from 'prop-types';
+import { ModalPortal } from '@seafile/sf-metadata-ui-component';
+import ContextMenu from '../../../components/context-menu';
+import { getRowById } from '../../../utils/table';
+import { checkIsDir } from '../../../utils/row';
+import { getFileNameFromRecord, getParentDirFromRecord } from '../../../utils/cell';
+import { gettext, useGoFileserver, fileServerRoot } from '../../../../utils/constants';
+import { openInNewTab, openParentFolder, downloadFile } from '../../../utils/file';
+import { useMetadataView } from '../../../hooks/metadata-view';
+import { PRIVATE_COLUMN_KEY } from '../../../constants';
+import RenameDialog from '../../../components/dialog/rename-dialog';
+import { Utils } from '../../../../utils/utils';
+import toaster from '../../../../components/toast';
+import metadataAPI from '../../../api';
+import ZipDownloadDialog from '../../../../components/dialog/zip-download-dialog';
+
+const CONTEXT_MENU_KEY = {
+ OPEN_IN_NEW_TAB: 'open_in_new_tab',
+ OPEN_PARENT_FOLDER: 'open_parent_folder',
+ DOWNLOAD: 'download',
+ DELETE: 'delete',
+ RENAME: 'rename',
+};
+
+const KanbanContextMenu = ({ boundaryCoordinates, selectedCard, onDelete, onRename }) => {
+ const [isRenameDialogShow, setIsRenameDialogShow] = useState(false);
+ const [isZipDialogOpen, setIsZipDialogOpen] = useState(false);
+
+ const { metadata } = useMetadataView();
+
+ const selectedRecord = useMemo(() => getRowById(metadata, selectedCard), [metadata, selectedCard]);
+ const isDir = useMemo(() => checkIsDir(selectedRecord), [selectedRecord]);
+ const oldName = useMemo(() => getFileNameFromRecord(selectedRecord), [selectedRecord]);
+ const parentDir = useMemo(() => getParentDirFromRecord(selectedRecord), [selectedRecord]);
+
+ const repoID = window.sfMetadataContext.getSetting('repoID');
+ const checkCanDeleteRow = window.sfMetadataContext.checkCanDeleteRow();
+ const canModifyRow = window.sfMetadataContext.canModifyRow();
+
+ const options = useMemo(() => {
+ let validOptions = [
+ { value: CONTEXT_MENU_KEY.OPEN_IN_NEW_TAB, label: isDir ? gettext('Open folder in new tab') : gettext('Open file in new tab') },
+ { value: CONTEXT_MENU_KEY.OPEN_PARENT_FOLDER, label: gettext('Open parent folder') },
+ { value: CONTEXT_MENU_KEY.DOWNLOAD, label: gettext('Download') },
+ ];
+ if (checkCanDeleteRow) {
+ validOptions.push({ value: CONTEXT_MENU_KEY.DELETE, label: isDir ? gettext('Delete folder') : gettext('Delete file') });
+ }
+ if (canModifyRow) {
+ validOptions.push({ value: CONTEXT_MENU_KEY.RENAME, label: isDir ? gettext('Rename folder') : gettext('Rename file') });
+ }
+
+ return validOptions;
+ }, [isDir, checkCanDeleteRow, canModifyRow]);
+
+ const closeZipDialog = useCallback(() => {
+ setIsZipDialogOpen(false);
+ }, []);
+
+ const openRenameDialog = useCallback(() => {
+ setIsRenameDialogShow(true);
+ }, []);
+
+ const handleRename = useCallback((newName) => {
+ if (!selectedCard) return;
+ const record = getRowById(metadata, selectedCard);
+ if (!record) return;
+
+ const oldName = getFileNameFromRecord(record);
+ const updates = { [PRIVATE_COLUMN_KEY.FILE_NAME]: newName };
+ const oldRowData = { [PRIVATE_COLUMN_KEY.FILE_NAME]: oldName };
+ onRename(selectedCard, updates, oldRowData, updates, oldRowData, {
+ success_callback: () => setIsRenameDialogShow(false),
+ });
+ }, [metadata, selectedCard, onRename]);
+
+ const handelDownload = useCallback((record) => {
+ if (!isDir) {
+ downloadFile(repoID, record);
+ return;
+ }
+ if (!useGoFileserver) {
+ setIsZipDialogOpen(true);
+ return;
+ }
+ const fileName = getFileNameFromRecord(record);
+ metadataAPI.zipDownload(repoID, parentDir, [fileName]).then((res) => {
+ const zipToken = res.data['zip_token'];
+ location.href = `${fileServerRoot}zip/${zipToken}`;
+ }).catch(error => {
+ const errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }, [repoID, isDir, parentDir]);
+
+ const handleOptionClick = useCallback((option) => {
+ if (!selectedCard) return;
+ const record = getRowById(metadata, selectedCard);
+ if (!record) return;
+
+ switch (option.value) {
+ case CONTEXT_MENU_KEY.OPEN_IN_NEW_TAB: {
+ openInNewTab(repoID, record);
+ break;
+ }
+ case CONTEXT_MENU_KEY.OPEN_PARENT_FOLDER: {
+ openParentFolder(record);
+ break;
+ }
+ case CONTEXT_MENU_KEY.DOWNLOAD: {
+ handelDownload(record);
+ break;
+ }
+ case CONTEXT_MENU_KEY.DELETE: {
+ onDelete([selectedCard]);
+ break;
+ }
+ case CONTEXT_MENU_KEY.RENAME: {
+ openRenameDialog();
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }, [metadata, repoID, selectedCard, onDelete, openRenameDialog, handelDownload]);
+
+ return (
+ <>
+
+ {isRenameDialogShow && (
+
+ setIsRenameDialogShow(false)}
+ />
+
+ )}
+ {isZipDialogOpen && (
+
+
+
+ )}
+ >
+ );
+};
+
+KanbanContextMenu.propTypes = {
+ boundaryCoordinates: PropTypes.object,
+ selectedCard: PropTypes.string,
+ onDelete: PropTypes.func,
+ onRename: PropTypes.func,
+};
+
+export default KanbanContextMenu;
diff --git a/frontend/src/metadata/views/kanban/index.js b/frontend/src/metadata/views/kanban/index.js
index b91e921f96..2ba184639b 100644
--- a/frontend/src/metadata/views/kanban/index.js
+++ b/frontend/src/metadata/views/kanban/index.js
@@ -4,32 +4,101 @@ import { EVENT_BUS_TYPE } from '../../constants';
import toaster from '../../../components/toast';
import Boards from './boards';
import Settings from './settings';
+import { getRowById } from '../../utils/table';
+import { getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell';
+import { Utils, validateName } from '../../../utils/utils';
+import { gettext } from '../../../utils/constants';
import './index.css';
const Kanban = () => {
const [isShowSettings, setShowSettings] = useState(false);
- const { metadata, store } = useMetadataView();
+ const { metadata, store, renameFileCallback, deleteFilesCallback } = useMetadataView();
const columns = useMemo(() => metadata.view.columns, [metadata.view.columns]);
- const modifyRecord = useCallback((rowId, updates, oldRowData, originalUpdates, originalOldRowData) => {
+ const modifyRecord = useCallback((rowId, updates, oldRowData, originalUpdates, originalOldRowData, { success_callback }) => {
const rowIds = [rowId];
const idRowUpdates = { [rowId]: updates };
const idOriginalRowUpdates = { [rowId]: originalUpdates };
const idOldRowData = { [rowId]: oldRowData };
const idOriginalOldRowData = { [rowId]: originalOldRowData };
- store.modifyRecords(rowIds, idRowUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, false, false, {
+ const isRename = store.checkIsRenameFileOperator(rowIds, idOriginalRowUpdates);
+ let newName = null;
+ if (isRename) {
+ const rowId = rowIds[0];
+ const row = getRowById(metadata, rowId);
+ const rowUpdates = idOriginalRowUpdates[rowId];
+ const { _parent_dir, _name } = row;
+ newName = getFileNameFromRecord(rowUpdates);
+ const { isValid, errMessage } = validateName(newName);
+ if (!isValid) {
+ toaster.danger(errMessage);
+ return;
+ }
+ if (newName === _name) {
+ return;
+ }
+ if (store.checkDuplicatedName(newName, _parent_dir)) {
+ 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;
+ }
+ }
+ store.modifyRecords(rowIds, idRowUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, false, isRename, {
fail_callback: (error) => {
error && toaster.danger(error);
},
- success_callback: () => {
+ success_callback: (operation) => {
+ if (operation.is_rename) {
+ const rowId = operation.row_ids[0];
+ const row = getRowById(metadata, rowId);
+ const rowUpdates = operation.id_original_row_updates[rowId];
+ const oldRow = operation.id_original_old_row_data[rowId];
+ const parentDir = getParentDirFromRecord(row);
+ const oldName = getFileNameFromRecord(oldRow);
+ const path = Utils.joinPath(parentDir, oldName);
+ const newName = getFileNameFromRecord(rowUpdates);
+ renameFileCallback(path, newName);
+ success_callback && success_callback();
+ }
const eventBus = window.sfMetadataContext.eventBus;
eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, rowId, updates);
},
});
- }, [store]);
+ }, [store, metadata, renameFileCallback]);
+
+ const deleteRecords = useCallback((recordsIds, { success_callback }) => {
+ if (!Array.isArray(recordsIds) || recordsIds.length === 0) return;
+ let paths = [];
+ let fileNames = [];
+ recordsIds.forEach((recordId) => {
+ const record = getRowById(metadata, recordId);
+ const { _parent_dir, _name } = record || {};
+ if (_parent_dir && _name) {
+ const path = Utils.joinPath(_parent_dir, _name);
+ paths.push(path);
+ fileNames.push(_name);
+ }
+ });
+ store.deleteRecords(recordsIds, {
+ fail_callback: (error) => {
+ toaster.danger(error);
+ },
+ success_callback: () => {
+ deleteFilesCallback(paths, fileNames);
+ let msg = fileNames.length > 1
+ ? gettext('Successfully deleted {name} and {n} other items')
+ : gettext('Successfully deleted {name}');
+ msg = msg.replace('{name}', fileNames[0])
+ .replace('{n}', fileNames.length - 1);
+ toaster.success(msg);
+ success_callback && success_callback();
+ },
+ });
+ }, [metadata, store, deleteFilesCallback]);
const modifySettings = useCallback((newSettings) => {
store.modifySettings(newSettings);
@@ -54,18 +123,25 @@ const Kanban = () => {
}, [isShowSettings]);
return (
-
-
-
- {isShowSettings && (
-
- )}
+
+
+
+
+ {isShowSettings && (
+
+ )}
+
);
diff --git a/frontend/src/metadata/views/table/context-menu/index.js b/frontend/src/metadata/views/table/context-menu/index.js
index 4e2f89df42..cae7d7f9be 100644
--- a/frontend/src/metadata/views/table/context-menu/index.js
+++ b/frontend/src/metadata/views/table/context-menu/index.js
@@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import toaster from '../../../../components/toast';
-import { gettext, siteRoot } from '../../../../utils/constants';
+import { gettext } from '../../../../utils/constants';
import { Utils } from '../../../../utils/utils';
import { useMetadataView } from '../../../hooks/metadata-view';
import { useMetadataStatus } from '../../../../hooks';
@@ -12,6 +12,7 @@ import { getFileNameFromRecord, getParentDirFromRecord, getFileObjIdFromRecord,
getRecordIdFromRecord,
} from '../../../utils/cell';
import FileTagsDialog from '../../../components/dialog/file-tags-dialog';
+import { openInNewTab, openParentFolder } from '../../../utils/file';
const OPERATION = {
CLEAR_SELECTED: 'clear-selected',
@@ -175,32 +176,6 @@ const ContextMenu = (props) => {
}
}, [menuRef, visible]);
- const onOpenFileInNewTab = useCallback((record) => {
- const repoID = window.sfMetadataStore.repoId;
- const isFolder = checkIsDir(record);
- const parentDir = getParentDirFromRecord(record);
- const fileName = getFileNameFromRecord(record);
-
- const url = isFolder ?
- window.location.origin + window.location.pathname + Utils.encodePath(Utils.joinPath(parentDir, fileName)) :
- `${siteRoot}lib/${repoID}/file${Utils.encodePath(Utils.joinPath(parentDir, fileName))}`;
-
- window.open(url, '_blank');
- }, []);
-
- const onOpenParentFolder = useCallback((event, record) => {
- event.preventDefault();
- event.stopPropagation();
- let parentDir = getParentDirFromRecord(record);
-
- if (window.location.pathname.endsWith('/')) {
- parentDir = parentDir.slice(1);
- }
-
- const url = window.location.origin + window.location.pathname + Utils.encodePath(parentDir);
- window.open(url, '_blank');
- }, []);
-
const generateDescription = useCallback((record) => {
const descriptionColumnKey = PRIVATE_COLUMN_KEY.FILE_DESCRIPTION;
let path = '';
@@ -325,17 +300,18 @@ const ContextMenu = (props) => {
const handleOptionClick = useCallback((event, option) => {
event.stopPropagation();
+ const repoID = window.sfMetadataStore.repoId;
switch (option.value) {
case OPERATION.OPEN_IN_NEW_TAB: {
const { record } = option;
- if (!record) break;
- onOpenFileInNewTab(record);
+ openInNewTab(repoID, record);
break;
}
case OPERATION.OPEN_PARENT_FOLDER: {
+ event.preventDefault();
+ event.stopPropagation();
const { record } = option;
- if (!record) break;
- onOpenParentFolder(event, record);
+ openParentFolder(record);
break;
}
case OPERATION.COPY_SELECTED: {
@@ -413,7 +389,7 @@ const ContextMenu = (props) => {
}
}
setVisible(false);
- }, [onOpenFileInNewTab, onOpenParentFolder, onCopySelected, onClearSelected, generateDescription, imageCaption, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateFileDetails, toggleFileTagsRecord]);
+ }, [onCopySelected, onClearSelected, generateDescription, imageCaption, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateFileDetails, toggleFileTagsRecord]);
const getMenuPosition = useCallback((x = 0, y = 0) => {
let menuStyles = {
diff --git a/frontend/src/metadata/views/table/table-main/records/record/cell/operation-btn/file-name-operation-btn/index.js b/frontend/src/metadata/views/table/table-main/records/record/cell/operation-btn/file-name-operation-btn/index.js
index a71c0811a9..64c4de8b5f 100644
--- a/frontend/src/metadata/views/table/table-main/records/record/cell/operation-btn/file-name-operation-btn/index.js
+++ b/frontend/src/metadata/views/table/table-main/records/record/cell/operation-btn/file-name-operation-btn/index.js
@@ -5,7 +5,7 @@ import { IconBtn } from '@seafile/sf-metadata-ui-component';
import { gettext } from '../../../../../../../../../utils/constants';
import { EVENT_BUS_TYPE as METADATA_EVENT_BUS_TYPE, EDITOR_TYPE } from '../../../../../../../../constants';
import { checkIsDir } from '../../../../../../../../utils/row';
-import { openFile } from '../../../../../../../../utils/open-file';
+import { openFile } from '../../../../../../../../utils/file';
import './index.css';
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 3a1176c6fc..d4c15821d3 100644
--- a/frontend/src/tag/views/tag-files/tag-file/index.js
+++ b/frontend/src/tag/views/tag-files/tag-file/index.js
@@ -9,7 +9,7 @@ import { getParentDirFromRecord, getRecordIdFromRecord, getFileNameFromRecord, g
getFileMTimeFromRecord, getTagsFromRecord, getFilePathByRecord,
} from '../../../../metadata/utils/cell';
import { Utils } from '../../../../utils/utils';
-import { openFile } from '../../../../metadata/utils/open-file';
+import { openFile } from '../../../../metadata/utils/file';
import './index.css';