Skip to content

Commit

Permalink
feat(metadata-table): delete/rename folder/file via contextmenu (#6848)
Browse files Browse the repository at this point in the history
  • Loading branch information
renjie-run authored Sep 29, 2024
1 parent d0a634a commit ac9ea56
Show file tree
Hide file tree
Showing 32 changed files with 740 additions and 348 deletions.
4 changes: 4 additions & 0 deletions frontend/src/components/dir-view-mode/dir-column-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const propTypes = {
onItemSelected: PropTypes.func.isRequired,
onItemDelete: PropTypes.func.isRequired,
onItemRename: PropTypes.func.isRequired,
deleteFilesCallback: PropTypes.func,
renameFileCallback: PropTypes.func,
onItemMove: PropTypes.func.isRequired,
onItemCopy: PropTypes.func.isRequired,
onItemConvert: PropTypes.func.isRequired,
Expand Down Expand Up @@ -198,6 +200,8 @@ class DirColumnView extends React.Component {
repoID={this.props.repoID}
repoInfo={this.props.currentRepoInfo}
viewID={this.props.viewId}
deleteFilesCallback={this.props.deleteFilesCallback}
renameFileCallback={this.props.renameFileCallback}
/>
}
{currentMode === LIST_MODE &&
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/tree-view/tree-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ class TreeHelper {
renameNodeByPath(tree, nodePath, newName) {
let treeCopy = tree.clone();
let node = treeCopy.getNodeByPath(nodePath);
if (!node) {
return treeCopy;
}
treeCopy.renameNode(node, newName);
return treeCopy;
}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/metadata/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,11 @@ class MetadataManagerAPI {
* @param {string[]} dirents - Array of file/folder paths to delete
* @returns {Promise} Axios delete request promise
*/
deleteImages(repoID, dirents) {
batchDeleteFiles(repo_id, file_names) {
const url = this.server + '/api/v2.1/repos/batch-delete-folders-item/';
const data = {
repo_id: repoID,
file_names: dirents
repo_id,
file_names,
};
return this.req.delete(url, { data });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import NormalEditorContainer from './normal-editor-container';
import PopupEditorContainer from './popup-editor-container';
import PreviewEditorContainer from './preview-editor-container';
import { CellType } from '../../../constants';
import { CellType, EDITOR_TYPE } from '../../../constants';

const POPUP_EDITOR_COLUMN_TYPES = [
CellType.DATE,
Expand All @@ -18,12 +18,12 @@ const PREVIEW_EDITOR_COLUMN_TYPES = [
];

const EditorContainer = (props) => {
const { column } = props;
const { column, openEditorMode } = props;
if (!column) return null;
const { type } = column;
if (POPUP_EDITOR_COLUMN_TYPES.includes(type)) {
return <PopupEditorContainer { ...props } />;
} else if (PREVIEW_EDITOR_COLUMN_TYPES.includes(type)) {
} else if (PREVIEW_EDITOR_COLUMN_TYPES.includes(type) && openEditorMode === EDITOR_TYPE.PREVIEWER) {
return <PreviewEditorContainer { ...props } />;
} else {
return <NormalEditorContainer { ...props } />;
Expand All @@ -32,6 +32,7 @@ const EditorContainer = (props) => {

EditorContainer.propTypes = {
column: PropTypes.object,
openEditorMode: PropTypes.string,
};

export default EditorContainer;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import Editor from '../editor';

const PreviewEditorContainer = (props) => {
return (<Editor { ...props } />);
return (<Editor { ...props } mode={props.openEditorMode} />);
};

export default PreviewEditorContainer;
135 changes: 33 additions & 102 deletions frontend/src/metadata/components/cell-editors/file-name-editor.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,29 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useImperativeHandle, useRef } from 'react';
import PropTypes from 'prop-types';
import { ModalPortal } from '@seafile/sf-metadata-ui-component';
import { Utils } from '../../../utils/utils';
import ImageDialog from '../../../components/dialog/image-dialog';
import { siteRoot, thumbnailSizeForOriginal, fileServerRoot, thumbnailDefaultSize } from '../../../utils/constants';
import { PRIVATE_COLUMN_KEY } from '../../constants';
import imageAPI from '../../../utils/image-api';
import { seafileAPI } from '../../../utils/seafile-api';
import toaster from '../../../components/toast';

const FileNameEditor = ({ column, record, table, onCommitCancel }) => {
const [imageIndex, setImageIndex] = useState(0);
const [imageItems, setImageItems] = useState([]);

useEffect(() => {
const repoID = window.sfMetadataContext.getSetting('repoID');
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
const newImageItems = table.rows
.filter(row => Utils.imageCheck(row[PRIVATE_COLUMN_KEY.FILE_NAME]))
.map(item => {
const fileName = item[PRIVATE_COLUMN_KEY.FILE_NAME];
const parentDir = item[PRIVATE_COLUMN_KEY.PARENT_DIR];
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
const fileExt = fileName.substr(fileName.lastIndexOf('.') + 1).toLowerCase();
const isGIF = fileExt === 'gif';
const useThumbnail = repoInfo?.encrypted;
const basePath = `${siteRoot}${useThumbnail && !isGIF ? 'thumbnail' : 'repo'}/${repoID}`;
const src = `${basePath}/${useThumbnail && !isGIF ? thumbnailSizeForOriginal : 'raw'}${path}`;
return {
name: fileName,
url: `${siteRoot}lib/${repoID}/file${path}`,
thumbnail: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`,
src: src,
downloadURL: `${fileServerRoot}repos/${repoID}/files${path}/?op=download`,
};
});
setImageItems(newImageItems);
}, [table]);
import { EDITOR_TYPE } from '../../constants';
import ImagePreviewer from '../cell-formatter/image-previewer';
import TextEditor from './text-editor';
import { checkIsDir } from '../../utils/row';

const FileNameEditor = React.forwardRef((props, ref) => {
const { column, record, mode } = props;
const textEditorRef = useRef(null);

useImperativeHandle(ref, () => {
return textEditorRef.current;
});

const getFileName = () => {
const { key } = column;
return record[key];
};

useEffect(() => {
if (imageItems.length > 0) {
const index = imageItems.findIndex(item => item.name === record[PRIVATE_COLUMN_KEY.FILE_NAME]);
if (index > -1) setImageIndex(index);
const getFileType = () => {
if (checkIsDir(record)) {
return 'folder';
}
}, [imageItems, record]);

const _isDir = useMemo(() => {
const isDirValue = record[PRIVATE_COLUMN_KEY.IS_DIR];
if (typeof isDirValue === 'string') return isDirValue.toUpperCase() === 'TRUE';
return isDirValue;
}, [record]);

const fileName = useMemo(() => {
return record[column.key];
}, [column, record]);

const fileType = useMemo(() => {
if (_isDir) return 'folder';
const fileName = getFileName();
if (!fileName) return '';
const index = fileName.lastIndexOf('.');
if (index === -1) return '';
Expand All @@ -66,63 +33,27 @@ const FileNameEditor = ({ column, record, table, onCommitCancel }) => {
if (Utils.isMarkdownFile(fileName)) return 'markdown';
if (Utils.isSdocFile(fileName)) return 'sdoc';
return '';
}, [_isDir, fileName]);

const moveToPrevImage = () => {
const imageItemsLength = imageItems.length;
setImageIndex((prevState) => (prevState + imageItemsLength - 1) % imageItemsLength);
};

const moveToNextImage = () => {
const imageItemsLength = imageItems.length;
setImageIndex((prevState) => (prevState + 1) % imageItemsLength);
};

const rotateImage = (imageIndex, angle) => {
if (imageIndex >= 0 && angle !== 0) {
const repoID = window.sfMetadataContext.getSetting('repoID');
const imageItem = imageItems[imageIndex];
const path = imageItem.url.slice(imageItem.url.indexOf('/file/') + 5);
imageAPI.rotateImage(repoID, path, 360 - angle).then((res) => {
if (res.data?.success) {
seafileAPI.createThumbnail(repoID, path, thumbnailDefaultSize).then((res) => {
if (res.data?.encoded_thumbnail_src) {
const cacheBuster = new Date().getTime();
const newThumbnailSrc = `${res.data.encoded_thumbnail_src}?t=${cacheBuster}`;
imageItems[imageIndex].src = newThumbnailSrc;
setImageItems(imageItems);
}
}).catch(error => {
toaster.danger(Utils.getErrorMsg(error));
});
}
}).catch(error => {
toaster.danger(Utils.getErrorMsg(error));
});
if (mode === EDITOR_TYPE.PREVIEWER) {
const fileType = getFileType();
if (fileType === 'image') {
return (
<ImagePreviewer {...props} closeImagePopup={props.onCommitCancel} />
);
}
};

if (fileType === 'image') {
return (
<ModalPortal>
<ImageDialog
imageItems={imageItems}
imageIndex={imageIndex}
closeImagePopup={onCommitCancel}
moveToPrevImage={moveToPrevImage}
moveToNextImage={moveToNextImage}
onRotateImage={rotateImage}
/>
</ModalPortal>
);
return null;
}

return null;
};
return (<TextEditor ref={textEditorRef} { ...props } readOnly={false} />);
});

FileNameEditor.propTypes = {
table: PropTypes.object,
column: PropTypes.object,
record: PropTypes.object,
mode: PropTypes.string,
onCommitCancel: PropTypes.func,
};

Expand Down
104 changes: 104 additions & 0 deletions frontend/src/metadata/components/cell-formatter/image-previewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { ModalPortal } from '@seafile/sf-metadata-ui-component';
import toaster from '../../../components/toast';
import ImageDialog from '../../../components/dialog/image-dialog';
import imageAPI from '../../../utils/image-api';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import { siteRoot, thumbnailSizeForOriginal, fileServerRoot, thumbnailDefaultSize } from '../../../utils/constants';
import { getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell';

const ImagePreviewer = (props) => {
const { record, table, closeImagePopup } = props;
const [imageIndex, setImageIndex] = useState(0);
const [imageItems, setImageItems] = useState([]);

useEffect(() => {
const repoID = window.sfMetadataContext.getSetting('repoID');
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
const newImageItems = table.rows
.filter((row) => Utils.imageCheck(getFileNameFromRecord(row)))
.map((row) => {
const fileName = getFileNameFromRecord(row);
const parentDir = getParentDirFromRecord(row);
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
const fileExt = fileName.substr(fileName.lastIndexOf('.') + 1).toLowerCase();
const isGIF = fileExt === 'gif';
const useThumbnail = repoInfo?.encrypted;
const basePath = `${siteRoot}${useThumbnail && !isGIF ? 'thumbnail' : 'repo'}/${repoID}`;
const src = `${basePath}/${useThumbnail && !isGIF ? thumbnailSizeForOriginal : 'raw'}${path}`;
return {
name: fileName,
url: `${siteRoot}lib/${repoID}/file${path}`,
thumbnail: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`,
src: src,
downloadURL: `${fileServerRoot}repos/${repoID}/files${path}/?op=download`,
};
});
setImageItems(newImageItems);
}, [table]);

useEffect(() => {
if (imageItems.length > 0) {
const index = imageItems.findIndex(item => item.name === getFileNameFromRecord(record));
if (index > -1) setImageIndex(index);
}
}, [imageItems, record]);

const moveToPrevImage = () => {
const imageItemsLength = imageItems.length;
setImageIndex((prevState) => (prevState + imageItemsLength - 1) % imageItemsLength);
};

const moveToNextImage = () => {
const imageItemsLength = imageItems.length;
setImageIndex((prevState) => (prevState + 1) % imageItemsLength);
};

const rotateImage = (imageIndex, angle) => {
if (imageIndex >= 0 && angle !== 0) {
const repoID = window.sfMetadataContext.getSetting('repoID');
const imageItem = imageItems[imageIndex];
const path = imageItem.url.slice(imageItem.url.indexOf('/file/') + 5);
imageAPI.rotateImage(repoID, path, 360 - angle).then((res) => {
if (res.data?.success) {
seafileAPI.createThumbnail(repoID, path, thumbnailDefaultSize).then((res) => {
if (res.data?.encoded_thumbnail_src) {
const cacheBuster = new Date().getTime();
const newThumbnailSrc = `${res.data.encoded_thumbnail_src}?t=${cacheBuster}`;
imageItems[imageIndex].src = newThumbnailSrc;
setImageItems(imageItems);
}
}).catch(error => {
toaster.danger(Utils.getErrorMsg(error));
});
}
}).catch(error => {
toaster.danger(Utils.getErrorMsg(error));
});
}
};

return (
<ModalPortal>
<ImageDialog
imageItems={imageItems}
imageIndex={imageIndex}
closeImagePopup={closeImagePopup}
moveToPrevImage={moveToPrevImage}
moveToNextImage={moveToNextImage}
onRotateImage={rotateImage}
/>
</ModalPortal>
);
};

ImagePreviewer.propTypes = {
table: PropTypes.object,
column: PropTypes.object,
record: PropTypes.object,
closeImagePopup: PropTypes.func,
};

export default ImagePreviewer;
4 changes: 2 additions & 2 deletions frontend/src/metadata/components/metadata-details/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import DetailItem from '../../../components/dirent-detail/detail-item';
import { Utils } from '../../../utils/utils';
import metadataAPI from '../../api';
import Column from '../../model/metadata/column';
import { getCellValueByColumn, getOptionName, getColumnOptionNamesByIds, getColumnOptionNameById } from '../../utils/cell';
import { getCellValueByColumn, getOptionName, getColumnOptionNamesByIds, getColumnOptionNameById, getFileNameFromRecord } from '../../utils/cell';
import { normalizeFields } from './utils';
import { gettext } from '../../../utils/constants';
import { CellType, PREDEFINED_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../../constants';
Expand Down Expand Up @@ -102,7 +102,7 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType }) => {
if (isLoading) return null;
const { fields, record } = metadata;
if (!record._id) return null;
const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME];
const fileName = getFileNameFromRecord(record);
const isImage = record && (Utils.imageCheck(fileName) || Utils.videoCheck(fileName));
return (
<>
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/metadata/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ class Context {
return true;
};

checkCanDeleteRow = () => {
if (this.permission === 'r') return false;
return true;
};

canModifyRows = () => {
if (this.permission === 'r') return false;
return true;
Expand Down Expand Up @@ -189,6 +194,10 @@ class Context {
return this.metadataAPI.modifyRecords(repoId, recordsData, isCopyPaste);
};

batchDeleteFiles = (repoId, fileNames) => {
return this.metadataAPI.batchDeleteFiles(repoId, fileNames);
};

// view
modifyView = (repoId, viewId, viewData) => {
return this.metadataAPI.modifyView(repoId, viewId, viewData);
Expand Down
Loading

0 comments on commit ac9ea56

Please sign in to comment.