mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-05 17:02:47 +00:00
feat(metadata-views): support add folder (#7175)
This commit is contained in:
@@ -1,33 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TreeView from '../../tree-view/tree-view';
|
||||
import Loading from '../../loading';
|
||||
import ModalPortal from '../../modal-portal';
|
||||
import Rename from '../../dialog/rename-dialog';
|
||||
import Copy from '../../dialog/copy-dirent-dialog';
|
||||
import Move from '../../dialog/move-dirent-dialog';
|
||||
import CreateFolder from '../../dialog/create-folder-dialog';
|
||||
import CreateFile from '../../dialog/create-file-dialog';
|
||||
import ImageDialog from '../../dialog/image-dialog';
|
||||
import { fileServerRoot, gettext, siteRoot, thumbnailSizeForOriginal, thumbnailDefaultSize } from '../../../utils/constants';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import TextTranslation from '../../../utils/text-translation';
|
||||
import TreeSection from '../../tree-section';
|
||||
import DirFiles from '../dir-files';
|
||||
import DirViews from '../dir-views';
|
||||
import DirTags from '../dir-tags';
|
||||
import DirOthers from '../dir-others';
|
||||
import imageAPI from '../../../utils/image-api';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import toaster from '../../toast';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const propTypes = {
|
||||
currentPath: PropTypes.string.isRequired,
|
||||
userPerm: PropTypes.string.isRequired,
|
||||
currentRepoInfo: PropTypes.object.isRequired,
|
||||
isTreeDataLoading: PropTypes.bool.isRequired,
|
||||
treeData: PropTypes.object.isRequired,
|
||||
direntList: PropTypes.array,
|
||||
selectedDirentList: PropTypes.array.isRequired,
|
||||
currentNode: PropTypes.object,
|
||||
repoID: PropTypes.string.isRequired,
|
||||
navRate: PropTypes.number,
|
||||
inResizing: PropTypes.bool.isRequired,
|
||||
onNodeClick: PropTypes.func.isRequired,
|
||||
onNodeCollapse: PropTypes.func.isRequired,
|
||||
onNodeExpanded: PropTypes.func.isRequired,
|
||||
@@ -35,472 +27,57 @@ const propTypes = {
|
||||
onDeleteNode: PropTypes.func.isRequired,
|
||||
onAddFileNode: PropTypes.func.isRequired,
|
||||
onAddFolderNode: PropTypes.func.isRequired,
|
||||
repoID: PropTypes.string.isRequired,
|
||||
navRate: PropTypes.number,
|
||||
inResizing: PropTypes.bool.isRequired,
|
||||
currentRepoInfo: PropTypes.object.isRequired,
|
||||
onItemMove: PropTypes.func.isRequired,
|
||||
onItemCopy: PropTypes.func.isRequired,
|
||||
selectedDirentList: PropTypes.array.isRequired,
|
||||
onItemsMove: PropTypes.func.isRequired,
|
||||
getMenuContainerSize: PropTypes.func,
|
||||
updateDirent: PropTypes.func,
|
||||
direntList: PropTypes.array
|
||||
};
|
||||
|
||||
class DirColumnNav extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
opNode: null,
|
||||
isAddFileDialogShow: false,
|
||||
isAddFolderDialogShow: false,
|
||||
isRenameDialogShow: false,
|
||||
isNodeImagePopupOpen: false,
|
||||
imageNodeItems: [],
|
||||
imageIndex: 0,
|
||||
isCopyDialogShow: false,
|
||||
isMoveDialogShow: false,
|
||||
isMultipleOperation: false,
|
||||
operationList: [],
|
||||
isDisplayFiles: localStorage.getItem('sf_display_files') === 'true' || false,
|
||||
};
|
||||
this.isNodeMenuShow = true;
|
||||
this.imageItemsSnapshot = [];
|
||||
this.imageIndexSnapshot = 0;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initMenuList();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.direntList.length < this.props.direntList.length && this.state.isNodeImagePopupOpen) {
|
||||
if (this.state.imageNodeItems.length === 0) {
|
||||
this.setState({
|
||||
isNodeImagePopupOpen: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
imageNodeItems: this.imageItemsSnapshot,
|
||||
imageIndex: this.imageIndexSnapshot,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initMenuList = () => {
|
||||
const menuList = this.getMenuList();
|
||||
this.setState({ operationList: menuList });
|
||||
};
|
||||
|
||||
getMenuList = () => {
|
||||
let menuList = [];
|
||||
menuList.push(TextTranslation.NEW_FOLDER);
|
||||
menuList.push(TextTranslation.NEW_FILE);
|
||||
menuList.push(TextTranslation.DISPLAY_FILES);
|
||||
return menuList;
|
||||
};
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({ opNode: nextProps.currentNode });
|
||||
}
|
||||
|
||||
onNodeClick = (node) => {
|
||||
this.setState({ opNode: node });
|
||||
if (Utils.imageCheck(node?.object?.name || '')) {
|
||||
this.showNodeImagePopup(node);
|
||||
return;
|
||||
}
|
||||
this.props.onNodeClick(node);
|
||||
};
|
||||
|
||||
onMoreOperationClick = (operation) => {
|
||||
this.onMenuItemClick(operation);
|
||||
};
|
||||
|
||||
onMenuItemClick = (operation, node) => {
|
||||
this.setState({ opNode: node });
|
||||
switch (operation) {
|
||||
case 'New Folder':
|
||||
if (!node) {
|
||||
this.onAddFolderToggle('root');
|
||||
} else {
|
||||
this.onAddFolderToggle();
|
||||
}
|
||||
break;
|
||||
case 'New File':
|
||||
if (!node) {
|
||||
this.onAddFileToggle('root');
|
||||
} else {
|
||||
this.onAddFileToggle();
|
||||
}
|
||||
break;
|
||||
case 'Rename':
|
||||
this.onRenameToggle();
|
||||
break;
|
||||
case 'Delete':
|
||||
this.onDeleteNode(node);
|
||||
break;
|
||||
case 'Copy':
|
||||
this.onCopyToggle();
|
||||
break;
|
||||
case 'Move':
|
||||
this.onMoveToggle();
|
||||
break;
|
||||
case 'Open in New Tab':
|
||||
this.onOpenFile(node);
|
||||
break;
|
||||
case 'Display files':
|
||||
this.onDisplayFilesToggle();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
onAddFileToggle = (type) => {
|
||||
if (type === 'root') {
|
||||
let root = this.props.treeData.root;
|
||||
this.setState({
|
||||
isAddFileDialogShow: !this.state.isAddFileDialogShow,
|
||||
opNode: root,
|
||||
});
|
||||
} else {
|
||||
this.setState({ isAddFileDialogShow: !this.state.isAddFileDialogShow });
|
||||
}
|
||||
};
|
||||
|
||||
onAddFolderToggle = (type) => {
|
||||
if (type === 'root') {
|
||||
let root = this.props.treeData.root;
|
||||
this.setState({
|
||||
isAddFolderDialogShow: !this.state.isAddFolderDialogShow,
|
||||
opNode: root,
|
||||
});
|
||||
} else {
|
||||
this.setState({ isAddFolderDialogShow: !this.state.isAddFolderDialogShow });
|
||||
}
|
||||
};
|
||||
|
||||
onRenameToggle = () => {
|
||||
this.setState({ isRenameDialogShow: !this.state.isRenameDialogShow });
|
||||
};
|
||||
|
||||
onCopyToggle = () => {
|
||||
this.setState({ isCopyDialogShow: !this.state.isCopyDialogShow });
|
||||
};
|
||||
|
||||
onMoveToggle = () => {
|
||||
this.setState({ isMoveDialogShow: !this.state.isMoveDialogShow });
|
||||
};
|
||||
|
||||
onAddFolderNode = (dirPath) => {
|
||||
this.setState({ isAddFolderDialogShow: !this.state.isAddFolderDialogShow });
|
||||
this.props.onAddFolderNode(dirPath);
|
||||
};
|
||||
|
||||
onRenameNode = (newName) => {
|
||||
this.setState({ isRenameDialogShow: !this.state.isRenameDialogShow });
|
||||
let node = this.state.opNode;
|
||||
this.props.onRenameNode(node, newName);
|
||||
};
|
||||
|
||||
onDeleteNode = (node) => {
|
||||
this.props.onDeleteNode(node);
|
||||
};
|
||||
|
||||
onOpenFile = (node) => {
|
||||
let newUrl = siteRoot + 'lib/' + this.props.repoID + '/file' + Utils.encodePath(node.path);
|
||||
window.open(newUrl, '_blank');
|
||||
};
|
||||
|
||||
onDisplayFilesToggle = () => {
|
||||
this.setState({ isDisplayFiles: !this.state.isDisplayFiles }, () => {
|
||||
localStorage.setItem('sf_display_files', this.state.isDisplayFiles);
|
||||
});
|
||||
};
|
||||
|
||||
checkDuplicatedName = (newName) => {
|
||||
let node = this.state.opNode;
|
||||
// root node to new node conditions: parentNode is null,
|
||||
let parentNode = node.parentNode ? node.parentNode : node;
|
||||
let childrenObject = parentNode.children.map(item => {
|
||||
return item.object;
|
||||
});
|
||||
let isDuplicated = childrenObject.some(object => {
|
||||
return object.name === newName;
|
||||
});
|
||||
return isDuplicated;
|
||||
};
|
||||
|
||||
showNodeImagePopup = (node) => {
|
||||
let childrenNode = node.parentNode.children;
|
||||
let items = childrenNode.filter((item) => {
|
||||
return Utils.imageCheck(item.object.name);
|
||||
});
|
||||
let imageNames = items.map((item) => {
|
||||
return item.object.name;
|
||||
});
|
||||
this.setState({
|
||||
isNodeImagePopupOpen: true,
|
||||
imageNodeItems: this.prepareImageItems(node),
|
||||
imageIndex: imageNames.indexOf(node.object.name)
|
||||
});
|
||||
};
|
||||
|
||||
prepareImageItems = (node) => {
|
||||
let childrenNode = node.parentNode.children;
|
||||
let items = childrenNode.filter((item) => {
|
||||
return Utils.imageCheck(item.object.name);
|
||||
});
|
||||
|
||||
const repoEncrypted = this.props.currentRepoInfo.encrypted;
|
||||
const repoID = this.props.repoID;
|
||||
let prepareItem = (item) => {
|
||||
const name = item.object.name;
|
||||
const path = Utils.encodePath(Utils.joinPath(node.parentNode.path, name));
|
||||
const fileExt = name.substr(name.lastIndexOf('.') + 1).toLowerCase();
|
||||
const isGIF = fileExt === 'gif';
|
||||
const src = `${siteRoot}repo/${repoID}/raw${path}`;
|
||||
let thumbnail = '';
|
||||
if (repoEncrypted || isGIF) {
|
||||
thumbnail = src;
|
||||
} else {
|
||||
thumbnail = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`;
|
||||
}
|
||||
return {
|
||||
name,
|
||||
src,
|
||||
thumbnail,
|
||||
'url': `${siteRoot}lib/${repoID}/file${path}`,
|
||||
'node': items.find(item => item.path.split('/').pop() === name),
|
||||
'downloadURL': `${fileServerRoot}repos/${repoID}/files${path}/?op=download`,
|
||||
};
|
||||
};
|
||||
|
||||
return items.map((item) => { return prepareItem(item); });
|
||||
};
|
||||
|
||||
closeNodeImagePopup = () => {
|
||||
this.setState({
|
||||
isNodeImagePopupOpen: false
|
||||
});
|
||||
};
|
||||
|
||||
moveToPrevImage = () => {
|
||||
const imageItemsLength = this.state.imageNodeItems.length;
|
||||
this.setState((prevState) => ({
|
||||
imageIndex: (prevState.imageIndex + imageItemsLength - 1) % imageItemsLength
|
||||
}));
|
||||
};
|
||||
|
||||
moveToNextImage = () => {
|
||||
const imageItemsLength = this.state.imageNodeItems.length;
|
||||
this.setState((prevState) => ({
|
||||
imageIndex: (prevState.imageIndex + 1) % imageItemsLength
|
||||
}));
|
||||
};
|
||||
|
||||
deleteImage = () => {
|
||||
this.imageItemsSnapshot = this.state.imageNodeItems;
|
||||
this.imageIndexSnapshot = this.state.imageIndex;
|
||||
|
||||
if (this.state.imageNodeItems.length > this.state.imageIndex) {
|
||||
this.props.onDeleteNode(this.state.imageNodeItems[this.state.imageIndex].node);
|
||||
}
|
||||
const imageNodeItems = this.state.imageNodeItems.filter((item, index) => index !== this.state.imageIndex);
|
||||
|
||||
if (!imageNodeItems.length) {
|
||||
this.setState({
|
||||
isNodeImagePopupOpen: false,
|
||||
imageNodeItems: [],
|
||||
imageIndex: 0
|
||||
});
|
||||
} else {
|
||||
this.setState((prevState) => ({
|
||||
imageNodeItems: imageNodeItems,
|
||||
imageIndex: (prevState.imageIndex + 1) % imageNodeItems.length,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
handleError = (error) => {
|
||||
toaster.danger(Utils.getErrorMsg(error));
|
||||
};
|
||||
|
||||
rotateImage = (imageIndex, angle) => {
|
||||
if (imageIndex >= 0 && angle !== 0) {
|
||||
let { repoID } = this.props;
|
||||
let imageName = this.state.imageNodeItems[imageIndex].name;
|
||||
let path = this.state.opNode.path;
|
||||
imageAPI.rotateImage(repoID, path, 360 - angle).then((res) => {
|
||||
seafileAPI.createThumbnail(repoID, path, thumbnailDefaultSize).then((res) => {
|
||||
// Generate a unique query parameter to bust the cache
|
||||
const cacheBuster = new Date().getTime();
|
||||
const newThumbnailSrc = `${res.data.encoded_thumbnail_src}?t=${cacheBuster}`;
|
||||
this.setState((prevState) => {
|
||||
const updatedImageItems = [...prevState.imageNodeItems];
|
||||
updatedImageItems[imageIndex].src = newThumbnailSrc;
|
||||
return { imageNodeItems: updatedImageItems };
|
||||
});
|
||||
// Update the thumbnail URL with the cache-busting query parameter
|
||||
const item = this.props.direntList.find((item) => item.name === imageName);
|
||||
this.props.updateDirent(item, 'encoded_thumbnail_src', newThumbnailSrc);
|
||||
}).catch(error => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}).catch(error => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
stopTreeScrollPropagation = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
renderContent = () => {
|
||||
render() {
|
||||
const {
|
||||
isTreeDataLoading,
|
||||
userPerm,
|
||||
treeData,
|
||||
currentPath,
|
||||
onNodeExpanded,
|
||||
onNodeCollapse,
|
||||
onItemMove,
|
||||
onItemsMove,
|
||||
currentRepoInfo,
|
||||
selectedDirentList,
|
||||
repoID,
|
||||
getMenuContainerSize,
|
||||
isTreeDataLoading, userPerm, treeData, repoID, currentPath, currentRepoInfo,
|
||||
} = this.props;
|
||||
|
||||
const flex = this.props.navRate ? '0 0 ' + this.props.navRate * 100 + '%' : '0 0 25%';
|
||||
const select = this.props.inResizing ? 'none' : '';
|
||||
return (
|
||||
<>
|
||||
{isTreeDataLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<div className="dir-content-nav" role="navigation" style={{ flex: (flex), userSelect: select }} onScroll={this.stopTreeScrollPropagation}>
|
||||
{isTreeDataLoading ? <Loading /> : (
|
||||
<>
|
||||
<TreeSection
|
||||
title={gettext('Files')}
|
||||
moreKey={{ name: 'files' }}
|
||||
moreOperations={this.state.operationList}
|
||||
moreOperationClick={this.onMoreOperationClick}
|
||||
isDisplayFiles={this.state.isDisplayFiles}
|
||||
>
|
||||
<TreeView
|
||||
userPerm={userPerm}
|
||||
isNodeMenuShow={this.isNodeMenuShow}
|
||||
treeData={treeData}
|
||||
currentPath={currentPath}
|
||||
onNodeClick={this.onNodeClick}
|
||||
onNodeExpanded={onNodeExpanded}
|
||||
onNodeCollapse={onNodeCollapse}
|
||||
onMenuItemClick={this.onMenuItemClick}
|
||||
onFreezedItem={this.onFreezedItem}
|
||||
onUnFreezedItem={this.onUnFreezedItem}
|
||||
onItemMove={onItemMove}
|
||||
currentRepoInfo={currentRepoInfo}
|
||||
selectedDirentList={selectedDirentList}
|
||||
onItemsMove={onItemsMove}
|
||||
repoID={repoID}
|
||||
getMenuContainerSize={getMenuContainerSize}
|
||||
isDisplayFiles={this.state.isDisplayFiles}
|
||||
/>
|
||||
</TreeSection>
|
||||
<DirViews repoID={repoID} currentPath={currentPath} userPerm={userPerm} currentRepoInfo={currentRepoInfo} />
|
||||
<DirTags repoID={repoID} currentPath={currentPath} userPerm={userPerm} currentRepoInfo={currentRepoInfo} />
|
||||
<DirOthers
|
||||
<DirFiles
|
||||
repoID={repoID}
|
||||
currentPath={currentPath}
|
||||
treeData={treeData}
|
||||
userPerm={userPerm}
|
||||
currentRepoInfo={currentRepoInfo}
|
||||
direntList={this.props.direntList}
|
||||
selectedDirentList={this.props.selectedDirentList}
|
||||
currentNode={this.props.currentNode}
|
||||
getMenuContainerSize={this.props.getMenuContainerSize}
|
||||
onNodeClick={this.props.onNodeClick}
|
||||
onNodeCollapse={this.props.onNodeCollapse}
|
||||
onNodeExpanded={this.props.onNodeExpanded}
|
||||
onRenameNode={this.props.onRenameNode}
|
||||
onDeleteNode={this.props.onDeleteNode}
|
||||
onAddFileNode={this.props.onAddFileNode}
|
||||
onAddFolderNode={this.props.onAddFolderNode}
|
||||
onItemCopy={this.props.onItemCopy}
|
||||
onItemMove={this.props.onItemMove}
|
||||
onItemsMove={this.props.onItemsMove}
|
||||
updateDirent={this.props.updateDirent}
|
||||
/>
|
||||
<DirViews repoID={repoID} currentPath={currentPath} userPerm={userPerm} currentRepoInfo={currentRepoInfo} />
|
||||
<DirTags repoID={repoID} currentPath={currentPath} userPerm={userPerm} currentRepoInfo={currentRepoInfo} />
|
||||
<DirOthers repoID={repoID} userPerm={userPerm} currentRepoInfo={currentRepoInfo} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
let flex = this.props.navRate ? '0 0 ' + this.props.navRate * 100 + '%' : '0 0 25%';
|
||||
const select = this.props.inResizing ? 'none' : '';
|
||||
const repoEncrypted = this.props.currentRepoInfo.encrypted;
|
||||
return (
|
||||
<>
|
||||
<div className="dir-content-nav" role="navigation" style={{ flex: (flex), userSelect: select }} onScroll={this.stopTreeScrollPropagation}>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
{this.state.isAddFolderDialogShow && (
|
||||
<ModalPortal>
|
||||
<CreateFolder
|
||||
parentPath={this.state.opNode.path}
|
||||
onAddFolder={this.onAddFolderNode}
|
||||
checkDuplicatedName={this.checkDuplicatedName}
|
||||
addFolderCancel={this.onAddFolderToggle}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isAddFileDialogShow && (
|
||||
<ModalPortal>
|
||||
<CreateFile
|
||||
parentPath={this.state.opNode.path}
|
||||
onAddFile={this.props.onAddFileNode}
|
||||
checkDuplicatedName={this.checkDuplicatedName}
|
||||
toggleDialog={this.onAddFileToggle}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isRenameDialogShow && (
|
||||
<ModalPortal>
|
||||
<Rename
|
||||
currentNode={this.state.opNode}
|
||||
onRename={this.onRenameNode}
|
||||
checkDuplicatedName={this.checkDuplicatedName}
|
||||
toggleCancel={this.onRenameToggle}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isCopyDialogShow && (
|
||||
<ModalPortal>
|
||||
<Copy
|
||||
path={this.state.opNode.parentNode.path}
|
||||
repoID={this.props.repoID}
|
||||
dirent={this.state.opNode.object}
|
||||
onItemCopy={this.props.onItemCopy}
|
||||
repoEncrypted={repoEncrypted}
|
||||
onCancelCopy={this.onCopyToggle}
|
||||
isMultipleOperation={this.state.isMultipleOperation}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isMoveDialogShow && (
|
||||
<ModalPortal>
|
||||
<Move
|
||||
path={this.state.opNode.parentNode.path}
|
||||
repoID={this.props.repoID}
|
||||
dirent={this.state.opNode.object}
|
||||
onItemMove={this.props.onItemMove}
|
||||
repoEncrypted={repoEncrypted}
|
||||
onCancelMove={this.onMoveToggle}
|
||||
isMultipleOperation={this.state.isMultipleOperation}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isNodeImagePopupOpen && (
|
||||
<ModalPortal>
|
||||
<ImageDialog
|
||||
imageItems={this.state.imageNodeItems}
|
||||
imageIndex={this.state.imageIndex}
|
||||
closeImagePopup={this.closeNodeImagePopup}
|
||||
moveToPrevImage={this.moveToPrevImage}
|
||||
moveToNextImage={this.moveToNextImage}
|
||||
onDeleteImage={this.deleteImage}
|
||||
onRotateImage={this.rotateImage}
|
||||
enableRotate={!repoEncrypted}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
462
frontend/src/components/dir-view-mode/dir-files.js
Normal file
462
frontend/src/components/dir-view-mode/dir-files.js
Normal file
@@ -0,0 +1,462 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TreeView from '../tree-view/tree-view';
|
||||
import ModalPortal from '../modal-portal';
|
||||
import Rename from '../dialog/rename-dialog';
|
||||
import Copy from '../dialog/copy-dirent-dialog';
|
||||
import Move from '../dialog/move-dirent-dialog';
|
||||
import CreateFolder from '../dialog/create-folder-dialog';
|
||||
import CreateFile from '../dialog/create-file-dialog';
|
||||
import ImageDialog from '../dialog/image-dialog';
|
||||
import toaster from '../toast';
|
||||
import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu';
|
||||
import { fileServerRoot, gettext, siteRoot, thumbnailSizeForOriginal, thumbnailDefaultSize } from '../../utils/constants';
|
||||
import { isMobile, Utils } from '../../utils/utils';
|
||||
import TextTranslation from '../../utils/text-translation';
|
||||
import TreeSection from '../tree-section';
|
||||
import imageAPI from '../../utils/image-api';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
currentPath: PropTypes.string.isRequired,
|
||||
treeData: PropTypes.object.isRequired,
|
||||
userPerm: PropTypes.string.isRequired,
|
||||
currentRepoInfo: PropTypes.object.isRequired,
|
||||
direntList: PropTypes.array,
|
||||
selectedDirentList: PropTypes.array.isRequired,
|
||||
currentNode: PropTypes.object,
|
||||
getMenuContainerSize: PropTypes.func,
|
||||
onNodeClick: PropTypes.func.isRequired,
|
||||
onNodeCollapse: PropTypes.func.isRequired,
|
||||
onNodeExpanded: PropTypes.func.isRequired,
|
||||
onRenameNode: PropTypes.func.isRequired,
|
||||
onDeleteNode: PropTypes.func.isRequired,
|
||||
onAddFileNode: PropTypes.func.isRequired,
|
||||
onAddFolderNode: PropTypes.func.isRequired,
|
||||
onItemCopy: PropTypes.func.isRequired,
|
||||
onItemMove: PropTypes.func.isRequired,
|
||||
onItemsMove: PropTypes.func.isRequired,
|
||||
updateDirent: PropTypes.func,
|
||||
};
|
||||
|
||||
class DirFiles extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
opNode: null,
|
||||
isAddFileDialogShow: false,
|
||||
isAddFolderDialogShow: false,
|
||||
isRenameDialogShow: false,
|
||||
isNodeImagePopupOpen: false,
|
||||
imageNodeItems: [],
|
||||
imageIndex: 0,
|
||||
isCopyDialogShow: false,
|
||||
isMoveDialogShow: false,
|
||||
isMultipleOperation: false,
|
||||
operationList: [],
|
||||
isDisplayFiles: localStorage.getItem('sf_display_files') === 'true' || false,
|
||||
};
|
||||
this.isNodeMenuShow = true;
|
||||
this.imageItemsSnapshot = [];
|
||||
this.imageIndexSnapshot = 0;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.direntList.length < this.props.direntList.length && this.state.isNodeImagePopupOpen) {
|
||||
if (this.state.imageNodeItems.length === 0) {
|
||||
this.setState({
|
||||
isNodeImagePopupOpen: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
imageNodeItems: this.imageItemsSnapshot,
|
||||
imageIndex: this.imageIndexSnapshot,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({ opNode: nextProps.currentNode });
|
||||
}
|
||||
|
||||
getMenuList = () => {
|
||||
return [
|
||||
TextTranslation.NEW_FOLDER,
|
||||
TextTranslation.NEW_FILE,
|
||||
TextTranslation.DISPLAY_FILES,
|
||||
];
|
||||
};
|
||||
|
||||
onNodeClick = (node) => {
|
||||
this.setState({ opNode: node });
|
||||
if (Utils.imageCheck(node?.object?.name || '')) {
|
||||
this.showNodeImagePopup(node);
|
||||
return;
|
||||
}
|
||||
this.props.onNodeClick(node);
|
||||
};
|
||||
|
||||
onMoreOperationClick = (operation) => {
|
||||
this.onMenuItemClick(operation);
|
||||
};
|
||||
|
||||
onMenuItemClick = (operation, node) => {
|
||||
this.setState({ opNode: node });
|
||||
switch (operation) {
|
||||
case 'New Folder':
|
||||
if (!node) {
|
||||
this.onAddFolderToggle('root');
|
||||
} else {
|
||||
this.onAddFolderToggle();
|
||||
}
|
||||
break;
|
||||
case 'New File':
|
||||
if (!node) {
|
||||
this.onAddFileToggle('root');
|
||||
} else {
|
||||
this.onAddFileToggle();
|
||||
}
|
||||
break;
|
||||
case 'Rename':
|
||||
this.onRenameToggle();
|
||||
break;
|
||||
case 'Delete':
|
||||
this.onDeleteNode(node);
|
||||
break;
|
||||
case 'Copy':
|
||||
this.onCopyToggle();
|
||||
break;
|
||||
case 'Move':
|
||||
this.onMoveToggle();
|
||||
break;
|
||||
case 'Open in New Tab':
|
||||
this.onOpenFile(node);
|
||||
break;
|
||||
case 'Display files':
|
||||
this.onDisplayFilesToggle();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
onAddFileToggle = (type) => {
|
||||
if (type === 'root') {
|
||||
let root = this.props.treeData.root;
|
||||
this.setState({
|
||||
isAddFileDialogShow: !this.state.isAddFileDialogShow,
|
||||
opNode: root,
|
||||
});
|
||||
} else {
|
||||
this.setState({ isAddFileDialogShow: !this.state.isAddFileDialogShow });
|
||||
}
|
||||
};
|
||||
|
||||
onAddFolderToggle = (type) => {
|
||||
if (type === 'root') {
|
||||
let root = this.props.treeData.root;
|
||||
this.setState({
|
||||
isAddFolderDialogShow: !this.state.isAddFolderDialogShow,
|
||||
opNode: root,
|
||||
});
|
||||
} else {
|
||||
this.setState({ isAddFolderDialogShow: !this.state.isAddFolderDialogShow });
|
||||
}
|
||||
};
|
||||
|
||||
onRenameToggle = () => {
|
||||
this.setState({ isRenameDialogShow: !this.state.isRenameDialogShow });
|
||||
};
|
||||
|
||||
onCopyToggle = () => {
|
||||
this.setState({ isCopyDialogShow: !this.state.isCopyDialogShow });
|
||||
};
|
||||
|
||||
onMoveToggle = () => {
|
||||
this.setState({ isMoveDialogShow: !this.state.isMoveDialogShow });
|
||||
};
|
||||
|
||||
onAddFolderNode = (dirPath) => {
|
||||
this.setState({ isAddFolderDialogShow: !this.state.isAddFolderDialogShow });
|
||||
this.props.onAddFolderNode(dirPath);
|
||||
};
|
||||
|
||||
onRenameNode = (newName) => {
|
||||
this.setState({ isRenameDialogShow: !this.state.isRenameDialogShow });
|
||||
let node = this.state.opNode;
|
||||
this.props.onRenameNode(node, newName);
|
||||
};
|
||||
|
||||
onDeleteNode = (node) => {
|
||||
this.props.onDeleteNode(node);
|
||||
};
|
||||
|
||||
onOpenFile = (node) => {
|
||||
let newUrl = siteRoot + 'lib/' + this.props.repoID + '/file' + Utils.encodePath(node.path);
|
||||
window.open(newUrl, '_blank');
|
||||
};
|
||||
|
||||
onDisplayFilesToggle = () => {
|
||||
this.setState({ isDisplayFiles: !this.state.isDisplayFiles }, () => {
|
||||
localStorage.setItem('sf_display_files', this.state.isDisplayFiles);
|
||||
});
|
||||
};
|
||||
|
||||
checkDuplicatedName = (newName) => {
|
||||
let node = this.state.opNode;
|
||||
// root node to new node conditions: parentNode is null,
|
||||
let parentNode = node.parentNode ? node.parentNode : node;
|
||||
let childrenObject = parentNode.children.map(item => {
|
||||
return item.object;
|
||||
});
|
||||
let isDuplicated = childrenObject.some(object => {
|
||||
return object.name === newName;
|
||||
});
|
||||
return isDuplicated;
|
||||
};
|
||||
|
||||
showNodeImagePopup = (node) => {
|
||||
let childrenNode = node.parentNode.children;
|
||||
let items = childrenNode.filter((item) => {
|
||||
return Utils.imageCheck(item.object.name);
|
||||
});
|
||||
let imageNames = items.map((item) => {
|
||||
return item.object.name;
|
||||
});
|
||||
this.setState({
|
||||
isNodeImagePopupOpen: true,
|
||||
imageNodeItems: this.prepareImageItems(node),
|
||||
imageIndex: imageNames.indexOf(node.object.name)
|
||||
});
|
||||
};
|
||||
|
||||
prepareImageItems = (node) => {
|
||||
let childrenNode = node.parentNode.children;
|
||||
let items = childrenNode.filter((item) => {
|
||||
return Utils.imageCheck(item.object.name);
|
||||
});
|
||||
|
||||
const repoEncrypted = this.props.currentRepoInfo.encrypted;
|
||||
const repoID = this.props.repoID;
|
||||
let prepareItem = (item) => {
|
||||
const name = item.object.name;
|
||||
const path = Utils.encodePath(Utils.joinPath(node.parentNode.path, name));
|
||||
const fileExt = name.substr(name.lastIndexOf('.') + 1).toLowerCase();
|
||||
const isGIF = fileExt === 'gif';
|
||||
const src = `${siteRoot}repo/${repoID}/raw${path}`;
|
||||
let thumbnail = '';
|
||||
if (repoEncrypted || isGIF) {
|
||||
thumbnail = src;
|
||||
} else {
|
||||
thumbnail = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`;
|
||||
}
|
||||
return {
|
||||
name,
|
||||
src,
|
||||
thumbnail,
|
||||
'url': `${siteRoot}lib/${repoID}/file${path}`,
|
||||
'node': items.find(item => item.path.split('/').pop() === name),
|
||||
'downloadURL': `${fileServerRoot}repos/${repoID}/files${path}/?op=download`,
|
||||
};
|
||||
};
|
||||
|
||||
return items.map((item) => { return prepareItem(item); });
|
||||
};
|
||||
|
||||
closeNodeImagePopup = () => {
|
||||
this.setState({
|
||||
isNodeImagePopupOpen: false
|
||||
});
|
||||
};
|
||||
|
||||
moveToPrevImage = () => {
|
||||
const imageItemsLength = this.state.imageNodeItems.length;
|
||||
this.setState((prevState) => ({
|
||||
imageIndex: (prevState.imageIndex + imageItemsLength - 1) % imageItemsLength
|
||||
}));
|
||||
};
|
||||
|
||||
moveToNextImage = () => {
|
||||
const imageItemsLength = this.state.imageNodeItems.length;
|
||||
this.setState((prevState) => ({
|
||||
imageIndex: (prevState.imageIndex + 1) % imageItemsLength
|
||||
}));
|
||||
};
|
||||
|
||||
deleteImage = () => {
|
||||
this.imageItemsSnapshot = this.state.imageNodeItems;
|
||||
this.imageIndexSnapshot = this.state.imageIndex;
|
||||
|
||||
if (this.state.imageNodeItems.length > this.state.imageIndex) {
|
||||
this.props.onDeleteNode(this.state.imageNodeItems[this.state.imageIndex].node);
|
||||
}
|
||||
const imageNodeItems = this.state.imageNodeItems.filter((item, index) => index !== this.state.imageIndex);
|
||||
|
||||
if (!imageNodeItems.length) {
|
||||
this.setState({
|
||||
isNodeImagePopupOpen: false,
|
||||
imageNodeItems: [],
|
||||
imageIndex: 0
|
||||
});
|
||||
} else {
|
||||
this.setState((prevState) => ({
|
||||
imageNodeItems: imageNodeItems,
|
||||
imageIndex: (prevState.imageIndex + 1) % imageNodeItems.length,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
handleError = (error) => {
|
||||
toaster.danger(Utils.getErrorMsg(error));
|
||||
};
|
||||
|
||||
rotateImage = (imageIndex, angle) => {
|
||||
if (imageIndex >= 0 && angle !== 0) {
|
||||
let { repoID } = this.props;
|
||||
let imageName = this.state.imageNodeItems[imageIndex].name;
|
||||
let path = this.state.opNode.path;
|
||||
imageAPI.rotateImage(repoID, path, 360 - angle).then((res) => {
|
||||
seafileAPI.createThumbnail(repoID, path, thumbnailDefaultSize).then((res) => {
|
||||
// Generate a unique query parameter to bust the cache
|
||||
const cacheBuster = new Date().getTime();
|
||||
const newThumbnailSrc = `${res.data.encoded_thumbnail_src}?t=${cacheBuster}`;
|
||||
this.setState((prevState) => {
|
||||
const updatedImageItems = [...prevState.imageNodeItems];
|
||||
updatedImageItems[imageIndex].src = newThumbnailSrc;
|
||||
return { imageNodeItems: updatedImageItems };
|
||||
});
|
||||
// Update the thumbnail URL with the cache-busting query parameter
|
||||
const item = this.props.direntList.find((item) => item.name === imageName);
|
||||
this.props.updateDirent(item, 'encoded_thumbnail_src', newThumbnailSrc);
|
||||
}).catch(error => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}).catch(error => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderTreeSectionHeaderOperations = (props) => {
|
||||
const moreOperation = (
|
||||
<div className="tree-section-header-operation tree-section-more-operation" key='tree-section-more-operation'>
|
||||
<ItemDropdownMenu
|
||||
{...props}
|
||||
item={{ name: 'files' }}
|
||||
toggleClass="sf3-font sf3-font-more"
|
||||
menuStyle={isMobile ? { zIndex: 1050 } : {}}
|
||||
isDisplayFiles={this.state.isDisplayFiles}
|
||||
getMenuList={this.getMenuList}
|
||||
onMenuItemClick={this.onMoreOperationClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return [moreOperation];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repoID, currentRepoInfo } = this.props;
|
||||
const repoEncrypted = currentRepoInfo.encrypted;
|
||||
return (
|
||||
<>
|
||||
<TreeSection
|
||||
title={gettext('Files')}
|
||||
renderHeaderOperations={this.renderTreeSectionHeaderOperations}
|
||||
>
|
||||
<TreeView
|
||||
repoID={repoID}
|
||||
userPerm={this.props.userPerm}
|
||||
treeData={this.props.treeData}
|
||||
currentPath={this.props.currentPath}
|
||||
currentRepoInfo={currentRepoInfo}
|
||||
selectedDirentList={this.props.selectedDirentList}
|
||||
isDisplayFiles={this.state.isDisplayFiles}
|
||||
isNodeMenuShow={this.isNodeMenuShow}
|
||||
onNodeClick={this.onNodeClick}
|
||||
onMenuItemClick={this.onMenuItemClick}
|
||||
getMenuContainerSize={this.props.getMenuContainerSize}
|
||||
onNodeExpanded={this.props.onNodeExpanded}
|
||||
onNodeCollapse={this.props.onNodeCollapse}
|
||||
onItemMove={this.props.onItemMove}
|
||||
onItemsMove={this.props.onItemsMove}
|
||||
/>
|
||||
</TreeSection>
|
||||
{this.state.isAddFolderDialogShow && (
|
||||
<ModalPortal>
|
||||
<CreateFolder
|
||||
parentPath={this.state.opNode.path}
|
||||
onAddFolder={this.onAddFolderNode}
|
||||
checkDuplicatedName={this.checkDuplicatedName}
|
||||
addFolderCancel={this.onAddFolderToggle}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isAddFileDialogShow && (
|
||||
<ModalPortal>
|
||||
<CreateFile
|
||||
parentPath={this.state.opNode.path}
|
||||
onAddFile={this.props.onAddFileNode}
|
||||
checkDuplicatedName={this.checkDuplicatedName}
|
||||
toggleDialog={this.onAddFileToggle}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isRenameDialogShow && (
|
||||
<ModalPortal>
|
||||
<Rename
|
||||
currentNode={this.state.opNode}
|
||||
onRename={this.onRenameNode}
|
||||
checkDuplicatedName={this.checkDuplicatedName}
|
||||
toggleCancel={this.onRenameToggle}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isCopyDialogShow && (
|
||||
<ModalPortal>
|
||||
<Copy
|
||||
repoID={repoID}
|
||||
path={this.state.opNode.parentNode.path}
|
||||
dirent={this.state.opNode.object}
|
||||
onItemCopy={this.props.onItemCopy}
|
||||
repoEncrypted={repoEncrypted}
|
||||
onCancelCopy={this.onCopyToggle}
|
||||
isMultipleOperation={this.state.isMultipleOperation}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isMoveDialogShow && (
|
||||
<ModalPortal>
|
||||
<Move
|
||||
repoID={repoID}
|
||||
path={this.state.opNode.parentNode.path}
|
||||
dirent={this.state.opNode.object}
|
||||
onItemMove={this.props.onItemMove}
|
||||
repoEncrypted={repoEncrypted}
|
||||
onCancelMove={this.onMoveToggle}
|
||||
isMultipleOperation={this.state.isMultipleOperation}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isNodeImagePopupOpen && (
|
||||
<ModalPortal>
|
||||
<ImageDialog
|
||||
imageItems={this.state.imageNodeItems}
|
||||
imageIndex={this.state.imageIndex}
|
||||
closeImagePopup={this.closeNodeImagePopup}
|
||||
moveToPrevImage={this.moveToPrevImage}
|
||||
moveToNextImage={this.moveToNextImage}
|
||||
onDeleteImage={this.deleteImage}
|
||||
onRotateImage={this.rotateImage}
|
||||
enableRotate={!repoEncrypted}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DirFiles.propTypes = propTypes;
|
||||
|
||||
export default DirFiles;
|
29
frontend/src/components/dir-view-mode/dir-views/index.css
Normal file
29
frontend/src/components/dir-view-mode/dir-views/index.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.metadata-views-dropdown-menu .dropdown-header {
|
||||
padding: 0 1rem;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.metadata-views-dropdown-menu .dropdown-header,
|
||||
.metadata-views-dropdown-menu .dropdown-item {
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.metadata-views-dropdown-menu .metadata-view-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.metadata-views-dropdown-menu .dropdown-item:hover .metadata-view-icon,
|
||||
.metadata-views-dropdown-menu .dropdown-item:focus .metadata-view-icon,
|
||||
.metadata-views-dropdown-menu .dropdown-item:focus .sf3-font {
|
||||
color: #fff;
|
||||
fill: #fff;
|
||||
}
|
@@ -1,17 +1,20 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import TreeSection from '../tree-section';
|
||||
import { MetadataTreeView, useMetadata } from '../../metadata';
|
||||
import ExtensionPrompts from './extension-prompts';
|
||||
import LibSettingsDialog, { TAB } from '../dialog/lib-settings';
|
||||
import { useMetadataStatus } from '../../hooks';
|
||||
import TreeSection from '../../tree-section';
|
||||
import ExtensionPrompts from '../extension-prompts';
|
||||
import LibSettingsDialog, { TAB } from '../../dialog/lib-settings';
|
||||
import ViewsMoreOperations from './views-more-operations';
|
||||
import { MetadataTreeView, useMetadata } from '../../../metadata';
|
||||
import { useMetadataStatus } from '../../../hooks';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
||||
const enableMetadataManagement = useMemo(() => {
|
||||
if (currentRepoInfo.encrypted) return false;
|
||||
return window.app.pageOptions.enableMetadataManagement;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [window.app.pageOptions.enableMetadataManagement, currentRepoInfo]);
|
||||
|
||||
const { navigation } = useMetadata();
|
||||
@@ -30,9 +33,27 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderTreeSectionHeaderOperations = (menuProps) => {
|
||||
const canAdd = userPerm === 'rw' || userPerm === 'admin';
|
||||
|
||||
let operations = [];
|
||||
if (enableMetadata && canAdd) {
|
||||
operations.push(
|
||||
<ViewsMoreOperations
|
||||
key={'tree-section-more-operation'}
|
||||
menuProps={menuProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return operations;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TreeSection title={gettext('Views')}>
|
||||
<TreeSection
|
||||
title={gettext('Views')}
|
||||
renderHeaderOperations={renderTreeSectionHeaderOperations}
|
||||
>
|
||||
{!enableMetadata ? (
|
||||
<ExtensionPrompts onExtendedProperties={onExtendedProperties} />
|
||||
) : Array.isArray(navigation) && navigation.length > 0 ? (
|
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import Icon from '../../icon';
|
||||
import TextTranslation from '../../../utils/text-translation';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { VIEW_TYPE, VIEW_TYPE_ICON } from '../../../metadata/constants';
|
||||
|
||||
export const KEY_ADD_VIEW_MAP = {
|
||||
ADD_FOLDER: 'ADD_FOLDER',
|
||||
ADD_TABLE: 'ADD_TABLE',
|
||||
ADD_GALLERY: 'ADD_GALLERY',
|
||||
ADD_KANBAN: 'ADD_KANBAN',
|
||||
ADD_MAP: 'ADD_MAP',
|
||||
};
|
||||
|
||||
const ADD_VIEW_OPTIONS = [
|
||||
{
|
||||
key: KEY_ADD_VIEW_MAP.ADD_TABLE,
|
||||
type: VIEW_TYPE.TABLE,
|
||||
},
|
||||
{
|
||||
key: KEY_ADD_VIEW_MAP.ADD_GALLERY,
|
||||
type: VIEW_TYPE.GALLERY,
|
||||
},
|
||||
{
|
||||
key: KEY_ADD_VIEW_MAP.ADD_KANBAN,
|
||||
type: VIEW_TYPE.KANBAN,
|
||||
},
|
||||
{
|
||||
key: KEY_ADD_VIEW_MAP.ADD_MAP,
|
||||
type: VIEW_TYPE.MAP,
|
||||
},
|
||||
];
|
||||
|
||||
const translateLabel = (type) => {
|
||||
switch (type) {
|
||||
case VIEW_TYPE.TABLE:
|
||||
return gettext('Table');
|
||||
case VIEW_TYPE.GALLERY:
|
||||
return gettext('Gallery');
|
||||
case VIEW_TYPE.KANBAN:
|
||||
return gettext('Kanban');
|
||||
case VIEW_TYPE.MAP:
|
||||
return gettext('Map');
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
const getNewViewSubMenus = () => {
|
||||
return ADD_VIEW_OPTIONS.map((option) => {
|
||||
const { key, type } = option;
|
||||
return {
|
||||
key,
|
||||
value: translateLabel(type),
|
||||
icon_dom: <Icon symbol={VIEW_TYPE_ICON[type] || VIEW_TYPE.TABLE} className="metadata-view-icon" />
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const getNewViewMenuItem = () => {
|
||||
return {
|
||||
...TextTranslation.ADD_VIEW,
|
||||
subOpListHeader: gettext('New view'),
|
||||
subOpList: getNewViewSubMenus(),
|
||||
};
|
||||
};
|
@@ -0,0 +1,66 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ItemDropdownMenu from '../../dropdown-menu/item-dropdown-menu';
|
||||
import TextTranslation from '../../../utils/text-translation';
|
||||
import { isMobile } from '../../../utils/utils';
|
||||
import EventBus from '../../common/event-bus';
|
||||
import { EVENT_BUS_TYPE, VIEW_TYPE } from '../../../metadata/constants';
|
||||
import { getNewViewMenuItem, KEY_ADD_VIEW_MAP } from './new-view-menu';
|
||||
|
||||
const ViewsMoreOperations = ({ menuProps }) => {
|
||||
const eventBus = EventBus.getInstance();
|
||||
|
||||
const addView = (viewType) => {
|
||||
eventBus.dispatch(EVENT_BUS_TYPE.ADD_VIEW, { viewType });
|
||||
};
|
||||
|
||||
const clickMenu = (option) => {
|
||||
switch (option) {
|
||||
case TextTranslation.ADD_FOLDER.key: {
|
||||
eventBus.dispatch(EVENT_BUS_TYPE.ADD_FOLDER);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_TABLE: {
|
||||
addView(VIEW_TYPE.TABLE);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_GALLERY: {
|
||||
addView(VIEW_TYPE.GALLERY);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_KANBAN: {
|
||||
addView(VIEW_TYPE.KANBAN);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_MAP: {
|
||||
addView(VIEW_TYPE.MAP);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getMoreOperationsMenus = useCallback(() => {
|
||||
return [
|
||||
TextTranslation.ADD_FOLDER,
|
||||
getNewViewMenuItem(),
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="tree-section-header-operation tree-section-more-operation">
|
||||
<ItemDropdownMenu
|
||||
{...menuProps}
|
||||
item={{ name: 'views' }}
|
||||
menuClassname="metadata-views-dropdown-menu"
|
||||
toggleClass="sf3-font sf3-font-more"
|
||||
menuStyle={isMobile ? { zIndex: 1050 } : {}}
|
||||
getMenuList={getMoreOperationsMenus}
|
||||
onMenuItemClick={clickMenu}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewsMoreOperations;
|
@@ -112,7 +112,9 @@ class ItemDropdownMenu extends React.Component {
|
||||
onMenuItemClick = (event) => {
|
||||
let operation = Utils.getEventData(event, 'toggle') ?? event.currentTarget.getAttribute('data-toggle');
|
||||
let item = this.props.item;
|
||||
this.props.unfreezeItem();
|
||||
this.props.onMenuItemClick(operation, event, item);
|
||||
this.setState({ isItemMenuShow: false });
|
||||
};
|
||||
|
||||
onDropDownMouseMove = () => {
|
||||
@@ -176,7 +178,7 @@ class ItemDropdownMenu extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown isOpen={this.state.isItemMenuShow} toggle={this.onDropdownToggleClick} className="vam">
|
||||
<Dropdown direction='down' isOpen={this.state.isItemMenuShow} toggle={this.onDropdownToggleClick} className="vam">
|
||||
<DropdownToggle
|
||||
tag={tagName || 'i'}
|
||||
role="button"
|
||||
@@ -192,6 +194,10 @@ class ItemDropdownMenu extends React.Component {
|
||||
<ModalPortal>
|
||||
<DropdownMenu
|
||||
style={menuStyle}
|
||||
className={this.props.menuClassname}
|
||||
positionFixed
|
||||
flip={false}
|
||||
modifiers={{ preventOverflow: { boundariesElement: document.body } }}
|
||||
>
|
||||
{menuList.map((menuItem, index) => {
|
||||
if (menuItem === 'Divider') {
|
||||
@@ -214,13 +220,21 @@ class ItemDropdownMenu extends React.Component {
|
||||
<span className="mr-auto">{menuItem.value}</span>
|
||||
<i className="sf3-font-down sf3-font rotate-270"></i>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu
|
||||
positionFixed
|
||||
flip={false}
|
||||
modifiers={{ preventOverflow: { boundariesElement: document.body } }}
|
||||
>
|
||||
{menuItem.subOpListHeader && <DropdownItem header>{menuItem.subOpListHeader}</DropdownItem>}
|
||||
{menuItem.subOpList.map((item, index) => {
|
||||
if (item == 'Divider') {
|
||||
return <DropdownItem key={index} divider />;
|
||||
} else {
|
||||
return (
|
||||
<DropdownItem key={index} data-toggle={item.key} onClick={this.onMenuItemClick} onKeyDown={this.onMenuItemKeyDown}>{item.value}</DropdownItem>
|
||||
<DropdownItem key={index} data-toggle={item.key} onClick={this.onMenuItemClick} onKeyDown={this.onMenuItemKeyDown}>
|
||||
{item.icon_dom || null}
|
||||
<span>{item.value}</span>
|
||||
</DropdownItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
@@ -73,7 +73,11 @@
|
||||
margin-left: 0;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
color: #999 !important;
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.tree-section .tree-section-more-operation .dropdown .sf-dropdown-toggle.sf3-font-new {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tree-section .tree-section-header .tree-section-more-operation {
|
||||
|
@@ -1,21 +1,14 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu';
|
||||
import { isMobile } from '../../utils/utils';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const TreeSection = ({ title, children, moreKey, moreOperations, moreOperationClick, className, isDisplayFiles }) => {
|
||||
const TreeSection = ({ title, children, renderHeaderOperations, className }) => {
|
||||
const [showChildren, setShowChildren] = useState(true);
|
||||
const [highlight, setHighlight] = useState(false);
|
||||
const [freeze, setFreeze] = useState(false);
|
||||
|
||||
const validMoreOperations = useMemo(() => {
|
||||
if (!Array.isArray(moreOperations) || moreOperations.length === 0) return [];
|
||||
return moreOperations.filter(operation => operation.key && operation.value);
|
||||
}, [moreOperations]);
|
||||
|
||||
const toggleShowChildren = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
@@ -39,6 +32,7 @@ const TreeSection = ({ title, children, moreKey, moreOperations, moreOperationCl
|
||||
|
||||
const freezeItem = useCallback(() => {
|
||||
setFreeze(true);
|
||||
setHighlight(true);
|
||||
}, []);
|
||||
|
||||
const unfreezeItem = useCallback(() => {
|
||||
@@ -46,6 +40,16 @@ const TreeSection = ({ title, children, moreKey, moreOperations, moreOperationCl
|
||||
setHighlight(false);
|
||||
}, []);
|
||||
|
||||
const renderOperations = useCallback(() => {
|
||||
if (!renderHeaderOperations) {
|
||||
return null;
|
||||
}
|
||||
return renderHeaderOperations({
|
||||
freezeItem,
|
||||
unfreezeItem,
|
||||
});
|
||||
}, [renderHeaderOperations, freezeItem, unfreezeItem]);
|
||||
|
||||
return (
|
||||
<div className={classnames('tree-section', className)}>
|
||||
<div
|
||||
@@ -56,22 +60,7 @@ const TreeSection = ({ title, children, moreKey, moreOperations, moreOperationCl
|
||||
>
|
||||
<div className="tree-section-header-title">{title}</div>
|
||||
<div className="tree-section-header-operations">
|
||||
{validMoreOperations.length > 0 && (
|
||||
<>
|
||||
<div className="tree-section-header-operation tree-section-more-operation">
|
||||
<ItemDropdownMenu
|
||||
item={moreKey}
|
||||
toggleClass="sf3-font sf3-font-more"
|
||||
freezeItem={freezeItem}
|
||||
unfreezeItem={unfreezeItem}
|
||||
getMenuList={() => validMoreOperations}
|
||||
onMenuItemClick={moreOperationClick}
|
||||
menuStyle={isMobile ? { zIndex: 1050 } : {}}
|
||||
isDisplayFiles={isDisplayFiles}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{renderOperations()}
|
||||
<div className="tree-section-header-operation" onClick={toggleShowChildren}>
|
||||
<i className={`sf3-font sf3-font-down ${showChildren ? '' : 'rotate-90'}`}></i>
|
||||
</div>
|
||||
@@ -88,12 +77,8 @@ const TreeSection = ({ title, children, moreKey, moreOperations, moreOperationCl
|
||||
|
||||
TreeSection.propTypes = {
|
||||
title: PropTypes.any.isRequired,
|
||||
moreOperations: PropTypes.array,
|
||||
children: PropTypes.any,
|
||||
moreKey: PropTypes.object,
|
||||
moreOperationClick: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
isDisplayFiles: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TreeSection;
|
||||
|
@@ -109,7 +109,28 @@ class MetadataManagerAPI {
|
||||
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
||||
};
|
||||
|
||||
// view
|
||||
// views
|
||||
addFolder = (repoID, name) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/folders/';
|
||||
const params = { name };
|
||||
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
||||
};
|
||||
|
||||
modifyFolder = (repoID, folder_id, folder_data) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/folders/';
|
||||
const params = {
|
||||
folder_id,
|
||||
folder_data,
|
||||
};
|
||||
return this.req.put(url, params);
|
||||
};
|
||||
|
||||
deleteFolder = (repoID, folder_id) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/folders/';
|
||||
const params = { folder_id };
|
||||
return this.req.delete(url, { data: params });
|
||||
};
|
||||
|
||||
listViews = (repoID) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/';
|
||||
return this.req.get(url);
|
||||
@@ -120,7 +141,7 @@ class MetadataManagerAPI {
|
||||
return this.req.get(url);
|
||||
};
|
||||
|
||||
addView = (repoID, name, type = 'table') => {
|
||||
addView = (repoID, name, type = 'table', folder_id = '') => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/';
|
||||
let params = {
|
||||
name,
|
||||
@@ -130,12 +151,18 @@ class MetadataManagerAPI {
|
||||
sorts: VIEW_TYPE_DEFAULT_SORTS[type],
|
||||
}
|
||||
};
|
||||
if (folder_id) {
|
||||
params.folder_id = folder_id;
|
||||
}
|
||||
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
||||
};
|
||||
|
||||
duplicateView = (repoID, viewId) => {
|
||||
duplicateView = (repoID, viewId, folder_id = '') => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/duplicate-view/';
|
||||
const params = { view_id: viewId };
|
||||
let params = { view_id: viewId };
|
||||
if (folder_id) {
|
||||
params.folder_id = folder_id;
|
||||
}
|
||||
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
||||
};
|
||||
|
||||
@@ -148,19 +175,22 @@ class MetadataManagerAPI {
|
||||
return this.req.put(url, params);
|
||||
};
|
||||
|
||||
deleteView = (repoID, viewId) => {
|
||||
deleteView = (repoID, viewId, folder_id = '') => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/';
|
||||
const params = {
|
||||
view_id: viewId,
|
||||
};
|
||||
let params = { view_id: viewId };
|
||||
if (folder_id) {
|
||||
params.folder_id = folder_id;
|
||||
}
|
||||
return this.req.delete(url, { data: params });
|
||||
};
|
||||
|
||||
moveView = (repoID, viewId, targetViewId) => {
|
||||
moveView = (repoID, source_view_id, source_folder_id, target_view_id, target_folder_id) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/move-views/';
|
||||
const params = {
|
||||
view_id: viewId,
|
||||
target_view_id: targetViewId,
|
||||
source_view_id,
|
||||
source_folder_id,
|
||||
target_view_id,
|
||||
target_folder_id,
|
||||
};
|
||||
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
||||
};
|
||||
|
@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
|
||||
import { useMetadata } from '../hooks';
|
||||
|
||||
const MetadataViewName = ({ id }) => {
|
||||
const { viewsMap } = useMetadata();
|
||||
const { idViewMap } = useMetadata();
|
||||
if (!id) return null;
|
||||
const view = viewsMap[id];
|
||||
const view = idViewMap[id];
|
||||
if (!view) return null;
|
||||
return (<>{view.name}</>);
|
||||
};
|
||||
|
@@ -1,96 +0,0 @@
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import { UncontrolledPopover } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from '../../../../../components/icon';
|
||||
import { gettext } from '../../../../../utils/constants';
|
||||
import { VIEW_TYPE, VIEW_TYPE_ICON } from '../../../../constants';
|
||||
|
||||
import '../index.css';
|
||||
|
||||
const VIEW_OPTIONS = [
|
||||
{
|
||||
key: 'table',
|
||||
type: VIEW_TYPE.TABLE,
|
||||
}, {
|
||||
key: 'gallery',
|
||||
type: VIEW_TYPE.GALLERY,
|
||||
}, {
|
||||
key: 'kanban',
|
||||
type: VIEW_TYPE.KANBAN,
|
||||
}, {
|
||||
key: 'map',
|
||||
type: VIEW_TYPE.MAP,
|
||||
}
|
||||
];
|
||||
|
||||
const AddView = ({ target, toggle, onOptionClick }) => {
|
||||
const popoverRef = useRef(null);
|
||||
|
||||
const handleClickOutside = useCallback((event) => {
|
||||
if (popoverRef.current && !popoverRef.current.contains(event.target)) {
|
||||
toggle(event);
|
||||
}
|
||||
}, [toggle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (popoverRef.current) {
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
};
|
||||
}, [handleClickOutside]);
|
||||
|
||||
const translateLabel = useCallback((type) => {
|
||||
switch (type) {
|
||||
case VIEW_TYPE.TABLE:
|
||||
return gettext('Table');
|
||||
case VIEW_TYPE.GALLERY:
|
||||
return gettext('Gallery');
|
||||
case VIEW_TYPE.KANBAN:
|
||||
return gettext('Kanban');
|
||||
case VIEW_TYPE.MAP:
|
||||
return gettext('Map');
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<UncontrolledPopover
|
||||
className='sf-metadata-addview-popover'
|
||||
isOpen={true}
|
||||
toggle={toggle}
|
||||
target={target}
|
||||
placement='right-start'
|
||||
hideArrow={true}
|
||||
fade={false}
|
||||
boundariesElement={document.body}
|
||||
>
|
||||
<div ref={popoverRef}>
|
||||
<div className='sf-metadata-addview-popover-header'>{gettext('New view')}</div>
|
||||
<div className='sf-metadata-addview-popover-body'>
|
||||
{VIEW_OPTIONS.map((item, index) => {
|
||||
return (
|
||||
<button key={index} className='dropdown-item sf-metadata-addview-popover-item' onClick={() => onOptionClick(item)}>
|
||||
<div className="left-icon">
|
||||
<Icon symbol={VIEW_TYPE_ICON[item.type] || 'table'} className='metadata-view-icon' />
|
||||
</div>
|
||||
<div>{translateLabel(item.type)}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</UncontrolledPopover>
|
||||
);
|
||||
};
|
||||
|
||||
AddView.propTypes = {
|
||||
target: PropTypes.string.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
onOptionClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddView;
|
@@ -1,51 +0,0 @@
|
||||
.sf-metadata-addview-popover .popover {
|
||||
min-width: 280px;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.sf-metadata-view-form {
|
||||
display: flex;
|
||||
padding-left: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.sf-metadata-addview-popover .sf-metadata-addview-popover-header {
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
color: #666;
|
||||
opacity: 1;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.sf-metadata-addview-popover
|
||||
.sf-metadata-addview-popover-body {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dropdown-item.sf-metadata-addview-popover-item {
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item.sf-metadata-addview-popover-item .metadata-view-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.dropdown-item:hover .metadata-view-icon {
|
||||
color: #fff;
|
||||
fill: #fff;
|
||||
}
|
@@ -1 +0,0 @@
|
||||
export { default as AddView } from './add-view';
|
@@ -9,9 +9,9 @@ import { VIEW_TYPE } from '../../constants';
|
||||
import './index.css';
|
||||
|
||||
const ViewDetails = ({ viewId, onClose }) => {
|
||||
const { viewsMap } = useMetadata();
|
||||
const { idViewMap } = useMetadata();
|
||||
|
||||
const view = useMemo(() => viewsMap[viewId], [viewId, viewsMap]);
|
||||
const view = useMemo(() => idViewMap[viewId], [viewId, idViewMap]);
|
||||
const icon = useMemo(() => {
|
||||
const type = view.type;
|
||||
if (type === VIEW_TYPE.GALLERY) return `${mediaUrl}favicons/gallery.png`;
|
||||
|
@@ -3,6 +3,10 @@
|
||||
*/
|
||||
|
||||
export const EVENT_BUS_TYPE = {
|
||||
// folder/views
|
||||
ADD_FOLDER: 'add_folder',
|
||||
ADD_VIEW: 'add_view',
|
||||
|
||||
QUERY_COLLABORATORS: 'query_collaborators',
|
||||
QUERY_COLLABORATOR: 'query_collaborator',
|
||||
UPDATE_TABLE_ROWS: 'update_table_rows',
|
||||
|
@@ -4,6 +4,16 @@ import { SORT_COLUMN_OPTIONS, GALLERY_SORT_COLUMN_OPTIONS, GALLERY_FIRST_SORT_CO
|
||||
GALLERY_SORT_PRIVATE_COLUMN_KEYS, GALLERY_FIRST_SORT_PRIVATE_COLUMN_KEYS,
|
||||
} from './sort';
|
||||
|
||||
export const METADATA_VIEWS_KEY = 'sf-metadata-views';
|
||||
|
||||
export const METADATA_VIEWS_DRAG_DATA_KEY = 'application/drag-sf-metadata-views';
|
||||
|
||||
export const TREE_NODE_LEFT_INDENT = 20;
|
||||
|
||||
export const VIEWS_TYPE_FOLDER = 'folder';
|
||||
|
||||
export const VIEWS_TYPE_VIEW = 'view';
|
||||
|
||||
export const VIEW_TYPE = {
|
||||
TABLE: 'table',
|
||||
GALLERY: 'gallery',
|
||||
|
@@ -2,13 +2,16 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'rea
|
||||
import metadataAPI from '../api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../../components/toast';
|
||||
import Folder from '../model/metadata/folder';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { FACE_RECOGNITION_VIEW_ID, VIEW_TYPE } from '../constants';
|
||||
import { FACE_RECOGNITION_VIEW_ID, VIEW_TYPE, VIEWS_TYPE_FOLDER, VIEWS_TYPE_VIEW } from '../constants';
|
||||
import { useMetadataStatus } from '../../hooks';
|
||||
import { updateFavicon } from '../utils/favicon';
|
||||
import { getViewName } from '../utils/view';
|
||||
|
||||
const CACHED_COLLAPSED_FOLDERS_PREFIX = 'sf-metadata-collapsed-folders';
|
||||
|
||||
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
|
||||
const MetadataContext = React.createContext(null);
|
||||
|
||||
@@ -16,13 +19,35 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [enableFaceRecognition, setEnableFaceRecognition] = useState(false);
|
||||
const [navigation, setNavigation] = useState([]);
|
||||
const [, setCount] = useState(0);
|
||||
const [idViewMap, setIdViewMap] = useState({});
|
||||
|
||||
const viewsMap = useRef({});
|
||||
const collapsedFoldersIds = useRef([]);
|
||||
const originalTitleRef = useRef(document.title);
|
||||
|
||||
const { enableMetadata, isBeingBuilt, setIsBeingBuilt } = useMetadataStatus();
|
||||
|
||||
const getCollapsedFolders = useCallback(() => {
|
||||
const strFoldedFolders = window.localStorage.getItem(`${CACHED_COLLAPSED_FOLDERS_PREFIX}-${repoID}`);
|
||||
const foldedFolders = strFoldedFolders && JSON.parse(strFoldedFolders);
|
||||
return Array.isArray(foldedFolders) ? foldedFolders : [];
|
||||
}, [repoID]);
|
||||
|
||||
const setCollapsedFolders = useCallback((collapsedFoldersIds) => {
|
||||
window.localStorage.setItem(`${CACHED_COLLAPSED_FOLDERS_PREFIX}-${repoID}`, JSON.stringify(collapsedFoldersIds));
|
||||
}, [repoID]);
|
||||
|
||||
const addViewIntoMap = useCallback((viewId, view) => {
|
||||
let updatedIdViewInMap = { ...idViewMap };
|
||||
updatedIdViewInMap[viewId] = view;
|
||||
setIdViewMap(updatedIdViewInMap);
|
||||
}, [idViewMap]);
|
||||
|
||||
const deleteViewFromMap = useCallback((viewId) => {
|
||||
let updatedIdViewInMap = { ...idViewMap };
|
||||
delete updatedIdViewInMap[viewId];
|
||||
setIdViewMap(updatedIdViewInMap);
|
||||
}, [idViewMap]);
|
||||
|
||||
// views
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
@@ -30,9 +55,11 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
metadataAPI.listViews(repoID).then(res => {
|
||||
const { navigation, views } = res.data;
|
||||
if (Array.isArray(views)) {
|
||||
let idViewMap = {};
|
||||
views.forEach(view => {
|
||||
viewsMap.current[view._id] = { ...view, name: getViewName(view) };
|
||||
idViewMap[view._id] = { ...view, name: getViewName(view) };
|
||||
});
|
||||
setIdViewMap(idViewMap);
|
||||
}
|
||||
setNavigation(navigation);
|
||||
setLoading(false);
|
||||
@@ -45,7 +72,7 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
}
|
||||
hideMetadataView && hideMetadataView();
|
||||
setEnableFaceRecognition(false);
|
||||
viewsMap.current = {};
|
||||
setIdViewMap({});
|
||||
setNavigation([]);
|
||||
setLoading(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -64,6 +91,15 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
});
|
||||
}, [repoID, enableMetadata]);
|
||||
|
||||
const getFirstView = useCallback(() => {
|
||||
const firstViewNav = navigation.find(item => item.type === VIEWS_TYPE_VIEW);
|
||||
const firstView = firstViewNav ? idViewMap[firstViewNav._id] : null;
|
||||
if (!firstView && Object.keys(idViewMap).length > 0) {
|
||||
return idViewMap[Object.keys(idViewMap)[0]];
|
||||
}
|
||||
return firstView;
|
||||
}, [navigation, idViewMap]);
|
||||
|
||||
const selectView = useCallback((view, isSelected) => {
|
||||
if (isSelected) return;
|
||||
const node = {
|
||||
@@ -87,95 +123,284 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [repoID, selectMetadataView]);
|
||||
|
||||
const addView = useCallback((name, type, successCallback, failCallback) => {
|
||||
metadataAPI.addView(repoID, name, type).then(res => {
|
||||
const view = res.data.view;
|
||||
let newNavigation = navigation.slice(0);
|
||||
newNavigation.push({ _id: view._id, type: 'view' });
|
||||
viewsMap.current[view._id] = { ...view, name: getViewName(view) };
|
||||
useEffect(() => {
|
||||
collapsedFoldersIds.current = getCollapsedFolders();
|
||||
}, [getCollapsedFolders]);
|
||||
|
||||
const collapseFolder = useCallback((folderId) => {
|
||||
let updatedCollapsedFoldersIds = getCollapsedFolders();
|
||||
if (updatedCollapsedFoldersIds.includes(folderId)) {
|
||||
return;
|
||||
}
|
||||
updatedCollapsedFoldersIds.push(folderId);
|
||||
setCollapsedFolders(updatedCollapsedFoldersIds);
|
||||
}, [getCollapsedFolders, setCollapsedFolders]);
|
||||
|
||||
const expandFolder = useCallback((folderId) => {
|
||||
let updatedCollapsedFoldersIds = getCollapsedFolders();
|
||||
if (!updatedCollapsedFoldersIds.includes(folderId)) {
|
||||
return;
|
||||
}
|
||||
updatedCollapsedFoldersIds = updatedCollapsedFoldersIds.filter((collapsedFolderId) => collapsedFolderId !== folderId);
|
||||
setCollapsedFolders(updatedCollapsedFoldersIds);
|
||||
}, [getCollapsedFolders, setCollapsedFolders]);
|
||||
|
||||
const addFolder = useCallback((name, successCallback, failCallback) => {
|
||||
metadataAPI.addFolder(repoID, name).then(res => {
|
||||
let newNavigation = [...navigation];
|
||||
const folder = new Folder(res.data.folder);
|
||||
newNavigation.push(folder);
|
||||
setNavigation(newNavigation);
|
||||
selectView(view);
|
||||
successCallback && successCallback();
|
||||
}).catch(error => {
|
||||
failCallback && failCallback(error);
|
||||
});
|
||||
}, [navigation, repoID, viewsMap, selectView]);
|
||||
}, [repoID, navigation]);
|
||||
|
||||
const duplicateView = useCallback((viewId) => {
|
||||
metadataAPI.duplicateView(repoID, viewId).then(res => {
|
||||
const view = res.data.view;
|
||||
let newNavigation = navigation.slice(0);
|
||||
newNavigation.push({ _id: view._id, type: 'view' });
|
||||
viewsMap.current[view._id] = view;
|
||||
const modifyFolder = useCallback((folderId, updates, successCallback, failCallback) => {
|
||||
metadataAPI.modifyFolder(repoID, folderId, updates).then(res => {
|
||||
let newNavigation = [...navigation];
|
||||
let folderIndex = newNavigation.findIndex((nav) => nav._id === folderId && nav.type === VIEWS_TYPE_FOLDER);
|
||||
if (folderIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const validUpdates = { ...updates };
|
||||
delete validUpdates._id;
|
||||
delete validUpdates.type;
|
||||
delete validUpdates.children;
|
||||
let updatedFolder = newNavigation[folderIndex];
|
||||
updatedFolder = Object.assign({}, updatedFolder, validUpdates);
|
||||
newNavigation[folderIndex] = updatedFolder;
|
||||
setNavigation(newNavigation);
|
||||
selectView(view);
|
||||
successCallback && successCallback();
|
||||
}).catch(error => {
|
||||
failCallback && failCallback(error);
|
||||
});
|
||||
}, [repoID, navigation]);
|
||||
|
||||
const deleteFolder = useCallback((folderId) => {
|
||||
metadataAPI.deleteFolder(repoID, folderId).then(res => {
|
||||
let newNavigation = [...navigation];
|
||||
let folderIndex = newNavigation.findIndex((nav) => nav._id === folderId && nav.type === VIEWS_TYPE_FOLDER);
|
||||
if (folderIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const viewsInFolder = newNavigation[folderIndex].children;
|
||||
newNavigation.splice(folderIndex, 1);
|
||||
if (viewsInFolder.length > 0) {
|
||||
newNavigation.push(...viewsInFolder);
|
||||
}
|
||||
setNavigation(newNavigation);
|
||||
}).catch((error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
}));
|
||||
}, [repoID, navigation]);
|
||||
|
||||
const addViewCallback = useCallback((view, folderId) => {
|
||||
const newViewNav = { _id: view._id, type: VIEWS_TYPE_VIEW };
|
||||
let newNavigation = [...navigation];
|
||||
if (folderId) {
|
||||
// add view into folder
|
||||
const folderIndex = newNavigation.findIndex((nav) => nav._id === folderId && nav.type === VIEWS_TYPE_FOLDER);
|
||||
if (folderIndex < 0) {
|
||||
return;
|
||||
}
|
||||
let updatedFolder = newNavigation[folderIndex];
|
||||
updatedFolder.children = Array.isArray(updatedFolder.children) ? updatedFolder.children : [];
|
||||
updatedFolder.children.push(newViewNav);
|
||||
} else {
|
||||
newNavigation.push(newViewNav);
|
||||
}
|
||||
const newView = { ...view, name: getViewName(view) };
|
||||
addViewIntoMap(newView._id, newView);
|
||||
setNavigation(newNavigation);
|
||||
selectView(newView);
|
||||
}, [navigation, addViewIntoMap, setNavigation, selectView]);
|
||||
|
||||
const addView = useCallback(({ folderId, name, type, successCallback, failCallback }) => {
|
||||
metadataAPI.addView(repoID, name, type, folderId).then(res => {
|
||||
const view = res.data.view;
|
||||
addViewCallback(view, folderId);
|
||||
successCallback && successCallback();
|
||||
}).catch(error => {
|
||||
failCallback && failCallback(error);
|
||||
});
|
||||
}, [repoID, addViewCallback]);
|
||||
|
||||
const duplicateView = useCallback(({ folderId, viewId }) => {
|
||||
metadataAPI.duplicateView(repoID, viewId, folderId).then(res => {
|
||||
const view = res.data.view;
|
||||
addViewCallback(view, folderId);
|
||||
}).catch(error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}, [navigation, repoID, viewsMap, selectView]);
|
||||
}, [repoID, addViewCallback]);
|
||||
|
||||
const deleteView = useCallback(({ folderId, viewId, isSelected }) => {
|
||||
metadataAPI.deleteView(repoID, viewId, folderId).then(res => {
|
||||
let newNavigation = [...navigation];
|
||||
let prevViewNav = null;
|
||||
if (folderId) {
|
||||
let folderIndex = newNavigation.findIndex((nav) => nav._id === folderId && nav.type === VIEWS_TYPE_FOLDER);
|
||||
if (folderIndex < 0) {
|
||||
return;
|
||||
}
|
||||
let updatedFolder = newNavigation[folderIndex];
|
||||
if (!Array.isArray(updatedFolder.children) || updatedFolder.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentViewIndex = updatedFolder.children.findIndex((viewNav) => viewNav._id === viewId);
|
||||
prevViewNav = updatedFolder.children[currentViewIndex - 1];
|
||||
updatedFolder.children = updatedFolder.children.filter(viewNav => viewNav._id !== viewId);
|
||||
} else {
|
||||
const currentViewIndex = newNavigation.findIndex(item => item._id === viewId);
|
||||
prevViewNav = newNavigation[currentViewIndex - 1];
|
||||
newNavigation = newNavigation.filter(nav => nav._id !== viewId);
|
||||
}
|
||||
|
||||
const deleteView = useCallback((viewId, isSelected) => {
|
||||
metadataAPI.deleteView(repoID, viewId).then(res => {
|
||||
const newNavigation = navigation.filter(item => item._id !== viewId);
|
||||
delete viewsMap.current[viewId];
|
||||
setNavigation(newNavigation);
|
||||
deleteViewFromMap(viewId);
|
||||
|
||||
// re-select the previous view
|
||||
if (isSelected) {
|
||||
const currentViewIndex = navigation.findIndex(item => item._id === viewId);
|
||||
const lastViewId = navigation[currentViewIndex - 1]._id;
|
||||
const lastView = viewsMap.current[lastViewId];
|
||||
selectView(lastView);
|
||||
let prevView = null;
|
||||
if (prevViewNav && prevViewNav.type === VIEWS_TYPE_VIEW) {
|
||||
prevView = idViewMap[prevViewNav._id];
|
||||
}
|
||||
if (!prevView) {
|
||||
prevView = getFirstView();
|
||||
}
|
||||
selectView(prevView);
|
||||
}
|
||||
}).catch((error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
}));
|
||||
}, [repoID, navigation, selectView, viewsMap]);
|
||||
}, [repoID, navigation, idViewMap, deleteViewFromMap, getFirstView, selectView]);
|
||||
|
||||
const updateView = useCallback((viewId, update, successCallback, failCallback) => {
|
||||
metadataAPI.modifyView(repoID, viewId, update).then(res => {
|
||||
const currentView = viewsMap.current[viewId];
|
||||
viewsMap.current[viewId] = { ...currentView, ...update };
|
||||
setCount(n => n + 1);
|
||||
const currentView = idViewMap[viewId];
|
||||
addViewIntoMap(viewId, { ...currentView, ...update });
|
||||
successCallback && successCallback();
|
||||
}).catch(error => {
|
||||
failCallback && failCallback(error);
|
||||
});
|
||||
}, [repoID, viewsMap]);
|
||||
}, [repoID, idViewMap, addViewIntoMap]);
|
||||
|
||||
const moveView = useCallback((sourceViewId, targetViewId) => {
|
||||
metadataAPI.moveView(repoID, sourceViewId, targetViewId).then(res => {
|
||||
const { navigation } = res.data;
|
||||
setNavigation(navigation);
|
||||
const moveView = useCallback(({ sourceViewId, sourceFolderId, targetViewId, targetFolderId }) => {
|
||||
if (
|
||||
(!sourceViewId && !sourceFolderId) // must drag view or folder
|
||||
|| (!targetViewId && !targetFolderId) // must move above to view/folder or move view into folder
|
||||
|| (sourceViewId === targetViewId && sourceFolderId === targetFolderId) // not changed
|
||||
|| (!sourceViewId && sourceFolderId && targetViewId && targetFolderId) // not allowed to drag folder into folder
|
||||
) {
|
||||
return;
|
||||
}
|
||||
metadataAPI.moveView(repoID, sourceViewId, sourceFolderId, targetViewId, targetFolderId).then(res => {
|
||||
let newNavigation = [...navigation];
|
||||
|
||||
// remove folder/view from old position
|
||||
let updatedSourceNavList = null;
|
||||
let sourceId = null;
|
||||
if (sourceFolderId) {
|
||||
if (sourceViewId) {
|
||||
// drag view from folder
|
||||
const sourceFolder = newNavigation.find((folder) => folder._id === sourceFolderId);
|
||||
updatedSourceNavList = sourceFolder && sourceFolder.children;
|
||||
sourceId = sourceViewId;
|
||||
} else {
|
||||
// drag folder
|
||||
updatedSourceNavList = newNavigation;
|
||||
sourceId = sourceFolderId;
|
||||
}
|
||||
} else if (sourceViewId) {
|
||||
// drag view outer of folders
|
||||
updatedSourceNavList = newNavigation;
|
||||
sourceId = sourceViewId;
|
||||
}
|
||||
|
||||
// invalid drag source
|
||||
if (!Array.isArray(updatedSourceNavList) || updatedSourceNavList.length === 0 || !sourceId) {
|
||||
return;
|
||||
}
|
||||
const movedNavIndex = updatedSourceNavList.findIndex((nav) => nav._id === sourceId);
|
||||
if (movedNavIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const movedNav = updatedSourceNavList[movedNavIndex];
|
||||
updatedSourceNavList.splice(movedNavIndex, 1);
|
||||
|
||||
// insert folder/view into new position
|
||||
let updatedTargetNavList = newNavigation;
|
||||
if (targetFolderId && sourceViewId) {
|
||||
// move view into folder
|
||||
let targetFolder = newNavigation.find((folder) => folder._id === targetFolderId);
|
||||
if (!Array.isArray(targetFolder.children)) {
|
||||
targetFolder.children = [];
|
||||
}
|
||||
updatedTargetNavList = targetFolder.children;
|
||||
}
|
||||
|
||||
let targetNavIndex = -1;
|
||||
if (targetViewId) {
|
||||
// move folder/view above to view
|
||||
targetNavIndex = updatedTargetNavList.findIndex((nav) => nav._id === targetViewId);
|
||||
} else if (!sourceViewId && targetFolderId) {
|
||||
// move folder above to folder
|
||||
targetNavIndex = updatedTargetNavList.findIndex((nav) => nav._id === targetFolderId);
|
||||
}
|
||||
|
||||
if (targetNavIndex > -1) {
|
||||
updatedTargetNavList.splice(targetNavIndex, 0, movedNav); // move above to the target folder/view
|
||||
} else {
|
||||
updatedTargetNavList.push(movedNav); // move into navigation or folder
|
||||
}
|
||||
|
||||
setNavigation(newNavigation);
|
||||
}).catch(error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}, [repoID]);
|
||||
}, [repoID, navigation]);
|
||||
|
||||
const updateEnableFaceRecognition = useCallback((newValue) => {
|
||||
if (newValue === enableFaceRecognition) return;
|
||||
if (newValue) {
|
||||
toaster.success(gettext('Recognizing portraits. Please refresh the page later.'));
|
||||
addView('_people', VIEW_TYPE.FACE_RECOGNITION, () => {}, () => {});
|
||||
addView({ name: '_people', type: VIEW_TYPE.FACE_RECOGNITION });
|
||||
} else {
|
||||
if (viewsMap.current[FACE_RECOGNITION_VIEW_ID]) {
|
||||
if (idViewMap[FACE_RECOGNITION_VIEW_ID]) {
|
||||
let isSelected = false;
|
||||
if (currentPath.includes('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/')) {
|
||||
const currentViewId = currentPath.split('/').pop();
|
||||
isSelected = currentViewId === FACE_RECOGNITION_VIEW_ID;
|
||||
}
|
||||
deleteView(FACE_RECOGNITION_VIEW_ID, isSelected);
|
||||
const folders = navigation.filter((nav) => nav.type === VIEWS_TYPE_FOLDER);
|
||||
const targetFolder = folders.find((folder) => {
|
||||
const { children } = folder;
|
||||
if (Array.isArray(children) && children.length > 0) {
|
||||
const view = children.find((viewNav) => viewNav._id === FACE_RECOGNITION_VIEW_ID);
|
||||
if (view) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const folderId = targetFolder ? targetFolder._id : null;
|
||||
deleteView({ folderId, viewId: FACE_RECOGNITION_VIEW_ID, isSelected });
|
||||
}
|
||||
}
|
||||
setEnableFaceRecognition(newValue);
|
||||
}, [enableFaceRecognition, currentPath, addView, deleteView]);
|
||||
}, [enableFaceRecognition, currentPath, idViewMap, navigation, addView, deleteView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
if (isBeingBuilt) {
|
||||
const firstViewObject = navigation.find(item => item.type === 'view');
|
||||
const firstView = firstViewObject ? viewsMap.current[firstViewObject._id] : '';
|
||||
const firstView = getFirstView();
|
||||
if (firstView) {
|
||||
selectView(firstView);
|
||||
}
|
||||
@@ -186,7 +411,7 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
if (!urlParams.has('view')) return;
|
||||
const viewID = urlParams.get('view');
|
||||
if (viewID) {
|
||||
const lastOpenedView = viewsMap.current[viewID] || '';
|
||||
const lastOpenedView = idViewMap[viewID] || '';
|
||||
if (lastOpenedView) {
|
||||
selectView(lastOpenedView);
|
||||
return;
|
||||
@@ -195,8 +420,7 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
window.history.pushState({ url: url, path: '' }, '', url);
|
||||
}
|
||||
|
||||
const firstViewObject = navigation.find(item => item.type === 'view');
|
||||
const firstView = firstViewObject ? viewsMap.current[firstViewObject._id] : '';
|
||||
const firstView = getFirstView();
|
||||
if (firstView) {
|
||||
selectView(firstView);
|
||||
}
|
||||
@@ -206,7 +430,7 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
useEffect(() => {
|
||||
if (!currentPath.includes('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/')) return;
|
||||
const currentViewId = currentPath.split('/').pop();
|
||||
const currentView = viewsMap.current[currentViewId];
|
||||
const currentView = idViewMap[currentViewId];
|
||||
if (currentView) {
|
||||
document.title = `${currentView.name} - Seafile`;
|
||||
updateFavicon(currentView.type);
|
||||
@@ -214,7 +438,7 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
}
|
||||
document.title = originalTitleRef.current;
|
||||
updateFavicon('default');
|
||||
}, [currentPath, viewsMap]);
|
||||
}, [currentPath, idViewMap]);
|
||||
|
||||
return (
|
||||
<MetadataContext.Provider value={{
|
||||
@@ -223,7 +447,13 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataVi
|
||||
isBeingBuilt,
|
||||
setIsBeingBuilt,
|
||||
navigation,
|
||||
viewsMap: viewsMap.current,
|
||||
collapsedFoldersIds: collapsedFoldersIds.current,
|
||||
idViewMap,
|
||||
collapseFolder,
|
||||
expandFolder,
|
||||
addFolder,
|
||||
modifyFolder,
|
||||
deleteFolder,
|
||||
selectView,
|
||||
addView,
|
||||
duplicateView,
|
||||
|
313
frontend/src/metadata/metadata-tree-view/folder.js
Normal file
313
frontend/src/metadata/metadata-tree-view/folder.js
Normal file
@@ -0,0 +1,313 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ViewItem from './view';
|
||||
import ItemDropdownMenu from '../../components/dropdown-menu/item-dropdown-menu';
|
||||
import toaster from '../../components/toast';
|
||||
import NewView from './new-view';
|
||||
import InlineNameEditor from './inline-name-editor';
|
||||
import { useMetadata } from '../hooks';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { Utils, isMobile } from '../../utils/utils';
|
||||
import TextTranslation from '../../utils/text-translation';
|
||||
import { validateName } from '../utils/validate';
|
||||
import { METADATA_VIEWS_DRAG_DATA_KEY, METADATA_VIEWS_KEY, TREE_NODE_LEFT_INDENT, VIEW_TYPE, VIEWS_TYPE_FOLDER, VIEWS_TYPE_VIEW } from '../constants';
|
||||
import { getNewViewMenuItem, KEY_ADD_VIEW_MAP } from '../../components/dir-view-mode/dir-views/new-view-menu';
|
||||
|
||||
const ViewsFolder = ({
|
||||
leftIndent, folder, currentPath, userPerm, canDeleteView, getFoldersNames, getMoveableFolders, generateNewViewDefaultName,
|
||||
setDragMode, getDragMode, selectView, modifyView,
|
||||
}) => {
|
||||
const {
|
||||
idViewMap, collapsedFoldersIds, collapseFolder, expandFolder, modifyFolder, deleteFolder,
|
||||
deleteView, duplicateView, addView, moveView,
|
||||
} = useMetadata();
|
||||
const { _id: folderId, name: folderName, children } = folder || {};
|
||||
const [expanded, setExpanded] = useState(!collapsedFoldersIds.includes(folderId));
|
||||
const [highlight, setHighlight] = useState(false);
|
||||
const [freeze, setFreeze] = useState(false);
|
||||
const [isRenaming, setRenaming] = useState(false);
|
||||
const [newView, setNewView] = useState(null);
|
||||
const [isDropShow, setDropShow] = useState(false);
|
||||
|
||||
const canUpdate = useMemo(() => {
|
||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
||||
return true;
|
||||
}, [userPerm]);
|
||||
|
||||
const canDrop = useMemo(() => {
|
||||
if (Utils.isIEBrowser() || !canUpdate) return false;
|
||||
return true;
|
||||
}, [canUpdate]);
|
||||
|
||||
const folderMoreOperationMenus = useMemo(() => {
|
||||
let menus = [];
|
||||
if (canUpdate) {
|
||||
menus.push(
|
||||
getNewViewMenuItem(),
|
||||
TextTranslation.RENAME,
|
||||
TextTranslation.DELETE,
|
||||
);
|
||||
}
|
||||
return menus;
|
||||
}, [canUpdate]);
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
if (freeze) return;
|
||||
setHighlight(true);
|
||||
}, [freeze]);
|
||||
|
||||
const onMouseOver = useCallback(() => {
|
||||
if (freeze) return;
|
||||
setHighlight(true);
|
||||
}, [freeze]);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
if (freeze) return;
|
||||
setHighlight(false);
|
||||
}, [freeze]);
|
||||
|
||||
const freezeItem = useCallback(() => {
|
||||
setFreeze(true);
|
||||
setHighlight(true);
|
||||
}, []);
|
||||
|
||||
const unfreezeItem = useCallback(() => {
|
||||
setFreeze(false);
|
||||
setHighlight(false);
|
||||
}, []);
|
||||
|
||||
const clickFolder = useCallback(() => {
|
||||
if (expanded) {
|
||||
collapseFolder(folderId);
|
||||
} else {
|
||||
expandFolder(folderId);
|
||||
}
|
||||
setExpanded(!expanded);
|
||||
}, [expanded, folderId, collapseFolder, expandFolder]);
|
||||
|
||||
const prepareAddView = useCallback((viewType) => {
|
||||
setNewView({ key: viewType, type: viewType, default_name: generateNewViewDefaultName() });
|
||||
}, [generateNewViewDefaultName]);
|
||||
|
||||
const clickMenu = useCallback((operationKey) => {
|
||||
switch (operationKey) {
|
||||
case KEY_ADD_VIEW_MAP.ADD_TABLE: {
|
||||
prepareAddView(VIEW_TYPE.TABLE);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_GALLERY: {
|
||||
prepareAddView(VIEW_TYPE.GALLERY);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_KANBAN: {
|
||||
prepareAddView(VIEW_TYPE.KANBAN);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_MAP: {
|
||||
prepareAddView(VIEW_TYPE.MAP);
|
||||
return;
|
||||
}
|
||||
case TextTranslation.RENAME.key: {
|
||||
setRenaming(true);
|
||||
return;
|
||||
}
|
||||
case TextTranslation.DELETE.key: {
|
||||
deleteFolder(folderId);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [prepareAddView, folderId, deleteFolder]);
|
||||
|
||||
const onDragStart = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
const dragData = JSON.stringify({ type: METADATA_VIEWS_KEY, folder_id: folderId, mode: VIEWS_TYPE_FOLDER });
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData(METADATA_VIEWS_DRAG_DATA_KEY, dragData);
|
||||
setDragMode(VIEWS_TYPE_FOLDER);
|
||||
}, [canDrop, folderId, setDragMode]);
|
||||
|
||||
const onDragEnter = useCallback((event) => {
|
||||
if (!canDrop) {
|
||||
// not allowed drag folder into folder
|
||||
return false;
|
||||
}
|
||||
if (!canDrop) {
|
||||
return false;
|
||||
}
|
||||
setDropShow(true);
|
||||
}, [canDrop]);
|
||||
|
||||
const onDragLeave = useCallback(() => {
|
||||
if (!canDrop) return false;
|
||||
setDropShow(false);
|
||||
}, [canDrop]);
|
||||
|
||||
const onDragMove = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, [canDrop]);
|
||||
|
||||
const onDrop = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
event.stopPropagation();
|
||||
setDropShow(false);
|
||||
|
||||
let dragData = event.dataTransfer.getData(METADATA_VIEWS_DRAG_DATA_KEY);
|
||||
if (!dragData) return;
|
||||
dragData = JSON.parse(dragData);
|
||||
if (dragData.type !== METADATA_VIEWS_KEY) return false;
|
||||
const dragMode = getDragMode();
|
||||
const { view_id: sourceViewId, folder_id: sourceFolderId } = dragData;
|
||||
if ((dragMode === VIEWS_TYPE_VIEW && !sourceViewId)) {
|
||||
return;
|
||||
}
|
||||
moveView({ sourceViewId, sourceFolderId, targetFolderId: folderId });
|
||||
}, [canDrop, folderId, getDragMode, moveView]);
|
||||
|
||||
const onConfirmRename = useCallback((name) => {
|
||||
const foldersNames = getFoldersNames();
|
||||
const otherFoldersNames = foldersNames.filter((currFolderName) => currFolderName !== folderName);
|
||||
const { isValid, message } = validateName(name, otherFoldersNames);
|
||||
if (!isValid) {
|
||||
toaster.danger(message);
|
||||
return;
|
||||
}
|
||||
if (message === folderName) {
|
||||
setRenaming(false);
|
||||
return;
|
||||
}
|
||||
modifyFolder(folderId, { name: message });
|
||||
setRenaming(false);
|
||||
}, [folderId, folderName, getFoldersNames, modifyFolder]);
|
||||
|
||||
const closeNewView = useCallback(() => {
|
||||
setNewView(null);
|
||||
}, []);
|
||||
|
||||
const addViewIntoFolder = useCallback((viewName, viewType) => {
|
||||
addView({ folderId, name: viewName, type: viewType });
|
||||
}, [folderId, addView]);
|
||||
|
||||
const deleteViewFromFolder = useCallback((viewId, isSelected) => {
|
||||
deleteView({ folderId, viewId, isSelected });
|
||||
}, [folderId, deleteView]);
|
||||
|
||||
const duplicateViewFromFolder = useCallback((viewId) => {
|
||||
duplicateView({ folderId, viewId });
|
||||
}, [folderId, duplicateView]);
|
||||
|
||||
const renderViews = () => {
|
||||
if (!Array.isArray(children) || children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return children.map((children) => {
|
||||
const { _id: viewId, type: childType } = children || {};
|
||||
if (!viewId || childType !== VIEWS_TYPE_VIEW) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const view = idViewMap[viewId];
|
||||
if (!view) return null;
|
||||
|
||||
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + viewId;
|
||||
const isSelected = currentPath === viewPath;
|
||||
return (
|
||||
<ViewItem
|
||||
key={viewId}
|
||||
folderId={folderId}
|
||||
leftIndent={leftIndent + TREE_NODE_LEFT_INDENT}
|
||||
canDelete={canDeleteView}
|
||||
isSelected={isSelected}
|
||||
userPerm={userPerm}
|
||||
view={view}
|
||||
getMoveableFolders={getMoveableFolders}
|
||||
setDragMode={setDragMode}
|
||||
getDragMode={getDragMode}
|
||||
onClick={selectView}
|
||||
onDelete={deleteViewFromFolder}
|
||||
onCopy={duplicateViewFromFolder}
|
||||
onUpdate={modifyView}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tree-node views-folder-wrapper">
|
||||
<div
|
||||
className={classnames('tree-node-inner views-folder-main text-nowrap', { 'tree-node-inner-hover': highlight, 'tree-node-drop': isDropShow })}
|
||||
type="dir"
|
||||
title={folderName}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={clickFolder}
|
||||
>
|
||||
<div
|
||||
className="tree-node-text views-folder-name"
|
||||
style={{ paddingLeft: leftIndent + 5 }}
|
||||
draggable={canUpdate}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOver={onDragMove}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{isRenaming ? (
|
||||
<InlineNameEditor
|
||||
name={folderName}
|
||||
className="rename mt-0"
|
||||
onSubmit={onConfirmRename}
|
||||
/>
|
||||
) : folderName}
|
||||
</div>
|
||||
<div className="left-icon" style={{ left: leftIndent - 40 }}>
|
||||
<i className={classnames('folder-toggle-icon sf3-font sf3-font-down', { 'rotate-270': !expanded })}></i>
|
||||
<span className="tree-node-icon">
|
||||
<i className="sf3-font sf3-font-folder"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="right-icon">
|
||||
{(highlight && folderMoreOperationMenus.length > 0) && (
|
||||
<ItemDropdownMenu
|
||||
item={{ name: 'metadata-folder' }}
|
||||
menuClassname="metadata-views-dropdown-menu"
|
||||
toggleClass="sf3-font sf3-font-more"
|
||||
freezeItem={freezeItem}
|
||||
unfreezeItem={unfreezeItem}
|
||||
getMenuList={() => folderMoreOperationMenus}
|
||||
onMenuItemClick={clickMenu}
|
||||
menuStyle={isMobile ? { zIndex: 1050 } : {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="children views-folder-children">
|
||||
{expanded && renderViews()}
|
||||
{newView && <NewView newView={newView} leftIndent={TREE_NODE_LEFT_INDENT * 3} closeNewView={closeNewView} addView={addViewIntoFolder} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ViewsFolder.propTypes = {
|
||||
leftIndent: PropTypes.number,
|
||||
folder: PropTypes.object,
|
||||
currentPath: PropTypes.string,
|
||||
userPerm: PropTypes.string,
|
||||
canDeleteView: PropTypes.bool,
|
||||
getFoldersNames: PropTypes.func,
|
||||
getMoveableFolders: PropTypes.func,
|
||||
generateNewViewDefaultName: PropTypes.func,
|
||||
setDragMode: PropTypes.func,
|
||||
getDragMode: PropTypes.func,
|
||||
selectView: PropTypes.func,
|
||||
modifyView: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ViewsFolder;
|
@@ -1,26 +1,3 @@
|
||||
.metadata-tree-view .tree-node-inner {
|
||||
height: 28px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.metadata-tree-view .tree-node-inner .left-icon {
|
||||
top: 5px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.metadata-tree-view .tree-node-inner .tree-node-text {
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.metadata-tree-view .tree-node-icon {
|
||||
height: 100%;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.metadata-tree-view .metadata-views-icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
@@ -28,45 +5,21 @@
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-add-view {
|
||||
border-top: none;
|
||||
height: 28px;
|
||||
padding: 2px 0 2px 28px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-add-view:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-add-view .sf-metadata-add-view-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
font-weight: 400;
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-add-view .text-truncate {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-view-input {
|
||||
width: 95%;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-view-input.rename {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.metadata-tree-view .metadata-views-icon {
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-dropdown-toggle {
|
||||
display: inline-block;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
@@ -1,160 +1,173 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { Input } from 'reactstrap';
|
||||
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component';
|
||||
import toaster from '../../components/toast';
|
||||
import Icon from '../../components/icon';
|
||||
import ViewItem from './view-item';
|
||||
import { AddView } from '../components/popover/view-popover';
|
||||
import ViewsFolder from './folder';
|
||||
import ViewItem from './view';
|
||||
import NewFolder from './new-folder';
|
||||
import NewView from './new-view';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { useMetadata } from '../hooks';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { VIEW_TYPE, VIEW_TYPE_ICON } from '../constants';
|
||||
import { isValidViewName } from '../utils/validate';
|
||||
import { isEnter } from '../utils/hotkey';
|
||||
import { EVENT_BUS_TYPE, TREE_NODE_LEFT_INDENT, VIEWS_TYPE_FOLDER } from '../constants';
|
||||
import EventBus from '../../components/common/event-bus';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
const canAdd = useMemo(() => {
|
||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
||||
return true;
|
||||
}, [userPerm]);
|
||||
const [, setState] = useState(0);
|
||||
const {
|
||||
navigation,
|
||||
viewsMap,
|
||||
idViewMap,
|
||||
selectView,
|
||||
addView,
|
||||
duplicateView,
|
||||
deleteView,
|
||||
updateView,
|
||||
moveView
|
||||
} = useMetadata();
|
||||
const [newView, setNewView] = useState(null);
|
||||
const [showAddViewPopover, setShowAddViewPopover] = useState(false);
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [newView, setNewView] = useState(null);
|
||||
|
||||
const inputRef = useRef(null);
|
||||
const eventBus = EventBus.getInstance();
|
||||
|
||||
const onUpdateView = useCallback((viewId, update, successCallback, failCallback) => {
|
||||
updateView(viewId, update, () => {
|
||||
setState(n => n + 1);
|
||||
successCallback && successCallback();
|
||||
}, failCallback);
|
||||
}, [updateView]);
|
||||
const dragMode = useRef(null);
|
||||
|
||||
const togglePopover = (event) => {
|
||||
event.stopPropagation();
|
||||
setShowAddViewPopover(!showAddViewPopover);
|
||||
const setDragMode = useCallback((currDragMode) => {
|
||||
dragMode.current = currDragMode;
|
||||
}, []);
|
||||
|
||||
const getDragMode = useCallback(() => {
|
||||
return dragMode.current;
|
||||
}, []);
|
||||
|
||||
const canDeleteView = useMemo(() => {
|
||||
return Object.keys(idViewMap).length > 1;
|
||||
}, [idViewMap]);
|
||||
|
||||
const getFolders = useCallback(() => {
|
||||
return navigation.filter((nav) => nav.type === VIEWS_TYPE_FOLDER);
|
||||
}, [navigation]);
|
||||
|
||||
const getFoldersNames = useCallback(() => {
|
||||
return getFolders().map((folder) => folder.name);
|
||||
}, [getFolders]);
|
||||
|
||||
const prepareAddFolder = () => {
|
||||
setShowInput(true);
|
||||
};
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
setInputValue(event.target.value);
|
||||
};
|
||||
|
||||
const handlePopoverOptionClick = useCallback((option) => {
|
||||
setNewView(option);
|
||||
const generateNewViewDefaultName = useCallback(() => {
|
||||
let newViewName = gettext('Untitled');
|
||||
const otherViewsName = Object.values(viewsMap).map(v => v.name);
|
||||
const otherViewsName = Object.values(idViewMap).map(v => v.name);
|
||||
let i = 1;
|
||||
while (otherViewsName.includes(newViewName)) {
|
||||
newViewName = gettext('Untitled') + ' (' + (i++) + ')';
|
||||
}
|
||||
setInputValue(newViewName);
|
||||
return newViewName;
|
||||
}, [idViewMap]);
|
||||
|
||||
const prepareAddView = useCallback(({ viewType }) => {
|
||||
setNewView({ key: viewType, type: viewType, default_name: generateNewViewDefaultName() });
|
||||
setShowInput(true);
|
||||
setShowAddViewPopover(false);
|
||||
}, [viewsMap]);
|
||||
}, [generateNewViewDefaultName]);
|
||||
|
||||
const handleInputSubmit = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const viewNames = Object.values(viewsMap).map(v => v.name);
|
||||
const { isValid, message } = isValidViewName(inputValue, viewNames);
|
||||
if (!isValid) {
|
||||
toaster.danger(message);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
addView(message, newView.type);
|
||||
const closeNewView = useCallback(() => {
|
||||
setShowInput(false);
|
||||
}, [inputValue, viewsMap, addView, newView]);
|
||||
}, []);
|
||||
|
||||
const onKeyDown = useCallback((event) => {
|
||||
if (isEnter(event)) {
|
||||
handleInputSubmit(event);
|
||||
}
|
||||
}, [handleInputSubmit]);
|
||||
const closeNewFolder = useCallback(() => {
|
||||
setShowInput(false);
|
||||
}, []);
|
||||
|
||||
const modifyView = useCallback((viewId, update, successCallback, failCallback) => {
|
||||
updateView(viewId, update, () => {
|
||||
successCallback && successCallback();
|
||||
}, failCallback);
|
||||
}, [updateView]);
|
||||
|
||||
const getMoveableFolders = useCallback((currentFolderId) => {
|
||||
const folders = getFolders();
|
||||
return folders.filter((folder) => folder._id !== currentFolderId);
|
||||
}, [getFolders]);
|
||||
|
||||
const handleAddView = useCallback((name, type) => {
|
||||
addView({ name, type });
|
||||
}, [addView]);
|
||||
|
||||
const handleDuplicateView = useCallback((viewId) => {
|
||||
duplicateView({ viewId });
|
||||
}, [duplicateView]);
|
||||
|
||||
const handleDeleteView = useCallback((viewId, isSelected) => {
|
||||
deleteView({ viewId, isSelected });
|
||||
}, [deleteView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showInput && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [showInput]);
|
||||
const unsubscribeAddFolder = eventBus.subscribe(EVENT_BUS_TYPE.ADD_FOLDER, prepareAddFolder);
|
||||
const unsubscribeAddView = eventBus.subscribe(EVENT_BUS_TYPE.ADD_VIEW, prepareAddView);
|
||||
return () => {
|
||||
unsubscribeAddFolder();
|
||||
unsubscribeAddView();
|
||||
};
|
||||
}, [prepareAddView, eventBus]);
|
||||
|
||||
const renderFolder = (folder) => {
|
||||
return (
|
||||
<ViewsFolder
|
||||
key={`metadata-views-folder-${folder._id}`}
|
||||
leftIndent={TREE_NODE_LEFT_INDENT * 2}
|
||||
folder={folder}
|
||||
currentPath={currentPath}
|
||||
userPerm={userPerm}
|
||||
canDeleteView={canDeleteView}
|
||||
getFoldersNames={getFoldersNames}
|
||||
getMoveableFolders={getMoveableFolders}
|
||||
generateNewViewDefaultName={generateNewViewDefaultName}
|
||||
setDragMode={setDragMode}
|
||||
getDragMode={getDragMode}
|
||||
selectView={selectView}
|
||||
modifyView={modifyView}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderView = (view) => {
|
||||
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id;
|
||||
const isSelected = currentPath === viewPath;
|
||||
return (
|
||||
<ViewItem
|
||||
key={`metadata-views-folder-${view._id}`}
|
||||
leftIndent={TREE_NODE_LEFT_INDENT * 2}
|
||||
canDelete={canDeleteView}
|
||||
isSelected={isSelected}
|
||||
userPerm={userPerm}
|
||||
view={view}
|
||||
getMoveableFolders={getMoveableFolders}
|
||||
setDragMode={setDragMode}
|
||||
getDragMode={getDragMode}
|
||||
onClick={selectView}
|
||||
onDelete={handleDeleteView}
|
||||
onCopy={handleDuplicateView}
|
||||
onUpdate={modifyView}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tree-view tree metadata-tree-view">
|
||||
<div className="tree-node">
|
||||
<div className="children">
|
||||
{navigation.map((item, index) => {
|
||||
const view = viewsMap[item._id];
|
||||
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id;
|
||||
const isSelected = currentPath === viewPath;
|
||||
return (
|
||||
<ViewItem
|
||||
key={view._id}
|
||||
canDelete={index !== 0}
|
||||
isSelected={isSelected}
|
||||
userPerm={userPerm}
|
||||
view={view}
|
||||
onClick={(view) => selectView(view, isSelected)}
|
||||
onDelete={() => deleteView(view._id, isSelected)}
|
||||
onCopy={() => duplicateView(view._id)}
|
||||
onUpdate={(update, successCallback, failCallback) => onUpdateView(view._id, update, successCallback, failCallback)}
|
||||
onMove={moveView}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{showInput && (
|
||||
<div className="tree-view-inner sf-metadata-view-form">
|
||||
<div className="left-icon">
|
||||
<Icon symbol={VIEW_TYPE_ICON[newView.type] || VIEW_TYPE.TABLE} className="metadata-views-icon" />
|
||||
</div>
|
||||
<Input
|
||||
className="sf-metadata-view-input"
|
||||
innerRef={inputRef}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
autoFocus={true}
|
||||
onBlur={handleInputSubmit}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{canAdd && (
|
||||
<div id="sf-metadata-view-popover">
|
||||
<CustomizeAddTool
|
||||
className="sf-metadata-add-view"
|
||||
callBack={togglePopover}
|
||||
footerName={gettext('Add view')}
|
||||
addIconClassName="sf-metadata-add-view-icon"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="tree-view tree metadata-tree-view">
|
||||
<div className="tree-node">
|
||||
<div className="children">
|
||||
{navigation.map((item, index) => {
|
||||
if (item.type === VIEWS_TYPE_FOLDER) {
|
||||
return renderFolder(item, index);
|
||||
}
|
||||
const view = idViewMap[item._id];
|
||||
return renderView(view, index);
|
||||
})}
|
||||
{showInput && (newView ?
|
||||
<NewView newView={newView} leftIndent={TREE_NODE_LEFT_INDENT * 2} closeNewView={closeNewView} addView={handleAddView} /> :
|
||||
<NewFolder closeNewFolder={closeNewFolder} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showAddViewPopover && (
|
||||
<AddView
|
||||
target='sf-metadata-view-popover'
|
||||
toggle={togglePopover}
|
||||
onOptionClick={handlePopoverOptionClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -0,0 +1,58 @@
|
||||
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Input } from 'reactstrap';
|
||||
import { isEnter } from '../utils/hotkey';
|
||||
|
||||
const InlineNameEditor = forwardRef(({ name, className, onSubmit }, ref) => {
|
||||
const [inputValue, setInputValue] = useState(name || '');
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
setInputValue(event.target.value);
|
||||
};
|
||||
|
||||
const handleInputSubmit = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onSubmit(inputValue);
|
||||
}, [inputValue, onSubmit]);
|
||||
|
||||
const onKeyDown = useCallback((event) => {
|
||||
if (isEnter(event)) {
|
||||
handleInputSubmit(event);
|
||||
}
|
||||
}, [handleInputSubmit]);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
inputRef,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Input
|
||||
autoFocus
|
||||
className={classnames('sf-metadata-view-input', className)}
|
||||
innerRef={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(handleInputChange)}
|
||||
onBlur={handleInputSubmit}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
InlineNameEditor.propTypes = {
|
||||
name: PropTypes.string,
|
||||
onSubmit: PropTypes.func,
|
||||
};
|
||||
|
||||
export default InlineNameEditor;
|
@@ -1,99 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input, Button, Alert } from 'reactstrap';
|
||||
import { KeyCodes } from '../../../constants';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
const NameDialog = ({ value: oldName, title, onSubmit, onToggle }) => {
|
||||
const [name, setName] = useState(oldName || '');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
|
||||
const onChange = useCallback((event) => {
|
||||
const value = event.target.value;
|
||||
if (value === name) return;
|
||||
setName(value);
|
||||
}, [name]);
|
||||
|
||||
const validate = useCallback((name) => {
|
||||
if (typeof name !== 'string') {
|
||||
return { isValid: false, message: gettext('Name should be string') };
|
||||
}
|
||||
name = name.trim();
|
||||
if (name === '') {
|
||||
return { isValid: false, message: gettext('Name is required') };
|
||||
}
|
||||
if (name.includes('/')) {
|
||||
return { isValid: false, message: gettext('Name cannot contain slash') };
|
||||
}
|
||||
if (name.includes('\\')) {
|
||||
return { isValid: false, message: gettext('Name cannot contain backslash') };
|
||||
}
|
||||
return { isValid: true, message: name };
|
||||
}, []);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
setSubmitting(true);
|
||||
const { isValid, message } = validate(name);
|
||||
if (!isValid) {
|
||||
setErrorMessage(message);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
if (message === oldName) {
|
||||
onToggle();
|
||||
return;
|
||||
}
|
||||
onSubmit(message, () => {
|
||||
onToggle();
|
||||
}, (error) => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
setErrorMessage(errorMsg);
|
||||
setSubmitting(false);
|
||||
});
|
||||
}, [validate, name, oldName, onToggle, onSubmit]);
|
||||
|
||||
const onHotKey = useCallback((event) => {
|
||||
if (event.keyCode === KeyCodes.Enter) {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
}
|
||||
}, [submit]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', onHotKey);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onHotKey);
|
||||
};
|
||||
}, [onHotKey]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} toggle={onToggle} autoFocus={false} className="sf-metadata-view-name-dialog">
|
||||
<ModalHeader toggle={onToggle}>{title}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Name')}</Label>
|
||||
<Input autoFocus={true} value={name} onChange={onChange}/>
|
||||
<Input style={{ display: 'none' }} />
|
||||
</FormGroup>
|
||||
</Form>
|
||||
{errorMessage && <Alert color="danger" className="mt-2">{errorMessage}</Alert>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" disabled={isSubmitting} onClick={onToggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" disabled={isSubmitting} onClick={submit}>{gettext('Submit')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
NameDialog.propTypes = {
|
||||
value: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default NameDialog;
|
54
frontend/src/metadata/metadata-tree-view/new-folder.js
Normal file
54
frontend/src/metadata/metadata-tree-view/new-folder.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import toaster from '../../components/toast';
|
||||
import InlineNameEditor from './inline-name-editor';
|
||||
import { validateName } from '../utils/validate';
|
||||
import { useMetadata } from '../hooks';
|
||||
import { VIEWS_TYPE_FOLDER } from '../constants';
|
||||
|
||||
const NewFolder = ({ closeNewFolder }) => {
|
||||
const { navigation, addFolder } = useMetadata();
|
||||
|
||||
const editorRef = useRef(null);
|
||||
|
||||
const getFoldersNames = useCallback(() => {
|
||||
return navigation.filter((nav) => nav.type === VIEWS_TYPE_FOLDER)
|
||||
.map((folder) => folder.name);
|
||||
}, [navigation]);
|
||||
|
||||
const handleInputSubmit = useCallback((name) => {
|
||||
const foldersNames = getFoldersNames();
|
||||
const { isValid, message } = validateName(name, foldersNames);
|
||||
if (!isValid) {
|
||||
toaster.danger(message);
|
||||
const { inputRef } = editorRef.current || {};
|
||||
inputRef.current && inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
addFolder(message);
|
||||
closeNewFolder();
|
||||
}, [addFolder, getFoldersNames, closeNewFolder]);
|
||||
|
||||
return (
|
||||
<div className="tree-node">
|
||||
<div className="tree-node-inner tree-view-inner sf-metadata-view-form">
|
||||
<div className="tree-node-text">
|
||||
<InlineNameEditor
|
||||
ref={editorRef}
|
||||
onSubmit={handleInputSubmit}
|
||||
/>
|
||||
</div>
|
||||
<div className="left-icon">
|
||||
<i className="folder-toggle-icon sf3-font sf3-font-down"></i>
|
||||
<span className="tree-node-icon"><i className="sf3-font sf3-font-folder"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NewFolder.propTypes = {
|
||||
closeNewView: PropTypes.func,
|
||||
};
|
||||
|
||||
export default NewFolder;
|
54
frontend/src/metadata/metadata-tree-view/new-view.js
Normal file
54
frontend/src/metadata/metadata-tree-view/new-view.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import toaster from '../../components/toast';
|
||||
import Icon from '../../components/icon';
|
||||
import InlineNameEditor from './inline-name-editor';
|
||||
import { useMetadata } from '../hooks';
|
||||
import { validateName } from '../utils/validate';
|
||||
import { VIEW_TYPE, VIEW_TYPE_ICON } from '../constants';
|
||||
|
||||
const NewView = ({ newView, leftIndent, closeNewView, addView }) => {
|
||||
const { type: newViewType } = newView;
|
||||
const { idViewMap } = useMetadata();
|
||||
const editorRef = useRef(null);
|
||||
|
||||
const handleInputSubmit = useCallback((name) => {
|
||||
const viewNames = Object.values(idViewMap).map(v => v.name);
|
||||
const { isValid, message } = validateName(name, viewNames);
|
||||
if (!isValid) {
|
||||
toaster.danger(message);
|
||||
const { inputRef } = editorRef.current || {};
|
||||
inputRef.current && inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
addView(message, newViewType);
|
||||
closeNewView();
|
||||
}, [newViewType, idViewMap, addView, closeNewView]);
|
||||
|
||||
return (
|
||||
<div className="tree-node">
|
||||
<div className="tree-node-inner tree-view-inner sf-metadata-view-form">
|
||||
<div className="tree-node-text" style={{ paddingLeft: leftIndent }}>
|
||||
<InlineNameEditor
|
||||
ref={editorRef}
|
||||
name={newView.default_name}
|
||||
onSubmit={handleInputSubmit}
|
||||
/>
|
||||
</div>
|
||||
<div className="left-icon" style={{ left: leftIndent - 40 }}>
|
||||
<span className="tree-node-icon">
|
||||
<Icon symbol={VIEW_TYPE_ICON[newViewType] || VIEW_TYPE.TABLE} className="metadata-views-icon" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NewView.propTypes = {
|
||||
newView: PropTypes.object,
|
||||
leftIndent: PropTypes.number,
|
||||
closeNewView: PropTypes.func,
|
||||
};
|
||||
|
||||
export default NewView;
|
@@ -1,4 +0,0 @@
|
||||
.metadata-tree-view .sf-dropdown-toggle {
|
||||
display: inline-block;
|
||||
transform: rotate(90deg);
|
||||
}
|
@@ -1,41 +1,42 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Input } from 'reactstrap';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import Icon from '../../../components/icon';
|
||||
import ItemDropdownMenu from '../../../components/dropdown-menu/item-dropdown-menu';
|
||||
import { Utils, isMobile } from '../../../utils/utils';
|
||||
import { useMetadata } from '../../hooks';
|
||||
import { FACE_RECOGNITION_VIEW_ID, VIEW_TYPE_ICON } from '../../constants';
|
||||
import { isValidViewName } from '../../utils/validate';
|
||||
import { isEnter } from '../../utils/hotkey';
|
||||
import toaster from '../../../components/toast';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import Icon from '../../components/icon';
|
||||
import ItemDropdownMenu from '../../components/dropdown-menu/item-dropdown-menu';
|
||||
import toaster from '../../components/toast';
|
||||
import InlineNameEditor from './inline-name-editor';
|
||||
import { Utils, isMobile } from '../../utils/utils';
|
||||
import { useMetadata } from '../hooks';
|
||||
import { FACE_RECOGNITION_VIEW_ID, METADATA_VIEWS_DRAG_DATA_KEY, METADATA_VIEWS_KEY, VIEW_TYPE_ICON, VIEWS_TYPE_FOLDER, VIEWS_TYPE_VIEW } from '../constants';
|
||||
import { validateName } from '../utils/validate';
|
||||
|
||||
import './index.css';
|
||||
const MOVE_TO_FOLDER_PREFIX = 'move_to_folder_';
|
||||
|
||||
const ViewItem = ({
|
||||
leftIndent,
|
||||
canDelete,
|
||||
userPerm,
|
||||
isSelected,
|
||||
folderId,
|
||||
view,
|
||||
getMoveableFolders,
|
||||
setDragMode,
|
||||
getDragMode,
|
||||
onClick,
|
||||
onDelete,
|
||||
onCopy,
|
||||
onUpdate,
|
||||
onMove,
|
||||
}) => {
|
||||
const { _id: viewId, name: viewName } = view;
|
||||
const [highlight, setHighlight] = useState(false);
|
||||
const [freeze, setFreeze] = useState(false);
|
||||
const [isDropShow, setDropShow] = useState(false);
|
||||
const [isRenaming, setRenaming] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(view.name || '');
|
||||
|
||||
const inputRef = useRef(null);
|
||||
const { idViewMap, moveView } = useMetadata();
|
||||
|
||||
const { viewsMap } = useMetadata();
|
||||
|
||||
const otherViewsName = Object.values(viewsMap).filter(v => v._id !== view._id).map(v => v.name);
|
||||
const otherViewsName = Object.values(idViewMap).filter(v => v._id !== view._id).map(v => v.name);
|
||||
|
||||
const canUpdate = useMemo(() => {
|
||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
||||
@@ -49,18 +50,27 @@ const ViewItem = ({
|
||||
|
||||
const operations = useMemo(() => {
|
||||
if (!canUpdate) return [];
|
||||
if (view._id === FACE_RECOGNITION_VIEW_ID) {
|
||||
if (viewId === FACE_RECOGNITION_VIEW_ID) {
|
||||
return [];
|
||||
}
|
||||
let value = [
|
||||
{ key: 'rename', value: gettext('Rename') },
|
||||
{ key: 'duplicate', value: gettext('Duplicate') }
|
||||
];
|
||||
|
||||
const moveableFolders = getMoveableFolders(folderId);
|
||||
if (moveableFolders.length > 0) {
|
||||
value.push({
|
||||
key: 'move',
|
||||
value: gettext('Move'),
|
||||
subOpList: moveableFolders.map((folder) => ({ key: `${MOVE_TO_FOLDER_PREFIX}${folder._id}`, value: folder.name, icon_dom: <i className="sf3-font sf3-font-folder"></i> })),
|
||||
});
|
||||
}
|
||||
if (canDelete) {
|
||||
value.push({ key: 'delete', value: gettext('Delete') });
|
||||
}
|
||||
return value;
|
||||
}, [view, canUpdate, canDelete]);
|
||||
}, [folderId, viewId, canUpdate, canDelete, getMoveableFolders]);
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
if (freeze) return;
|
||||
@@ -79,6 +89,7 @@ const ViewItem = ({
|
||||
|
||||
const freezeItem = useCallback(() => {
|
||||
setFreeze(true);
|
||||
setHighlight(true);
|
||||
}, []);
|
||||
|
||||
const unfreezeItem = useCallback(() => {
|
||||
@@ -87,45 +98,60 @@ const ViewItem = ({
|
||||
}, []);
|
||||
|
||||
const operationClick = useCallback((operationKey) => {
|
||||
if (operationKey.startsWith(MOVE_TO_FOLDER_PREFIX)) {
|
||||
const targetFolderId = operationKey.split(MOVE_TO_FOLDER_PREFIX)[1];
|
||||
moveView({ sourceViewId: viewId, sourceFolderId: folderId, targetFolderId });
|
||||
return;
|
||||
}
|
||||
if (operationKey === 'rename') {
|
||||
setRenaming(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (operationKey === 'duplicate') {
|
||||
onCopy();
|
||||
onCopy(viewId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (operationKey === 'delete') {
|
||||
onDelete();
|
||||
onDelete(viewId, isSelected);
|
||||
return;
|
||||
}
|
||||
}, [onDelete, onCopy]);
|
||||
}, [folderId, viewId, isSelected, onDelete, onCopy, moveView]);
|
||||
|
||||
const renameView = useCallback((name, failCallback) => {
|
||||
onUpdate({ name }, () => {
|
||||
onUpdate(viewId, { name }, () => {
|
||||
setRenaming(false);
|
||||
if (!isSelected) return;
|
||||
document.title = `${name} - Seafile`;
|
||||
}, (error) => {
|
||||
failCallback(error);
|
||||
if (!isSelected) return;
|
||||
document.title = `${view.name} - Seafile`;
|
||||
document.title = `${viewName} - Seafile`;
|
||||
});
|
||||
}, [isSelected, onUpdate, view.name]);
|
||||
}, [isSelected, onUpdate, viewId, viewName]);
|
||||
|
||||
const onDragStart = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
const dragData = JSON.stringify({ type: 'sf-metadata-view', view_id: view._id });
|
||||
const dragData = JSON.stringify({
|
||||
type: METADATA_VIEWS_KEY,
|
||||
mode: VIEWS_TYPE_VIEW,
|
||||
view_id: viewId,
|
||||
folder_id: folderId,
|
||||
});
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('application/drag-sf-metadata-view', dragData);
|
||||
}, [canDrop, view]);
|
||||
event.dataTransfer.setData(METADATA_VIEWS_DRAG_DATA_KEY, dragData);
|
||||
setDragMode(VIEWS_TYPE_VIEW);
|
||||
}, [canDrop, viewId, folderId, setDragMode]);
|
||||
|
||||
const onDragEnter = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
const dragMode = getDragMode();
|
||||
if (!canDrop || folderId && dragMode === VIEWS_TYPE_FOLDER) {
|
||||
// not allowed drag folder into folder
|
||||
return false;
|
||||
}
|
||||
setDropShow(true);
|
||||
}, [canDrop]);
|
||||
}, [canDrop, folderId, getDragMode]);
|
||||
|
||||
const onDragLeave = useCallback(() => {
|
||||
if (!canDrop) return false;
|
||||
@@ -139,78 +165,43 @@ const ViewItem = ({
|
||||
}, [canDrop]);
|
||||
|
||||
const onDrop = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
const dragMode = getDragMode();
|
||||
if (!canDrop || (folderId && dragMode === VIEWS_TYPE_FOLDER)) return false;
|
||||
event.stopPropagation();
|
||||
setDropShow(false);
|
||||
|
||||
let dragData = event.dataTransfer.getData('application/drag-sf-metadata-view');
|
||||
let dragData = event.dataTransfer.getData(METADATA_VIEWS_DRAG_DATA_KEY);
|
||||
if (!dragData) return;
|
||||
dragData = JSON.parse(dragData);
|
||||
if (dragData.type !== 'sf-metadata-view') return false;
|
||||
if (!dragData.view_id) return;
|
||||
onMove && onMove(dragData.view_id, view._id);
|
||||
}, [canDrop, view, onMove]);
|
||||
const { view_id: sourceViewId, folder_id: sourceFolderId } = dragData;
|
||||
if ((dragMode === VIEWS_TYPE_VIEW && !sourceViewId) || (dragMode === VIEWS_TYPE_FOLDER && !sourceFolderId)) {
|
||||
return;
|
||||
}
|
||||
moveView({ sourceViewId, sourceFolderId, targetViewId: viewId, targetFolderId: folderId });
|
||||
}, [canDrop, folderId, viewId, getDragMode, moveView]);
|
||||
|
||||
const onChange = useCallback((e) => {
|
||||
setInputValue(e.target.value);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const { isValid, message } = isValidViewName(inputValue, otherViewsName);
|
||||
const handleSubmit = useCallback((name) => {
|
||||
const { isValid, message } = validateName(name, otherViewsName);
|
||||
if (!isValid) {
|
||||
toaster.danger(message);
|
||||
return;
|
||||
}
|
||||
if (message === view.name) {
|
||||
if (message === viewName) {
|
||||
setRenaming(false);
|
||||
return;
|
||||
}
|
||||
renameView(message);
|
||||
}, [view, inputValue, otherViewsName, renameView]);
|
||||
|
||||
const onKeyDown = useCallback((event) => {
|
||||
if (isEnter(event)) {
|
||||
handleSubmit(event);
|
||||
unfreezeItem();
|
||||
}
|
||||
}, [handleSubmit, unfreezeItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRenaming && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isRenaming]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (inputRef.current && !inputRef.current.contains(event.target)) {
|
||||
handleSubmit(event);
|
||||
}
|
||||
};
|
||||
|
||||
if (isRenaming) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isRenaming, handleSubmit]);
|
||||
}, [viewName, otherViewsName, renameView]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tree-node">
|
||||
<div
|
||||
className={classnames('tree-node-inner text-nowrap', { 'tree-node-inner-hover': highlight, 'tree-node-hight-light': isSelected, 'tree-node-drop': isDropShow })}
|
||||
title={view.name}
|
||||
title={viewName}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => onClick(view)}
|
||||
onClick={() => onClick(view, isSelected)}
|
||||
>
|
||||
<div
|
||||
className="tree-node-text"
|
||||
@@ -220,29 +211,27 @@ const ViewItem = ({
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOver={onDragMove}
|
||||
onDrop={onDrop}
|
||||
style={{ paddingLeft: leftIndent + 5 }}
|
||||
>
|
||||
{isRenaming ? (
|
||||
<Input
|
||||
innerRef={inputRef}
|
||||
className="sf-metadata-view-input mt-0"
|
||||
value={inputValue}
|
||||
onChange={onChange}
|
||||
autoFocus={true}
|
||||
onBlur={() => setRenaming(false)}
|
||||
onKeyDown={onKeyDown}
|
||||
<InlineNameEditor
|
||||
name={viewName}
|
||||
className="rename mt-0"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
) : view.name}
|
||||
) : viewName}
|
||||
</div>
|
||||
<div className="left-icon">
|
||||
<div className="left-icon" style={{ left: leftIndent - 40 }}>
|
||||
<div className="tree-node-icon">
|
||||
<Icon symbol={VIEW_TYPE_ICON[view.type] || 'table'} className="metadata-views-icon" />
|
||||
</div>
|
||||
</div>
|
||||
{operations.length > 0 && (
|
||||
<div className="right-icon" id={`metadata-view-dropdown-item-${view._id}`} >
|
||||
<div className="right-icon" id={`metadata-view-dropdown-item-${viewId}`} >
|
||||
{highlight && (
|
||||
<ItemDropdownMenu
|
||||
item={{ name: 'metadata-view' }}
|
||||
menuClassname="metadata-views-dropdown-menu"
|
||||
toggleClass="sf3-font sf3-font-more"
|
||||
freezeItem={freezeItem}
|
||||
unfreezeItem={unfreezeItem}
|
||||
@@ -254,14 +243,19 @@ const ViewItem = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ViewItem.propTypes = {
|
||||
leftIndent: PropTypes.number,
|
||||
canDelete: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
folderId: PropTypes.string,
|
||||
view: PropTypes.object,
|
||||
getMoveableFolders: PropTypes.func,
|
||||
setDragMode: PropTypes.func,
|
||||
getDragMode: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
10
frontend/src/metadata/model/metadata/folder.js
Normal file
10
frontend/src/metadata/model/metadata/folder.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { VIEWS_TYPE_FOLDER } from '../../constants';
|
||||
|
||||
export default class Folder {
|
||||
constructor(object) {
|
||||
this._id = object._id || '';
|
||||
this.name = object.name || '';
|
||||
this.type = object.type || VIEWS_TYPE_FOLDER;
|
||||
this.children = Array.isArray(object.children) ? object.children : [];
|
||||
}
|
||||
}
|
@@ -6,4 +6,4 @@ export {
|
||||
export {
|
||||
isValidPosition,
|
||||
} from './geolocation';
|
||||
export { isValidViewName } from './view';
|
||||
export { validateName } from './view';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
export const isValidViewName = (name, names) => {
|
||||
export const validateName = (name, names) => {
|
||||
if (typeof name !== 'string') {
|
||||
return { isValid: false, message: gettext('Name should be string') };
|
||||
}
|
||||
|
17
frontend/src/tag/tags-tree-view/index.css
Normal file
17
frontend/src/tag/tags-tree-view/index.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.metadata-tree-view-tag .tree-node-inner .left-icon {
|
||||
top: 5px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.metadata-tree-view-tag .tree-node-inner .tree-node-text {
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.metadata-tree-view-tag .tree-node-icon {
|
||||
height: 100%;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translateY(1px);
|
||||
}
|
@@ -6,6 +6,8 @@ import { getTagId } from '../utils';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import AllTags from './all-tags';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const TagsTreeView = ({ currentPath }) => {
|
||||
const { tagsData, selectTag } = useTags();
|
||||
|
||||
@@ -15,7 +17,7 @@ const TagsTreeView = ({ currentPath }) => {
|
||||
}, [tagsData]);
|
||||
|
||||
return (
|
||||
<div className="tree-view tree metadata-tree-view">
|
||||
<div className="tree-view tree metadata-tree-view metadata-tree-view-tag">
|
||||
<div className="tree-node">
|
||||
<div className="children">
|
||||
{tags.slice(0, 20).map(tag => {
|
||||
|
@@ -3,187 +3,197 @@ import { gettext } from './constants';
|
||||
// item --> '' : {key : '', value : gettext('')};
|
||||
const TextTranslation = {
|
||||
// app-menu
|
||||
'NEW_FOLDER': {
|
||||
NEW_FOLDER: {
|
||||
key: 'New Folder',
|
||||
value: gettext('New Folder')
|
||||
},
|
||||
'NEW_FILE': {
|
||||
NEW_FILE: {
|
||||
key: 'New File',
|
||||
value: gettext('New File')
|
||||
},
|
||||
'NEW_MARKDOWN_FILE': {
|
||||
NEW_MARKDOWN_FILE: {
|
||||
key: 'New Markdown File',
|
||||
value: gettext('New Markdown File')
|
||||
},
|
||||
'NEW_EXCEL_FILE': {
|
||||
NEW_EXCEL_FILE: {
|
||||
key: 'New Excel File',
|
||||
value: gettext('New Excel File')
|
||||
},
|
||||
'NEW_POWERPOINT_FILE': {
|
||||
NEW_POWERPOINT_FILE: {
|
||||
key: 'New PowerPoint File',
|
||||
value: gettext('New PowerPoint File')
|
||||
},
|
||||
'NEW_WORD_FILE': {
|
||||
NEW_WORD_FILE: {
|
||||
key: 'New Word File',
|
||||
value: gettext('New Word File')
|
||||
},
|
||||
'NEW_SEADOC_FILE': {
|
||||
NEW_SEADOC_FILE: {
|
||||
key: 'New SeaDoc File',
|
||||
value: gettext('New SeaDoc File')
|
||||
},
|
||||
'SHARE': {
|
||||
SHARE: {
|
||||
key: 'Share',
|
||||
value: gettext('Share')
|
||||
},
|
||||
'DOWNLOAD': {
|
||||
DOWNLOAD: {
|
||||
key: 'Download',
|
||||
value: gettext('Download')
|
||||
},
|
||||
'DELETE': {
|
||||
DELETE: {
|
||||
key: 'Delete',
|
||||
value: gettext('Delete')
|
||||
},
|
||||
'RENAME': {
|
||||
RENAME: {
|
||||
key: 'Rename',
|
||||
value: gettext('Rename')
|
||||
},
|
||||
'MOVE': {
|
||||
MOVE: {
|
||||
key: 'Move',
|
||||
value: gettext('Move')
|
||||
},
|
||||
'COPY': {
|
||||
COPY: {
|
||||
key: 'Copy',
|
||||
value: gettext('Copy')
|
||||
},
|
||||
'PERMISSION': {
|
||||
PERMISSION: {
|
||||
key: 'Permission',
|
||||
value: gettext('Permission')
|
||||
},
|
||||
'DETAILS': {
|
||||
DETAILS: {
|
||||
key: 'Details',
|
||||
value: gettext('Details')
|
||||
},
|
||||
'OPEN_VIA_CLIENT': {
|
||||
OPEN_VIA_CLIENT: {
|
||||
key: 'Open via Client',
|
||||
value: gettext('Open via Client')
|
||||
},
|
||||
'LOCK': {
|
||||
LOCK: {
|
||||
key: 'Lock',
|
||||
value: gettext('Lock')
|
||||
},
|
||||
'UNLOCK': {
|
||||
UNLOCK: {
|
||||
key: 'Unlock',
|
||||
value: gettext('Unlock')
|
||||
},
|
||||
'FREEZE_DOCUMENT': {
|
||||
FREEZE_DOCUMENT: {
|
||||
key: 'Freeze Document',
|
||||
value: gettext('Freeze Document')
|
||||
},
|
||||
'UNFREEZE_DOCUMENT': {
|
||||
UNFREEZE_DOCUMENT: {
|
||||
key: 'Unfreeze Document',
|
||||
value: gettext('Unfreeze Document')
|
||||
},
|
||||
'CONVERT_AND_EXPORT': {
|
||||
CONVERT_AND_EXPORT: {
|
||||
key: 'Convert & Export',
|
||||
value: gettext('Convert & Export')
|
||||
},
|
||||
'CONVERT_TO_MARKDOWN': {
|
||||
CONVERT_TO_MARKDOWN: {
|
||||
key: 'Convert to Markdown',
|
||||
value: gettext('Convert to Markdown')
|
||||
},
|
||||
'CONVERT_TO_SDOC': {
|
||||
CONVERT_TO_SDOC: {
|
||||
key: 'Convert to sdoc',
|
||||
value: gettext('Convert to sdoc')
|
||||
},
|
||||
'CONVERT_TO_DOCX': {
|
||||
CONVERT_TO_DOCX: {
|
||||
key: 'Convert to docx',
|
||||
value: gettext('Convert to docx')
|
||||
},
|
||||
'EXPORT_DOCX': {
|
||||
EXPORT_DOCX: {
|
||||
key: 'Export docx',
|
||||
value: gettext('Export as docx')
|
||||
},
|
||||
'HISTORY': {
|
||||
HISTORY: {
|
||||
key: 'History',
|
||||
value: gettext('History')
|
||||
},
|
||||
'ACCESS_LOG': {
|
||||
ACCESS_LOG: {
|
||||
key: 'Access Log',
|
||||
value: gettext('Access Log')
|
||||
},
|
||||
'PROPERTIES': {
|
||||
PROPERTIES: {
|
||||
key: 'Properties',
|
||||
value: gettext('Properties')
|
||||
},
|
||||
'TAGS': {
|
||||
TAGS: {
|
||||
key: 'Tags',
|
||||
value: gettext('Tags')
|
||||
},
|
||||
'TRASH': {
|
||||
TRASH: {
|
||||
key: 'Trash',
|
||||
value: gettext('Trash')
|
||||
},
|
||||
'ONLYOFFICE_CONVERT': {
|
||||
ONLYOFFICE_CONVERT: {
|
||||
key: 'Convert with ONLYOFFICE',
|
||||
value: gettext('Convert with ONLYOFFICE')
|
||||
},
|
||||
'DISPLAY_FILES': {
|
||||
DISPLAY_FILES: {
|
||||
key: 'Display files',
|
||||
value: gettext('Display files')
|
||||
},
|
||||
'EXPORT_SDOC': {
|
||||
EXPORT_SDOC: {
|
||||
key: 'Export sdoc',
|
||||
value: gettext('Export as zip')
|
||||
},
|
||||
|
||||
// repo operations
|
||||
'TRANSFER': {
|
||||
TRANSFER: {
|
||||
key: 'Transfer',
|
||||
value: gettext('Transfer')
|
||||
},
|
||||
'FOLDER_PERMISSION': {
|
||||
FOLDER_PERMISSION: {
|
||||
key: 'Folder Permission',
|
||||
value: gettext('Folder Permission')
|
||||
},
|
||||
'SHARE_ADMIN': {
|
||||
SHARE_ADMIN: {
|
||||
key: 'Share Admin',
|
||||
value: gettext('Share Admin')
|
||||
},
|
||||
'CHANGE_PASSWORD': {
|
||||
CHANGE_PASSWORD: {
|
||||
key: 'Change Password',
|
||||
value: gettext('Change Password')
|
||||
},
|
||||
'RESET_PASSWORD': {
|
||||
RESET_PASSWORD: {
|
||||
key: 'Reset Password',
|
||||
value: gettext('Reset Password')
|
||||
},
|
||||
'UNWATCH_FILE_CHANGES': {
|
||||
UNWATCH_FILE_CHANGES: {
|
||||
key: 'Unwatch File Changes',
|
||||
value: gettext('Unwatch File Changes')
|
||||
},
|
||||
'WATCH_FILE_CHANGES': {
|
||||
WATCH_FILE_CHANGES: {
|
||||
key: 'Watch File Changes',
|
||||
value: gettext('Watch File Changes')
|
||||
},
|
||||
'ADVANCED': {
|
||||
ADVANCED: {
|
||||
key: 'advanced',
|
||||
value: gettext('Advanced')
|
||||
},
|
||||
|
||||
// advanced operations
|
||||
'API_TOKEN': {
|
||||
API_TOKEN: {
|
||||
key: 'API Token',
|
||||
value: gettext('API Token')
|
||||
},
|
||||
'LABEL_CURRENT_STATE': {
|
||||
LABEL_CURRENT_STATE: {
|
||||
key: 'Label Current State',
|
||||
value: gettext('Label Current State')
|
||||
},
|
||||
|
||||
'UNSHARE': {
|
||||
UNSHARE: {
|
||||
key: 'Unshare',
|
||||
value: gettext('Unshare')
|
||||
},
|
||||
|
||||
// metadata views
|
||||
ADD_FOLDER: {
|
||||
key: 'ADD_FOLDER',
|
||||
value: gettext('Add Folder')
|
||||
},
|
||||
ADD_VIEW: {
|
||||
key: 'ADD_VIEW',
|
||||
value: gettext('Add view')
|
||||
}
|
||||
};
|
||||
|
||||
export default TextTranslation;
|
||||
|
@@ -681,6 +681,142 @@ class MetadataColumns(APIView):
|
||||
return Response({'success': True})
|
||||
|
||||
|
||||
class MetadataFolders(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def post(self, request, repo_id):
|
||||
# add metadata folder
|
||||
folder_name = request.data.get('name')
|
||||
|
||||
# check view name
|
||||
if not folder_name:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder_name is invalid')
|
||||
|
||||
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||
if not record or not record.enabled:
|
||||
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
error_msg = 'Library %s not found.' % repo_id
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
permission = check_folder_permission(request, repo_id, '/')
|
||||
if permission != 'rw':
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
# check metadata_views
|
||||
metadata_views = RepoMetadataViews.objects.filter(repo_id = repo_id).first()
|
||||
if not metadata_views:
|
||||
error_msg = 'The metadata views does not exists.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
try:
|
||||
new_folder = RepoMetadataViews.objects.add_folder(repo_id, folder_name)
|
||||
if not new_folder:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'add folder failed')
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
return Response({'folder': new_folder})
|
||||
|
||||
def put(self, request, repo_id):
|
||||
# update metadata folder: name etc.
|
||||
folder_id = request.data.get('folder_id', None)
|
||||
folder_data = request.data.get('folder_data', None)
|
||||
|
||||
# check folder_id
|
||||
if not folder_id:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder_id is invalid')
|
||||
|
||||
# check folder_data
|
||||
if not folder_data:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder_data is invalid')
|
||||
if folder_data.get('_id') or folder_data.get('type') or folder_data.get('children'):
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder_data is invalid')
|
||||
|
||||
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||
if not record or not record.enabled:
|
||||
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
error_msg = 'Library %s not found.' % repo_id
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
permission = check_folder_permission(request, repo_id, '/')
|
||||
if permission != 'rw':
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
# check metadata_views
|
||||
metadata_views = RepoMetadataViews.objects.filter(repo_id = repo_id).first()
|
||||
if not metadata_views:
|
||||
error_msg = 'The metadata views does not exists.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
# check folder exist
|
||||
if folder_id not in metadata_views.folders_ids:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder %s does not exists.' % folder_id)
|
||||
|
||||
try:
|
||||
result = RepoMetadataViews.objects.update_folder(repo_id, folder_id, folder_data)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
return Response({'success': True})
|
||||
|
||||
def delete(self, request, repo_id):
|
||||
# delete metadata folder by id
|
||||
# check folder_id
|
||||
folder_id = request.data.get('folder_id', None)
|
||||
if not folder_id:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder_id is invalid')
|
||||
|
||||
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||
if not record or not record.enabled:
|
||||
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
error_msg = 'Library %s not found.' % repo_id
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
permission = check_folder_permission(request, repo_id, '/')
|
||||
if permission != 'rw':
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
# check metadata_views
|
||||
metadata_views = RepoMetadataViews.objects.filter(repo_id = repo_id).first()
|
||||
if not metadata_views:
|
||||
error_msg = 'The metadata views does not exists.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
# check folder exist
|
||||
if folder_id not in metadata_views.folders_ids:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder %s does not exists.' % folder_id)
|
||||
|
||||
try:
|
||||
result = RepoMetadataViews.objects.delete_folder(repo_id, folder_id)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
return Response({'success': True})
|
||||
|
||||
|
||||
class MetadataViews(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
@@ -706,15 +842,29 @@ class MetadataViews(APIView):
|
||||
|
||||
return Response(metadata_views)
|
||||
|
||||
|
||||
def post(self, request, repo_id):
|
||||
# Add a metadata view
|
||||
metadata_views = RepoMetadataViews.objects.filter(repo_id=repo_id).first()
|
||||
view_name = request.data.get('name')
|
||||
folder_id = request.data.get('folder_id', None)
|
||||
view_type = request.data.get('type', 'table')
|
||||
view_data = request.data.get('data', {})
|
||||
|
||||
# check view name
|
||||
if not view_name:
|
||||
error_msg = 'view name is invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
# check folder exist
|
||||
if folder_id:
|
||||
if not metadata_views:
|
||||
error_msg = 'The metadata views does not exists.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if folder_id not in metadata_views.folders_ids:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder %s does not exists' % folder_id)
|
||||
|
||||
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||
if not record or not record.enabled:
|
||||
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||
@@ -731,7 +881,9 @@ class MetadataViews(APIView):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
try:
|
||||
new_view = RepoMetadataViews.objects.add_view(repo_id, view_name, view_type, view_data)
|
||||
new_view = RepoMetadataViews.objects.add_view(repo_id, view_name, view_type, view_data, folder_id)
|
||||
if not new_view:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'add view failed')
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
@@ -739,7 +891,6 @@ class MetadataViews(APIView):
|
||||
|
||||
return Response({'view': new_view})
|
||||
|
||||
|
||||
def put(self, request, repo_id):
|
||||
# Update a metadata view, including rename, change filters and so on
|
||||
# by a json data
|
||||
@@ -764,7 +915,7 @@ class MetadataViews(APIView):
|
||||
error_msg = 'The metadata views does not exists.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if view_id not in views.view_ids:
|
||||
if view_id not in views.views_ids:
|
||||
error_msg = 'view_id %s does not exists.' % view_id
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
@@ -791,6 +942,7 @@ class MetadataViews(APIView):
|
||||
# Update a metadata view, including rename, change filters and so on
|
||||
# by a json data
|
||||
view_id = request.data.get('view_id', None)
|
||||
folder_id = request.data.get('folder_id', None)
|
||||
if not view_id:
|
||||
error_msg = 'view_id is invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
@@ -800,17 +952,22 @@ class MetadataViews(APIView):
|
||||
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
views = RepoMetadataViews.objects.filter(
|
||||
metadata_views = RepoMetadataViews.objects.filter(
|
||||
repo_id=repo_id
|
||||
).first()
|
||||
if not views:
|
||||
if not metadata_views:
|
||||
error_msg = 'The metadata views does not exists.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if view_id not in views.view_ids:
|
||||
# check view exist
|
||||
if view_id not in metadata_views.views_ids:
|
||||
error_msg = 'view_id %s does not exists.' % view_id
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
# check folder exist
|
||||
if folder_id and folder_id not in metadata_views.folders_ids:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder %s does not exists' % folder_id)
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
error_msg = 'Library %s not found.' % repo_id
|
||||
@@ -822,7 +979,7 @@ class MetadataViews(APIView):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
try:
|
||||
result = RepoMetadataViews.objects.delete_view(repo_id, view_id)
|
||||
result = RepoMetadataViews.objects.delete_view(repo_id, view_id, folder_id)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
@@ -838,6 +995,7 @@ class MetadataViewsDuplicateView(APIView):
|
||||
|
||||
def post(self, request, repo_id):
|
||||
view_id = request.data.get('view_id')
|
||||
folder_id = request.data.get('folder_id', None)
|
||||
if not view_id:
|
||||
error_msg = 'view_id invalid'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
@@ -847,17 +1005,21 @@ class MetadataViewsDuplicateView(APIView):
|
||||
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
views = RepoMetadataViews.objects.filter(
|
||||
metadata_views = RepoMetadataViews.objects.filter(
|
||||
repo_id=repo_id
|
||||
).first()
|
||||
if not views:
|
||||
if not metadata_views:
|
||||
error_msg = 'The metadata views does not exists.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if view_id not in views.view_ids:
|
||||
if view_id not in metadata_views.views_ids:
|
||||
error_msg = 'view_id %s does not exists.' % view_id
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
# check folder exist
|
||||
if folder_id and folder_id not in metadata_views.folders_ids:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'folder %s does not exists' % folder_id)
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
error_msg = 'Library %s not found.' % repo_id
|
||||
@@ -868,13 +1030,15 @@ class MetadataViewsDuplicateView(APIView):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
try:
|
||||
result = RepoMetadataViews.objects.duplicate_view(repo_id, view_id)
|
||||
new_view = RepoMetadataViews.objects.duplicate_view(repo_id, view_id, folder_id)
|
||||
if not new_view:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'duplicate view failed')
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
return Response({'view': result})
|
||||
return Response({'view': new_view})
|
||||
|
||||
|
||||
class MetadataViewsDetailView(APIView):
|
||||
@@ -913,37 +1077,29 @@ class MetadataViewsMoveView(APIView):
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def post(self, request, repo_id):
|
||||
# put view_id in front of target_view_id
|
||||
view_id = request.data.get('view_id')
|
||||
if not view_id:
|
||||
error_msg = 'view_id is invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
# move view or folder to another position
|
||||
source_view_id = request.data.get('source_view_id')
|
||||
source_folder_id = request.data.get('source_folder_id')
|
||||
target_view_id = request.data.get('target_view_id')
|
||||
if not target_view_id:
|
||||
error_msg = 'target_view_id is invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
target_folder_id = request.data.get('target_folder_id')
|
||||
|
||||
# must drag view or folder
|
||||
if not source_view_id and not source_folder_id:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'source_view_id and source_folder_id is invalid')
|
||||
|
||||
# must move above to view/folder or move view into folder
|
||||
if not target_view_id and not target_folder_id:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'target_view_id and target_folder_id is invalid')
|
||||
|
||||
# not allowed to drag folder into folder
|
||||
if not source_view_id and source_folder_id and target_view_id and target_folder_id:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'not allowed to drag folder into folder')
|
||||
|
||||
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||
if not record or not record.enabled:
|
||||
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
views = RepoMetadataViews.objects.filter(
|
||||
repo_id=repo_id,
|
||||
).first()
|
||||
if not views:
|
||||
error_msg = 'The metadata views does not exists.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if view_id not in views.view_ids:
|
||||
error_msg = 'view_id %s does not exists.' % view_id
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
if target_view_id not in views.view_ids:
|
||||
error_msg = 'target_view_id %s does not exists.' % target_view_id
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
error_msg = 'Library %s not found.' % repo_id
|
||||
@@ -954,8 +1110,33 @@ class MetadataViewsMoveView(APIView):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
metadata_views = RepoMetadataViews.objects.filter(
|
||||
repo_id=repo_id,
|
||||
).first()
|
||||
if not metadata_views:
|
||||
error_msg = 'The metadata views does not exists.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
# check dragged view exist
|
||||
if source_view_id and source_view_id not in metadata_views.views_ids:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'source_view_id %s does not exists.' % source_view_id)
|
||||
|
||||
# check dragged view exist
|
||||
if source_folder_id and source_folder_id not in metadata_views.folders_ids:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'source_view_id %s does not exists.' % source_view_id)
|
||||
|
||||
# check target view exist
|
||||
if target_view_id and target_view_id not in metadata_views.views_ids:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'target_view_id %s does not exists.' % target_view_id)
|
||||
|
||||
# check target view exist
|
||||
if target_folder_id and target_folder_id not in metadata_views.folders_ids:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'target_folder_id %s does not exists.' % target_folder_id)
|
||||
|
||||
try:
|
||||
results = RepoMetadataViews.objects.move_view(repo_id, view_id, target_view_id)
|
||||
results = RepoMetadataViews.objects.move_view(repo_id, source_view_id, source_folder_id, target_view_id, target_folder_id)
|
||||
if not results:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'move view or folder failed')
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
|
@@ -18,16 +18,16 @@ def generate_random_string_lower_digits(length):
|
||||
return random_string
|
||||
|
||||
|
||||
def generate_view_id(length, type, view_ids=None):
|
||||
def generate_views_unique_id(length, type, folders_views_ids=None):
|
||||
if type == 'face_recognition':
|
||||
return FACE_RECOGNITION_VIEW_ID
|
||||
|
||||
if not view_ids:
|
||||
if not folders_views_ids:
|
||||
return generate_random_string_lower_digits(length)
|
||||
|
||||
while True:
|
||||
new_id = generate_random_string_lower_digits(length)
|
||||
if new_id not in view_ids:
|
||||
if new_id not in folders_views_ids:
|
||||
break
|
||||
|
||||
return new_id
|
||||
@@ -77,19 +77,37 @@ class RepoMetadata(models.Model):
|
||||
db_table = 'repo_metadata'
|
||||
|
||||
|
||||
class RepoFolder(object):
|
||||
|
||||
def __init__(self, name, children=[], folders_views_ids=None):
|
||||
self.name = name
|
||||
self.type = 'folder'
|
||||
self.children = children
|
||||
|
||||
self.init_folder(folders_views_ids)
|
||||
|
||||
def init_folder(self, folders_views_ids=None):
|
||||
self.folder_json = {
|
||||
"_id": generate_views_unique_id(4, self.type, folders_views_ids),
|
||||
"name": self.name,
|
||||
"type": self.type,
|
||||
"children": self.children
|
||||
}
|
||||
|
||||
|
||||
class RepoView(object):
|
||||
|
||||
def __init__(self, name, type='table', view_data={}, view_ids=None):
|
||||
def __init__(self, name, type='table', view_data={}, folders_views_ids=None):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.view_data = view_data
|
||||
self.view_json = {}
|
||||
|
||||
self.init_view(view_ids)
|
||||
self.init_view(folders_views_ids)
|
||||
|
||||
def init_view(self, view_ids=None):
|
||||
def init_view(self, folders_views_ids=None):
|
||||
self.view_json = {
|
||||
"_id": generate_view_id(4, self.type, view_ids),
|
||||
"_id": generate_views_unique_id(4, self.type, folders_views_ids),
|
||||
"table_id": '0001', # by default
|
||||
"name": self.name,
|
||||
"filters": [],
|
||||
@@ -104,7 +122,57 @@ class RepoView(object):
|
||||
|
||||
class RepoMetadataViewsManager(models.Manager):
|
||||
|
||||
def add_view(self, repo_id, view_name, view_type='table', view_data={}):
|
||||
def add_folder(self, repo_id, folder_name):
|
||||
metadata_views = self.filter(repo_id=repo_id).first()
|
||||
if not metadata_views:
|
||||
return None
|
||||
|
||||
view_details = json.loads(metadata_views.details)
|
||||
navigation = view_details.get('navigation', [])
|
||||
exist_folders_views_ids = metadata_views.folders_views_ids
|
||||
new_folder = RepoFolder(folder_name, [], exist_folders_views_ids)
|
||||
folder_json = new_folder.folder_json
|
||||
navigation.append(folder_json)
|
||||
metadata_views.details = json.dumps(view_details)
|
||||
metadata_views.save()
|
||||
return folder_json
|
||||
|
||||
def update_folder(self, repo_id, folder_id, folder_dict):
|
||||
metadata_views = self.filter(repo_id=repo_id).first()
|
||||
folder_dict.pop('_id', '')
|
||||
folder_dict.pop('type', '')
|
||||
folder_dict.pop('children', '')
|
||||
if 'name' in folder_dict:
|
||||
exist_obj_names = metadata_views.folders_names
|
||||
folder_dict['name'] = get_no_duplicate_obj_name(folder_dict['name'], exist_obj_names)
|
||||
view_details = json.loads(metadata_views.details)
|
||||
for folder in view_details['navigation']:
|
||||
if folder.get('type', None) == 'folder' and folder.get('_id') == folder_id:
|
||||
folder.update(folder_dict)
|
||||
break
|
||||
metadata_views.details = json.dumps(view_details)
|
||||
metadata_views.save()
|
||||
return json.loads(metadata_views.details)
|
||||
|
||||
def delete_folder(self, repo_id, folder_id):
|
||||
metadata_views = self.filter(repo_id=repo_id).first()
|
||||
view_details = json.loads(metadata_views.details)
|
||||
navigation = view_details.get('navigation', [])
|
||||
views = view_details.get('views', [])
|
||||
for folder in navigation:
|
||||
if folder.get('_id') == folder_id:
|
||||
# add views which in the folder into navigation
|
||||
if folder.get('children'):
|
||||
navigation.extend(folder.get('children'))
|
||||
|
||||
# remove folder
|
||||
navigation.remove(folder)
|
||||
break
|
||||
metadata_views.details = json.dumps(view_details)
|
||||
metadata_views.save()
|
||||
return json.loads(metadata_views.details)
|
||||
|
||||
def add_view(self, repo_id, view_name, view_type='table', view_data={}, folder_id=None):
|
||||
metadata_views = self.filter(repo_id=repo_id).first()
|
||||
if not metadata_views:
|
||||
from seafevents.repo_metadata.constants import METADATA_TABLE
|
||||
@@ -127,13 +195,22 @@ class RepoMetadataViewsManager(models.Manager):
|
||||
)
|
||||
else:
|
||||
view_details = json.loads(metadata_views.details)
|
||||
view_name = get_no_duplicate_obj_name(view_name, metadata_views.view_names)
|
||||
exist_view_ids = metadata_views.view_ids
|
||||
new_view = RepoView(view_name, view_type, view_data, exist_view_ids)
|
||||
navigation = view_details.get('navigation', [])
|
||||
view_name = get_no_duplicate_obj_name(view_name, metadata_views.views_names)
|
||||
exist_folders_views_ids = metadata_views.folders_views_ids
|
||||
new_view = RepoView(view_name, view_type, view_data, exist_folders_views_ids)
|
||||
view_json = new_view.view_json
|
||||
view_id = view_json.get('_id')
|
||||
view_details['views'].append(view_json)
|
||||
view_details['navigation'].append({'_id': view_id, 'type': 'view'})
|
||||
new_view_nav = { '_id': view_id, 'type': 'view' }
|
||||
if folder_id:
|
||||
folder = next((folder for folder in navigation if folder.get('_id') == folder_id), None)
|
||||
if not folder:
|
||||
return None
|
||||
folderChildren = folder.get('children', [])
|
||||
folderChildren.append(new_view_nav)
|
||||
else:
|
||||
navigation.append(new_view_nav)
|
||||
metadata_views.details = json.dumps(view_details)
|
||||
metadata_views.save()
|
||||
return new_view.view_json
|
||||
@@ -157,7 +234,7 @@ class RepoMetadataViewsManager(models.Manager):
|
||||
metadata_views = self.filter(repo_id=repo_id).first()
|
||||
view_dict.pop('_id', '')
|
||||
if 'name' in view_dict:
|
||||
exist_obj_names = metadata_views.view_names
|
||||
exist_obj_names = metadata_views.views_names
|
||||
view_dict['name'] = get_no_duplicate_obj_name(view_dict['name'], exist_obj_names)
|
||||
view_details = json.loads(metadata_views.details)
|
||||
for v in view_details['views']:
|
||||
@@ -168,56 +245,125 @@ class RepoMetadataViewsManager(models.Manager):
|
||||
metadata_views.save()
|
||||
return json.loads(metadata_views.details)
|
||||
|
||||
def duplicate_view(self, repo_id, view_id):
|
||||
def duplicate_view(self, repo_id, view_id, folder_id=None):
|
||||
metadata_views = self.filter(repo_id=repo_id).first()
|
||||
view_details = json.loads(metadata_views.details)
|
||||
exist_view_ids = metadata_views.view_ids
|
||||
new_view_id = generate_view_id(4, exist_view_ids)
|
||||
exist_folders_views_ids = metadata_views.folders_views_ids
|
||||
new_view_id = generate_views_unique_id(4, exist_folders_views_ids)
|
||||
duplicate_view = next((copy.deepcopy(view) for view in view_details['views'] if view.get('_id') == view_id), None)
|
||||
if not duplicate_view:
|
||||
return None
|
||||
|
||||
duplicate_view['_id'] = new_view_id
|
||||
view_name = get_no_duplicate_obj_name(duplicate_view['name'], metadata_views.view_names)
|
||||
view_name = get_no_duplicate_obj_name(duplicate_view['name'], metadata_views.views_names)
|
||||
duplicate_view['name'] = view_name
|
||||
view_details['views'].append(duplicate_view)
|
||||
view_details['navigation'].append({'_id': new_view_id, 'type': 'view'})
|
||||
navigation = view_details.get('navigation', [])
|
||||
new_view_nav = {'_id': new_view_id, 'type': 'view'}
|
||||
if folder_id:
|
||||
# add duplicate_view into folder
|
||||
folder = next((folder for folder in navigation if folder.get('_id') == folder_id), None)
|
||||
if not folder:
|
||||
return None
|
||||
folderChildren = folder.get('children', [])
|
||||
folderChildren.append(new_view_nav)
|
||||
else:
|
||||
navigation.append(new_view_nav)
|
||||
|
||||
metadata_views.details = json.dumps(view_details)
|
||||
metadata_views.save()
|
||||
|
||||
return duplicate_view
|
||||
|
||||
def delete_view(self, repo_id, view_id):
|
||||
def delete_view(self, repo_id, view_id, folder_id=None):
|
||||
metadata_views = self.filter(repo_id=repo_id).first()
|
||||
view_details = json.loads(metadata_views.details)
|
||||
for v in view_details['views']:
|
||||
if v.get('_id') == view_id:
|
||||
view_details['views'].remove(v)
|
||||
navigation = view_details.get('navigation', [])
|
||||
views = view_details.get('views', [])
|
||||
|
||||
for view in views:
|
||||
if view.get('_id') == view_id:
|
||||
views.remove(view)
|
||||
break
|
||||
for v in view_details['navigation']:
|
||||
if v.get('_id') == view_id:
|
||||
view_details['navigation'].remove(v)
|
||||
for nav_item in navigation:
|
||||
# delete view from folder
|
||||
if folder_id and nav_item.get('_id') == folder_id and nav_item.get('type') == 'folder' and nav_item.get('children'):
|
||||
for child in nav_item.get('children'):
|
||||
if child.get('_id') == view_id:
|
||||
nav_item.get('children').remove(child)
|
||||
break
|
||||
break
|
||||
|
||||
# delete view not in folders
|
||||
if nav_item.get('_id') == view_id:
|
||||
navigation.remove(nav_item)
|
||||
break
|
||||
|
||||
metadata_views.details = json.dumps(view_details)
|
||||
metadata_views.save()
|
||||
return json.loads(metadata_views.details)
|
||||
|
||||
def move_view(self, repo_id, view_id, target_view_id):
|
||||
def move_view(self, repo_id, source_view_id, source_folder_id, target_view_id, target_folder_id):
|
||||
metadata_views = self.filter(repo_id=repo_id).first()
|
||||
view_details = json.loads(metadata_views.details)
|
||||
view_index = None
|
||||
target_index = None
|
||||
for i, view in enumerate(view_details['navigation']):
|
||||
if view['_id'] == view_id:
|
||||
view_index = i
|
||||
if view['_id'] == target_view_id:
|
||||
target_index = i
|
||||
navigation = view_details.get('navigation', [])
|
||||
|
||||
if view_index is not None and target_index is not None:
|
||||
if view_index < target_index:
|
||||
view_to_move = view_details['navigation'][view_index]
|
||||
view_details['navigation'].insert(target_index, view_to_move)
|
||||
view_details['navigation'].pop(view_index)
|
||||
# find drag source
|
||||
if source_folder_id:
|
||||
if source_view_id:
|
||||
# drag view from folder
|
||||
dragged_id = source_view_id
|
||||
source_folder = next((folder for folder in navigation if folder.get('_id') == source_folder_id), None)
|
||||
if source_folder:
|
||||
updated_source_nav_list = source_folder.get('children', [])
|
||||
else:
|
||||
view_to_move = view_details['navigation'].pop(view_index)
|
||||
view_details['navigation'].insert(target_index, view_to_move)
|
||||
# drag folder
|
||||
dragged_id = source_folder_id
|
||||
updated_source_nav_list = navigation
|
||||
elif source_view_id:
|
||||
# drag view not in folders
|
||||
dragged_id = source_view_id
|
||||
updated_source_nav_list = navigation
|
||||
|
||||
# invalid drag source
|
||||
if not dragged_id or not updated_source_nav_list:
|
||||
return None
|
||||
drag_source = next((nav for nav in updated_source_nav_list if nav.get('_id') == dragged_id), None)
|
||||
if not drag_source:
|
||||
return None
|
||||
|
||||
# remove drag source from navigation
|
||||
updated_source_nav_list.remove(drag_source)
|
||||
|
||||
# find drop target
|
||||
updated_target_nav_list = navigation
|
||||
if target_folder_id and source_view_id:
|
||||
target_folder = next((folder for folder in navigation if folder.get('_id') == target_folder_id), None)
|
||||
if target_folder:
|
||||
updated_target_nav_list = target_folder.get('children', [])
|
||||
|
||||
# drag source already exist
|
||||
exist_drag_source = next((nav for nav in updated_target_nav_list if nav.get('_id') == drag_source.get('_id')), None)
|
||||
if exist_drag_source:
|
||||
return None
|
||||
|
||||
# drop drag source to the target position
|
||||
target_nav = None
|
||||
if target_view_id:
|
||||
# move folder/view above to view
|
||||
target_nav = next((nav for nav in updated_target_nav_list if nav.get('_id') == target_view_id), None)
|
||||
elif not source_view_id and target_folder_id:
|
||||
# move folder above to folder
|
||||
target_nav = next((nav for nav in updated_target_nav_list if nav.get('_id') == target_folder_id), None)
|
||||
|
||||
insert_index = -1
|
||||
if target_nav:
|
||||
insert_index = updated_target_nav_list.index(target_nav)
|
||||
|
||||
if insert_index > -1:
|
||||
updated_target_nav_list.insert(insert_index, drag_source)
|
||||
else:
|
||||
updated_target_nav_list.append(drag_source)
|
||||
|
||||
metadata_views.details = json.dumps(view_details)
|
||||
metadata_views.save()
|
||||
@@ -234,11 +380,27 @@ class RepoMetadataViews(models.Model):
|
||||
db_table = 'repo_metadata_view'
|
||||
|
||||
@property
|
||||
def view_ids(self):
|
||||
def folders_ids(self):
|
||||
metadata_views = json.loads(self.details)
|
||||
navigation = metadata_views.get('navigation', [])
|
||||
return [folder.get('_id') for folder in navigation if folder.get('type', None) == 'folder']
|
||||
|
||||
@property
|
||||
def folders_names(self):
|
||||
metadata_views = json.loads(self.details)
|
||||
navigation = metadata_views.get('navigation', [])
|
||||
return [folder.get('name') for folder in navigation if folder.get('type', None) == 'folder']
|
||||
|
||||
@property
|
||||
def views_ids(self):
|
||||
views = json.loads(self.details)['views']
|
||||
return [v.get('_id') for v in views]
|
||||
|
||||
@property
|
||||
def view_names(self):
|
||||
def views_names(self):
|
||||
views = json.loads(self.details)['views']
|
||||
return [v.get('name') for v in views]
|
||||
|
||||
@property
|
||||
def folders_views_ids(self):
|
||||
return self.folders_ids + self.views_ids
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from django.urls import re_path
|
||||
from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
||||
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
||||
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
||||
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
|
||||
MetadataFileTags, MetadataTagFiles, MetadataDetailsSettingsView, MetadataOCRManageView
|
||||
|
||||
@@ -11,6 +11,7 @@ urlpatterns = [
|
||||
re_path(r'^columns/$', MetadataColumns.as_view(), name='api-v2.1-metadata-columns'),
|
||||
|
||||
# view
|
||||
re_path(r'^folders/$', MetadataFolders.as_view(), name='api-v2.1-metadata-folders'),
|
||||
re_path(r'^views/$', MetadataViews.as_view(), name='api-v2.1-metadata-views'),
|
||||
re_path(r'^views/(?P<view_id>.+)/$', MetadataViewsDetailView.as_view(), name='api-v2.1-metadata-views-detail'),
|
||||
re_path(r'^move-views/$', MetadataViewsMoveView.as_view(), name='api-v2.1-metadata-views-move'),
|
||||
|
Reference in New Issue
Block a user