mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-13 05:39:59 +00:00
add context menu to kanban (#7176)
* add context menu to kanban * optimize code * fix: download error, rename error, ... * feat: optimize code * feat: support download folder * feat: optimize delete file callback * feat: op permission * feat: rename op * fix: url // --------- Co-authored-by: zhouwenxuan <aries@Mac.local> Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
114
frontend/src/metadata/components/context-menu/index.js
Normal file
114
frontend/src/metadata/components/context-menu/index.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const ContextMenu = ({ options, boundaryCoordinates, onOptionClick, ignoredTriggerElements }) => {
|
||||
const menuRef = useRef(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
|
||||
const handleHide = useCallback((event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
setVisible(false);
|
||||
}
|
||||
}, [menuRef]);
|
||||
|
||||
const getMenuPosition = useCallback((x = 0, y = 0) => {
|
||||
let menuStyles = {
|
||||
top: y,
|
||||
left: x
|
||||
};
|
||||
if (!menuRef.current) return menuStyles;
|
||||
const rect = menuRef.current.getBoundingClientRect();
|
||||
const { top: boundaryTop, right: boundaryRight, bottom: boundaryBottom, left: boundaryLeft } = boundaryCoordinates || {};
|
||||
menuStyles.top = menuStyles.top - boundaryTop;
|
||||
menuStyles.left = menuStyles.left - boundaryLeft;
|
||||
|
||||
if (y + rect.height > boundaryBottom - 10) {
|
||||
menuStyles.top -= rect.height;
|
||||
}
|
||||
if (x + rect.width > boundaryRight) {
|
||||
menuStyles.left -= rect.width;
|
||||
}
|
||||
if (menuStyles.top < 0) {
|
||||
menuStyles.top = rect.bottom > boundaryBottom ? (boundaryBottom - 10 - rect.height) / 2 : 0;
|
||||
}
|
||||
if (menuStyles.left < 0) {
|
||||
menuStyles.left = rect.width < boundaryRight ? (boundaryRight - rect.width) / 2 : 0;
|
||||
}
|
||||
return menuStyles;
|
||||
}, [boundaryCoordinates]);
|
||||
|
||||
const handleOptionClick = useCallback((event, option) => {
|
||||
event.stopPropagation();
|
||||
onOptionClick(option);
|
||||
setVisible(false);
|
||||
}, [onOptionClick]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleShow = (event) => {
|
||||
event.preventDefault();
|
||||
if (menuRef.current && menuRef.current.contains(event.target)) return;
|
||||
|
||||
if (ignoredTriggerElements && !ignoredTriggerElements.some(target => event.target.closest(target))) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
const position = getMenuPosition(event.clientX, event.clientY);
|
||||
setPosition(position);
|
||||
};
|
||||
|
||||
document.addEventListener('contextmenu', handleShow);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('contextmenu', handleShow);
|
||||
};
|
||||
}, [getMenuPosition, ignoredTriggerElements]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
document.addEventListener('mousedown', handleHide);
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleHide);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleHide);
|
||||
};
|
||||
}, [visible, handleHide]);
|
||||
|
||||
if (!visible) return null;
|
||||
if (options.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="dropdown-menu sf-metadata-contextmenu"
|
||||
style={position}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="dropdown-item sf-metadata-contextmenu-item"
|
||||
onClick={(event) => handleOptionClick(event, option)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ContextMenu.propTypes = {
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
boundaryCoordinates: PropTypes.object,
|
||||
ignoredTriggerElements: PropTypes.array,
|
||||
onOptionClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
@@ -0,0 +1,85 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Alert } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import { validateName } from '../../../../utils/utils';
|
||||
import { isEnter } from '../../../utils/hotkey';
|
||||
|
||||
const RenameDialog = ({ isDir, oldName, onSubmit, onCancel }) => {
|
||||
const [newName, setNewName] = useState('');
|
||||
const [errMessage, setErrMessage] = useState('');
|
||||
const [isSubmitBtnActive, setIsSubmitBtnActive] = useState(false);
|
||||
const newInput = useRef(null);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const value = e.target.value.trim();
|
||||
setNewName(value);
|
||||
setIsSubmitBtnActive(!!value);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const { isValid, errMessage } = validateName(newName);
|
||||
if (!isValid) {
|
||||
setErrMessage(errMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(newName);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (isEnter(e)) {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const onAfterModelOpened = () => {
|
||||
if (!newInput.current) return;
|
||||
newInput.current.focus();
|
||||
if (!isDir) {
|
||||
const endIndex = oldName.lastIndexOf('.');
|
||||
if (endIndex !== -1) {
|
||||
newInput.current.setSelectionRange(0, endIndex, 'forward');
|
||||
} else {
|
||||
newInput.current.setSelectionRange(0, oldName.length, 'forward');
|
||||
}
|
||||
} else {
|
||||
newInput.current.setSelectionRange(0, -1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setNewName(oldName);
|
||||
}, [oldName]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} toggle={onCancel} onOpened={onAfterModelOpened}>
|
||||
<ModalHeader toggle={onCancel}>
|
||||
{isDir ? gettext('Rename Folder') : gettext('Rename File')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{isDir ? gettext('New folder name') : gettext('New file name')}</p>
|
||||
<Input
|
||||
onKeyDown={handleKeyDown}
|
||||
innerRef={newInput}
|
||||
value={newName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{errMessage && <Alert color="danger" className="mt-2">{errMessage}</Alert>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={onCancel}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={handleSubmit} disabled={!isSubmitBtnActive}>{gettext('Submit')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
RenameDialog.propTypes = {
|
||||
isDir: PropTypes.bool.isRequired,
|
||||
oldName: PropTypes.string.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RenameDialog;
|
@@ -2,6 +2,7 @@ import { getFileNameFromRecord, getParentDirFromRecord } from './cell';
|
||||
import { checkIsDir } from './row';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { siteRoot } from '../../utils/constants';
|
||||
import URLDecorator from '../../utils/url-decorator';
|
||||
|
||||
const FILE_TYPE = {
|
||||
FOLDER: 'folder',
|
||||
@@ -99,3 +100,34 @@ export const openFile = (repoID, record, _openImage = () => {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const openInNewTab = (repoID, record) => {
|
||||
if (!record) return;
|
||||
const isDir = checkIsDir(record);
|
||||
const parentDir = getParentDirFromRecord(record);
|
||||
const fileName = getFileNameFromRecord(record);
|
||||
const url = isDir
|
||||
? 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');
|
||||
};
|
||||
|
||||
export const openParentFolder = (record) => {
|
||||
if (!record) return;
|
||||
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');
|
||||
};
|
||||
|
||||
export const downloadFile = (repoID, record) => {
|
||||
if (!repoID || !record) return;
|
||||
if (checkIsDir(record)) return;
|
||||
const parentDir = _getParentDir(record);
|
||||
const name = getFileNameFromRecord(record);
|
||||
const direntPath = Utils.joinPath(parentDir, name);
|
||||
const url = URLDecorator.getUrl({ type: 'download_file_url', repoID: repoID, filePath: direntPath });
|
||||
location.href = url;
|
||||
};
|
@@ -26,7 +26,7 @@ const updateTableRowsWithRowsData = (tables, tableId, recordsData = []) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const checkIsDir = (record) => {
|
||||
const checkIsDir = (record) => {
|
||||
if (!record) return false;
|
||||
const isDir = record[PRIVATE_COLUMN_KEY.IS_DIR];
|
||||
if (typeof isDir === 'string') {
|
||||
@@ -38,4 +38,5 @@ export const checkIsDir = (record) => {
|
||||
export {
|
||||
isTableRows,
|
||||
updateTableRowsWithRowsData,
|
||||
checkIsDir,
|
||||
};
|
||||
|
@@ -16,7 +16,7 @@ const Content = ({
|
||||
onImageSelect,
|
||||
onImageClick,
|
||||
onImageDoubleClick,
|
||||
onImageRightClick
|
||||
onContextMenu
|
||||
}) => {
|
||||
const containerRef = useRef(null);
|
||||
const imageRef = useRef(null);
|
||||
@@ -136,7 +136,7 @@ const Content = ({
|
||||
size={size}
|
||||
onClick={(e) => onImageClick(e, img)}
|
||||
onDoubleClick={(e) => onImageDoubleClick(e, img)}
|
||||
onContextMenu={(e) => onImageRightClick(e, img)}
|
||||
onContextMenu={(e) => onContextMenu(e, img)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -144,7 +144,7 @@ const Content = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [overScan, columns, size, imageHeight, mode, selectedImageIds, onImageClick, onImageDoubleClick, onImageRightClick]);
|
||||
}, [overScan, columns, size, imageHeight, mode, selectedImageIds, onImageClick, onImageDoubleClick, onContextMenu]);
|
||||
|
||||
if (!Array.isArray(groups) || groups.length === 0) {
|
||||
return <EmptyTip text={gettext('No record')}/>;
|
||||
@@ -191,7 +191,7 @@ Content.propTypes = {
|
||||
onImageSelect: PropTypes.func.isRequired,
|
||||
onImageClick: PropTypes.func.isRequired,
|
||||
onImageDoubleClick: PropTypes.func.isRequired,
|
||||
onImageRightClick: PropTypes.func.isRequired,
|
||||
onContextMenu: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Content;
|
||||
|
@@ -1,139 +1,104 @@
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useMemo, useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import './index.css';
|
||||
import ContextMenu from '../../../components/context-menu';
|
||||
import { gettext, useGoFileserver, fileServerRoot } from '../../../../utils/constants';
|
||||
import { getRowById } from '../../../utils/table';
|
||||
import { useMetadataView } from '../../../hooks/metadata-view';
|
||||
import { downloadFile } from '../../../utils/file';
|
||||
import ZipDownloadDialog from '../../../../components/dialog/zip-download-dialog';
|
||||
import metadataAPI from '../../../api';
|
||||
import toaster from '../../../../components/toast';
|
||||
import { Utils } from '../../../../utils/utils';
|
||||
import ModalPortal from '../../../../components/modal-portal';
|
||||
|
||||
const OPERATION = {
|
||||
const CONTEXT_MENU_KEY = {
|
||||
DOWNLOAD: 'download',
|
||||
DELETE: 'delete',
|
||||
};
|
||||
|
||||
const ContextMenu = ({ getContentRect, getContainerRect, onDownload, onDelete }) => {
|
||||
const menuRef = useRef(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
const GalleryContextMenu = ({ selectedImages, boundaryCoordinates, onDelete }) => {
|
||||
const [isZipDialogOpen, setIsZipDialogOpen] = useState(false);
|
||||
|
||||
const { metadata } = useMetadataView();
|
||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||
const checkCanDeleteRow = window.sfMetadataContext.checkCanDeleteRow();
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (!visible) return [];
|
||||
return [
|
||||
{ value: OPERATION.DOWNLOAD, label: gettext('Download') },
|
||||
{ value: OPERATION.DELETE, label: gettext('Delete') }
|
||||
];
|
||||
}, [visible]);
|
||||
|
||||
const handleHide = useCallback((event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
setVisible(false);
|
||||
let validOptions = [{ value: CONTEXT_MENU_KEY.DOWNLOAD, label: gettext('Download') }];
|
||||
if (checkCanDeleteRow) {
|
||||
validOptions.push({ value: CONTEXT_MENU_KEY.DELETE, label: selectedImages.length > 1 ? gettext('Delete') : gettext('Delete file') });
|
||||
}
|
||||
}, [menuRef]);
|
||||
return validOptions;
|
||||
}, [checkCanDeleteRow, selectedImages]);
|
||||
|
||||
const handleOptionClick = useCallback((event, option) => {
|
||||
event.stopPropagation();
|
||||
const closeZipDialog = () => {
|
||||
setIsZipDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
if (!selectedImages.length) return;
|
||||
if (selectedImages.length === 1) {
|
||||
const image = selectedImages[0];
|
||||
const record = getRowById(metadata, image.id);
|
||||
downloadFile(repoID, record);
|
||||
return;
|
||||
}
|
||||
if (!useGoFileserver) {
|
||||
setIsZipDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
const dirents = selectedImages.map(image => {
|
||||
const value = image.path === '/' ? image.name : `${image.path}/${image.name}`;
|
||||
return value;
|
||||
});
|
||||
metadataAPI.zipDownload(repoID, '/', dirents).then((res) => {
|
||||
const zipToken = res.data['zip_token'];
|
||||
location.href = `${fileServerRoot}zip/${zipToken}`;
|
||||
}).catch(error => {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}, [repoID, metadata, selectedImages]);
|
||||
|
||||
const handleOptionClick = useCallback(option => {
|
||||
switch (option.value) {
|
||||
case OPERATION.DOWNLOAD: {
|
||||
onDownload && onDownload();
|
||||
case 'download':
|
||||
handleDownload();
|
||||
break;
|
||||
}
|
||||
case OPERATION.DELETE: {
|
||||
onDelete && onDelete();
|
||||
case 'delete':
|
||||
onDelete(selectedImages);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
setVisible(false);
|
||||
}, [onDownload, onDelete]);
|
||||
|
||||
const getMenuPosition = useCallback((x = 0, y = 0) => {
|
||||
let menuStyles = {
|
||||
top: y,
|
||||
left: x
|
||||
};
|
||||
if (!menuRef.current) return menuStyles;
|
||||
const rect = menuRef.current.getBoundingClientRect();
|
||||
const containerRect = getContainerRect();
|
||||
const { right: innerWidth, bottom: innerHeight } = getContentRect();
|
||||
menuStyles.top = menuStyles.top - containerRect.top;
|
||||
menuStyles.left = menuStyles.left - containerRect.left;
|
||||
|
||||
if (y + rect.height > innerHeight - 10) {
|
||||
menuStyles.top -= rect.height;
|
||||
}
|
||||
if (x + rect.width > innerWidth) {
|
||||
menuStyles.left -= rect.width;
|
||||
}
|
||||
if (menuStyles.top < 0) {
|
||||
menuStyles.top = rect.bottom > innerHeight ? (innerHeight - 10 - rect.height) / 2 : 0;
|
||||
}
|
||||
if (menuStyles.left < 0) {
|
||||
menuStyles.left = rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
|
||||
}
|
||||
return menuStyles;
|
||||
}, [getContentRect, getContainerRect]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleShow = (event) => {
|
||||
event.preventDefault();
|
||||
if (menuRef.current && menuRef.current.contains(event.target)) return;
|
||||
|
||||
if (event.target.tagName.toLowerCase() !== 'img') {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
|
||||
const position = getMenuPosition(event.clientX, event.clientY);
|
||||
setPosition(position);
|
||||
};
|
||||
|
||||
document.addEventListener('contextmenu', handleShow);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('contextmenu', handleShow);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
document.addEventListener('mousedown', handleHide);
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleHide);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleHide);
|
||||
};
|
||||
}, [visible, handleHide]);
|
||||
|
||||
if (!visible) return null;
|
||||
if (options.length === 0) return null;
|
||||
}, [selectedImages, handleDownload, onDelete]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className='dropdown-menu sf-metadata-contextmenu'
|
||||
style={position}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className='dropdown-item sf-metadata-contextmenu-item'
|
||||
onClick={(event) => handleOptionClick(event, option)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<ContextMenu
|
||||
options={options}
|
||||
boundaryCoordinates={boundaryCoordinates}
|
||||
ignoredTriggerElements={['.metadata-gallery-image-item', '.metadata-gallery-grid-image']}
|
||||
onOptionClick={handleOptionClick}
|
||||
/>
|
||||
{isZipDialogOpen && (
|
||||
<ModalPortal>
|
||||
<ZipDownloadDialog
|
||||
repoID={repoID}
|
||||
path="/"
|
||||
target={selectedImages.map(image => image.path === '/' ? image.name : `${image.path}/${image.name}`)}
|
||||
toggleDialog={closeZipDialog}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ContextMenu.propTypes = {
|
||||
getContentRect: PropTypes.func.isRequired,
|
||||
getContainerRect: PropTypes.func.isRequired,
|
||||
onDownload: PropTypes.func,
|
||||
GalleryContextMenu.propTypes = {
|
||||
selectedImages: PropTypes.array,
|
||||
boundaryCoordinates: PropTypes.object,
|
||||
onDelete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
||||
export default GalleryContextMenu;
|
||||
|
@@ -1,21 +1,17 @@
|
||||
import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||
import metadataAPI from '../../api';
|
||||
import URLDecorator from '../../../utils/url-decorator';
|
||||
import toaster from '../../../components/toast';
|
||||
import Content from './content';
|
||||
import ContextMenu from './context-menu';
|
||||
import ImageDialog from '../../../components/dialog/image-dialog';
|
||||
import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog';
|
||||
import ModalPortal from '../../../components/modal-portal';
|
||||
import { useMetadataView } from '../../hooks/metadata-view';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell';
|
||||
import { siteRoot, fileServerRoot, useGoFileserver, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
|
||||
import { siteRoot, fileServerRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
|
||||
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants';
|
||||
import { getRowById } from '../../utils/table';
|
||||
import { getEventClassName } from '../../utils/common';
|
||||
import GalleryContextmenu from './context-menu';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -28,7 +24,6 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||
const [overScan, setOverScan] = useState({ top: 0, bottom: 0 });
|
||||
const [mode, setMode] = useState(GALLERY_DATE_MODE.DAY);
|
||||
const [isImagePopupOpen, setIsImagePopupOpen] = useState(false);
|
||||
const [isZipDialogOpen, setIsZipDialogOpen] = useState(false);
|
||||
const [imageIndex, setImageIndex] = useState(0);
|
||||
const [selectedImages, setSelectedImages] = useState([]);
|
||||
|
||||
@@ -267,7 +262,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||
setIsImagePopupOpen(true);
|
||||
}, [imageItems]);
|
||||
|
||||
const handleRightClick = useCallback((event, image) => {
|
||||
const handleContextMenu = useCallback((event, image) => {
|
||||
event.preventDefault();
|
||||
const index = imageItems.findIndex(item => item.id === image.id);
|
||||
if (isNaN(index) || index === -1) return;
|
||||
@@ -291,46 +286,17 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||
setSelectedImages(selectedImages);
|
||||
}, []);
|
||||
|
||||
const closeImagePopup = () => {
|
||||
const closeImagePopup = useCallback(() => {
|
||||
setIsImagePopupOpen(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
if (!selectedImages.length) return;
|
||||
if (selectedImages.length === 1) {
|
||||
const image = selectedImages[0];
|
||||
let direntPath = image.path === '/' ? image.name : Utils.joinPath(image.path, image.name);
|
||||
let url = URLDecorator.getUrl({ type: 'download_file_url', repoID: repoID, filePath: direntPath });
|
||||
location.href = url;
|
||||
return;
|
||||
}
|
||||
if (!useGoFileserver) {
|
||||
setIsZipDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
const dirents = selectedImages.map(image => {
|
||||
const value = image.path === '/' ? image.name : `${image.path}/${image.name}`;
|
||||
return value;
|
||||
});
|
||||
metadataAPI.zipDownload(repoID, '/', dirents).then((res) => {
|
||||
const zipToken = res.data['zip_token'];
|
||||
location.href = `${fileServerRoot}zip/${zipToken}`;
|
||||
}).catch(error => {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}, [repoID, selectedImages]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
const handleDeleteSelectedImages = useCallback((selectedImages) => {
|
||||
if (!selectedImages.length) return;
|
||||
onDelete(selectedImages, () => {
|
||||
updateCurrentDirent();
|
||||
setSelectedImages([]);
|
||||
});
|
||||
}, [selectedImages, onDelete]);
|
||||
|
||||
const closeZipDialog = () => {
|
||||
setIsZipDialogOpen(false);
|
||||
};
|
||||
}, [onDelete, updateCurrentDirent]);
|
||||
|
||||
const handleClickOutside = useCallback((event) => {
|
||||
const className = getEventClassName(event);
|
||||
@@ -385,7 +351,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||
onImageSelect={handleImageSelection}
|
||||
onImageClick={handleClick}
|
||||
onImageDoubleClick={handleDoubleClick}
|
||||
onImageRightClick={handleRightClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
/>
|
||||
{isLoadingMore &&
|
||||
<div className="sf-metadata-gallery-loading-more">
|
||||
@@ -395,11 +361,10 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ContextMenu
|
||||
getContentRect={() => containerRef.current.getBoundingClientRect()}
|
||||
getContainerRect={() => containerRef.current.getBoundingClientRect()}
|
||||
onDownload={handleDownload}
|
||||
onDelete={handleDelete}
|
||||
<GalleryContextmenu
|
||||
selectedImages={selectedImages}
|
||||
boundaryCoordinates={containerRef?.current?.getBoundingClientRect() || {}}
|
||||
onDelete={handleDeleteSelectedImages}
|
||||
/>
|
||||
{isImagePopupOpen && (
|
||||
<ModalPortal>
|
||||
@@ -413,16 +378,6 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{isZipDialogOpen && (
|
||||
<ModalPortal>
|
||||
<ZipDownloadDialog
|
||||
repoID={repoID}
|
||||
path={'/'}
|
||||
target={selectedImages.map(image => image.path === '/' ? image.name : `${image.path}/${image.name}`)}
|
||||
toggleDialog={closeZipDialog}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -17,6 +17,7 @@ const Card = ({
|
||||
displayColumns,
|
||||
onOpenFile,
|
||||
onSelectCard,
|
||||
onContextMenu,
|
||||
}) => {
|
||||
const titleValue = getCellValueByColumn(record, titleColumn);
|
||||
|
||||
@@ -41,6 +42,7 @@ const Card = ({
|
||||
data-id={record._id}
|
||||
className={classnames('sf-metadata-kanban-card', { 'selected': isSelected })}
|
||||
onClick={handleClickCard}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
{titleColumn && (
|
||||
<div className="sf-metadata-kanban-card-header" onClick={handleClickFilename}>
|
||||
@@ -82,6 +84,7 @@ Card.propTypes = {
|
||||
displayColumns: PropTypes.array,
|
||||
onOpenFile: PropTypes.func.isRequired,
|
||||
onSelectCard: PropTypes.func.isRequired,
|
||||
onContextMenu: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Card;
|
||||
|
@@ -28,6 +28,7 @@ const Board = ({
|
||||
onOpenFile,
|
||||
onSelectCard,
|
||||
updateDragging,
|
||||
onContextMenu,
|
||||
}) => {
|
||||
const [isDraggingOver, setDraggingOver] = useState(false);
|
||||
const boardName = useMemo(() => `sf_metadata_kanban_board_${board.key}`, [board]);
|
||||
@@ -80,7 +81,8 @@ const Board = ({
|
||||
{board.children.map((cardKey) => {
|
||||
const record = getRowById(metadata, cardKey);
|
||||
if (!record) return null;
|
||||
const isSelected = selectedCard === getRecordIdFromRecord(record);
|
||||
const recordId = getRecordIdFromRecord(record);
|
||||
const isSelected = selectedCard === recordId;
|
||||
const CardElement = (
|
||||
<Card
|
||||
key={cardKey}
|
||||
@@ -92,6 +94,7 @@ const Board = ({
|
||||
displayColumns={displayColumns}
|
||||
onOpenFile={onOpenFile}
|
||||
onSelectCard={onSelectCard}
|
||||
onContextMenu={(e) => onContextMenu(e, recordId)}
|
||||
/>
|
||||
);
|
||||
if (readonly) return CardElement;
|
||||
@@ -124,6 +127,7 @@ Board.propTypes = {
|
||||
onOpenFile: PropTypes.func.isRequired,
|
||||
onSelectCard: PropTypes.func.isRequired,
|
||||
updateDragging: PropTypes.func.isRequired,
|
||||
onContextMenu: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Board;
|
||||
|
@@ -10,16 +10,17 @@ import { checkIsPredefinedOption, getCellValueByColumn, isValidCellValue, getRec
|
||||
getFileNameFromRecord, getParentDirFromRecord
|
||||
} from '../../../utils/cell';
|
||||
import { getColumnOptions, getColumnOriginName } from '../../../utils/column';
|
||||
import { openFile } from '../../../utils/file';
|
||||
import { checkIsDir } from '../../../utils/row';
|
||||
import AddBoard from '../add-board';
|
||||
import EmptyTip from '../../../../components/empty-tip';
|
||||
import Board from './board';
|
||||
import ImagePreviewer from '../../../components/cell-formatter/image-previewer';
|
||||
import { openFile } from '../../../utils/open-file';
|
||||
import { checkIsDir } from '../../../utils/row';
|
||||
import ContextMenu from '../context-menu';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
|
||||
const Boards = ({ modifyRecord, deleteRecords, modifyColumnData, onCloseSettings }) => {
|
||||
const [haveFreezed, setHaveFreezed] = useState(false);
|
||||
const [isImagePreviewerVisible, setImagePreviewerVisible] = useState(false);
|
||||
const [selectedCard, setSelectedCard] = useState('');
|
||||
@@ -31,6 +32,9 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
|
||||
const { isDirentDetailShow, metadata, store, updateCurrentDirent, showDirentDetail } = useMetadataView();
|
||||
const { collaborators } = useCollaborators();
|
||||
|
||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
|
||||
|
||||
const groupByColumn = useMemo(() => {
|
||||
const groupByColumnKey = metadata.view.settings[KANBAN_SETTINGS_KEYS.GROUP_BY_COLUMN_KEY];
|
||||
return metadata.key_column_map[groupByColumnKey];
|
||||
@@ -216,6 +220,24 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
|
||||
setDragging(isDragging);
|
||||
}, []);
|
||||
|
||||
const onContextMenu = useCallback((event, recordId) => {
|
||||
event.preventDefault();
|
||||
setSelectedCard(recordId);
|
||||
}, []);
|
||||
|
||||
const onDeleteRecords = useCallback((recordIds) => {
|
||||
deleteRecords(recordIds, {
|
||||
success_callback: () => {
|
||||
setSelectedCard(null);
|
||||
updateCurrentDirent();
|
||||
},
|
||||
});
|
||||
}, [deleteRecords, updateCurrentDirent]);
|
||||
|
||||
const onRename = useCallback((rowId, updates, oldRowData, originalUpdates, originalOldRowData, { success_callback }) => {
|
||||
modifyRecord(rowId, updates, oldRowData, originalUpdates, originalOldRowData, { success_callback });
|
||||
}, [modifyRecord]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDirentDetailShow) {
|
||||
setSelectedCard(null);
|
||||
@@ -223,50 +245,57 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
|
||||
}, [isDirentDetailShow]);
|
||||
|
||||
const isEmpty = boards.length === 0;
|
||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classnames('sf-metadata-view-kanban-boards', {
|
||||
'sf-metadata-view-kanban-boards-text-wrap': textWrap,
|
||||
'readonly': readonly,
|
||||
})}
|
||||
onClick={handleClickOutside}
|
||||
>
|
||||
<div className="smooth-dnd-container horizontal">
|
||||
{isEmpty && (<EmptyTip className="tips-empty-boards" text={gettext('No categories')} />)}
|
||||
{!isEmpty && (
|
||||
<>
|
||||
{boards.map((board, index) => {
|
||||
return (
|
||||
<Board
|
||||
key={board.key}
|
||||
board={board}
|
||||
index={index}
|
||||
readonly={readonly}
|
||||
displayEmptyValue={displayEmptyValue}
|
||||
displayColumnName={displayColumnName}
|
||||
haveFreezed={haveFreezed}
|
||||
groupByColumn={groupByColumn}
|
||||
titleColumn={titleColumn}
|
||||
displayColumns={displayColumns}
|
||||
selectedCard={selectedCard}
|
||||
onMove={onMove}
|
||||
deleteOption={deleteOption}
|
||||
onFreezed={onFreezed}
|
||||
onUnFreezed={onUnFreezed}
|
||||
onOpenFile={onOpenFile}
|
||||
onSelectCard={onSelectCard}
|
||||
updateDragging={updateDragging}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{!readonly && (<AddBoard groupByColumn={groupByColumn}/>)}
|
||||
<>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classnames('sf-metadata-view-kanban-boards', {
|
||||
'sf-metadata-view-kanban-boards-text-wrap': textWrap,
|
||||
'readonly': readonly,
|
||||
})}
|
||||
onClick={handleClickOutside}
|
||||
>
|
||||
<div className="smooth-dnd-container horizontal">
|
||||
{isEmpty && (<EmptyTip className="tips-empty-boards" text={gettext('No categories')} />)}
|
||||
{!isEmpty && (
|
||||
<>
|
||||
{boards.map((board, index) => {
|
||||
return (
|
||||
<Board
|
||||
key={board.key}
|
||||
board={board}
|
||||
index={index}
|
||||
readonly={readonly}
|
||||
displayEmptyValue={displayEmptyValue}
|
||||
displayColumnName={displayColumnName}
|
||||
haveFreezed={haveFreezed}
|
||||
groupByColumn={groupByColumn}
|
||||
titleColumn={titleColumn}
|
||||
displayColumns={displayColumns}
|
||||
selectedCard={selectedCard}
|
||||
onMove={onMove}
|
||||
deleteOption={deleteOption}
|
||||
onFreezed={onFreezed}
|
||||
onUnFreezed={onUnFreezed}
|
||||
onOpenFile={onOpenFile}
|
||||
onSelectCard={onSelectCard}
|
||||
updateDragging={updateDragging}
|
||||
onContextMenu={onContextMenu}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{!readonly && (<AddBoard groupByColumn={groupByColumn}/>)}
|
||||
</div>
|
||||
</div>
|
||||
<ContextMenu
|
||||
boundaryCoordinates={containerRef?.current?.getBoundingClientRect()}
|
||||
selectedCard={selectedCard}
|
||||
onDelete={onDeleteRecords}
|
||||
onRename={onRename}
|
||||
/>
|
||||
{isImagePreviewerVisible && (
|
||||
<ImagePreviewer
|
||||
repoID={repoID}
|
||||
@@ -276,7 +305,7 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
|
||||
closeImagePopup={closeImagePreviewer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
163
frontend/src/metadata/views/kanban/context-menu/index.js
Normal file
163
frontend/src/metadata/views/kanban/context-menu/index.js
Normal file
@@ -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 (
|
||||
<>
|
||||
<ContextMenu
|
||||
options={options}
|
||||
boundaryCoordinates={boundaryCoordinates}
|
||||
onOptionClick={handleOptionClick}
|
||||
ignoredTriggerElements={['.sf-metadata-kanban-card']}
|
||||
/>
|
||||
{isRenameDialogShow && (
|
||||
<ModalPortal>
|
||||
<RenameDialog
|
||||
isDir={isDir}
|
||||
oldName={oldName}
|
||||
onSubmit={handleRename}
|
||||
onCancel={() => setIsRenameDialogShow(false)}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{isZipDialogOpen && (
|
||||
<ModalPortal>
|
||||
<ZipDownloadDialog repoID={repoID} path={parentDir} target={[oldName]} toggleDialog={closeZipDialog}/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
KanbanContextMenu.propTypes = {
|
||||
boundaryCoordinates: PropTypes.object,
|
||||
selectedCard: PropTypes.string,
|
||||
onDelete: PropTypes.func,
|
||||
onRename: PropTypes.func,
|
||||
};
|
||||
|
||||
export default KanbanContextMenu;
|
@@ -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 (
|
||||
<div className="sf-metadata-view-kanban">
|
||||
<Boards modifyRecord={modifyRecord} modifyColumnData={modifyColumnData} onCloseSettings={closeSettings} />
|
||||
<div className="sf-metadata-view-setting-panel sf-metadata-view-kanban-setting h-100">
|
||||
{isShowSettings && (
|
||||
<Settings
|
||||
columns={columns}
|
||||
columnsMap={metadata.key_column_map}
|
||||
settings={metadata.view.settings}
|
||||
modifySettings={modifySettings}
|
||||
onClose={closeSettings}
|
||||
/>
|
||||
)}
|
||||
<div className="sf-metadata-container">
|
||||
<div className="sf-metadata-view-kanban">
|
||||
<Boards
|
||||
modifyRecord={modifyRecord}
|
||||
deleteRecords={deleteRecords}
|
||||
modifyColumnData={modifyColumnData}
|
||||
onCloseSettings={closeSettings}
|
||||
/>
|
||||
<div className="sf-metadata-view-setting-panel sf-metadata-view-kanban-setting h-100">
|
||||
{isShowSettings && (
|
||||
<Settings
|
||||
columns={columns}
|
||||
columnsMap={metadata.key_column_map}
|
||||
settings={metadata.view.settings}
|
||||
modifySettings={modifySettings}
|
||||
onClose={closeSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -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 = {
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
||||
|
Reference in New Issue
Block a user