diff --git a/frontend/src/components/dialog/create-file-dialog.js b/frontend/src/components/dialog/create-file-dialog.js index fa19b2ebaf..480f3db3e2 100644 --- a/frontend/src/components/dialog/create-file-dialog.js +++ b/frontend/src/components/dialog/create-file-dialog.js @@ -1,12 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label } from 'reactstrap'; +import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap'; import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; const propTypes = { fileType: PropTypes.string, parentPath: PropTypes.string.isRequired, onAddFile: PropTypes.func.isRequired, + checkDuplicatedName: PropTypes.func.isRequired, addFileCancel: PropTypes.func.isRequired, }; @@ -17,10 +19,22 @@ class CreateFile extends React.Component { parentPath: '', childName: props.fileType, isDraft: false, + errMessage: '', }; this.newInput = React.createRef(); } + componentDidMount() { + let parentPath = this.props.parentPath; + if (parentPath[parentPath.length - 1] === '/') { // mainPanel + this.setState({parentPath: parentPath}); + } else { + this.setState({parentPath: parentPath + '/'}); // sidePanel + } + this.newInput.focus(); + this.newInput.setSelectionRange(0,0); + } + handleChange = (e) => { this.setState({ childName: e.target.value, @@ -28,14 +42,23 @@ class CreateFile extends React.Component { } handleSubmit = () => { - let path = this.state.parentPath + this.state.childName; - let isDraft = this.state.isDraft; - this.props.onAddFile(path, isDraft); + let isDuplicated = this.checkDuplicatedName(); + let newName = this.state.childName + if (isDuplicated) { + let errMessage = gettext('The name \'{name}\' is already occupied, please choose another name.'); + errMessage = errMessage.replace('{name}', Utils.HTMLescape(newName)); + this.setState({errMessage: errMessage}); + } else { + let path = this.state.parentPath + newName; + let isDraft = this.state.isDraft; + this.props.onAddFile(path, isDraft); + } } handleKeyPress = (e) => { if (e.key === 'Enter') { this.handleSubmit(); + e.preventDefault(); } } @@ -92,15 +115,9 @@ class CreateFile extends React.Component { this.props.addFileCancel(); } - componentDidMount() { - let parentPath = this.props.parentPath; - if (parentPath[parentPath.length - 1] === '/') { // mainPanel - this.setState({parentPath: parentPath}); - } else { - this.setState({parentPath: parentPath + '/'}); // sidePanel - } - this.newInput.focus(); - this.newInput.setSelectionRange(0,0); + checkDuplicatedName = () => { + let isDuplicated = this.props.checkDuplicatedName(this.state.childName); + return isDuplicated; } render() { @@ -127,6 +144,7 @@ class CreateFile extends React.Component { )} + {this.state.errMessage && {this.state.errMessage}} {gettext('Cancel')} diff --git a/frontend/src/components/dialog/create-folder-dialog.js b/frontend/src/components/dialog/create-folder-dialog.js index 85c609d761..cce7671c3e 100644 --- a/frontend/src/components/dialog/create-folder-dialog.js +++ b/frontend/src/components/dialog/create-folder-dialog.js @@ -1,12 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label } from 'reactstrap'; +import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap'; import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; const propTypes = { fileType: PropTypes.string, parentPath: PropTypes.string.isRequired, onAddFolder: PropTypes.func.isRequired, + checkDuplicatedName: PropTypes.func.isRequired, addFolderCancel: PropTypes.func.isRequired, }; @@ -15,32 +17,12 @@ class CreateForder extends React.Component { super(props); this.state = { parentPath: '', - childName: '' + childName: '', + errMessage: '' }; this.newInput = React.createRef(); } - handleChange = (e) => { - this.setState({ - childName: e.target.value, - }); - } - - handleSubmit = () => { - let path = this.state.parentPath + this.state.childName; - this.props.onAddFolder(path); - } - - handleKeyPress = (e) => { - if (e.key === 'Enter') { - this.handleSubmit(); - } - } - - toggle = () => { - this.props.addFolderCancel(); - } - componentDidMount() { let parentPath = this.props.parentPath; if (parentPath[parentPath.length - 1] === '/') { // mainPanel @@ -52,6 +34,39 @@ class CreateForder extends React.Component { this.newInput.setSelectionRange(0,0); } + handleChange = (e) => { + this.setState({childName: e.target.value}); + } + + handleSubmit = () => { + let newName = this.state.childName; + let isDuplicated = this.checkDuplicatedName(); + if (isDuplicated) { + let errMessage = gettext('The name \'{name}\' is already occupied, please choose another name.'); + errMessage = errMessage.replace('{name}', Utils.HTMLescape(newName)); + this.setState({errMessage: errMessage}); + } else { + let path = this.state.parentPath + newName; + this.props.onAddFolder(path); + } + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.handleSubmit(); + e.preventDefault(); + } + } + + toggle = () => { + this.props.addFolderCancel(); + } + + checkDuplicatedName = () => { + let isDuplicated = this.props.checkDuplicatedName(this.state.childName); + return isDuplicated; + } + render() { return ( @@ -69,6 +84,7 @@ class CreateForder extends React.Component { /> + {this.state.errMessage && {this.state.errMessage}} {gettext('Cancel')} diff --git a/frontend/src/components/dialog/rename-dialog.js b/frontend/src/components/dialog/rename-dialog.js index 9897bedf60..986633e57b 100644 --- a/frontend/src/components/dialog/rename-dialog.js +++ b/frontend/src/components/dialog/rename-dialog.js @@ -1,12 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { gettext } from '../../utils/constants'; -import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter } from 'reactstrap'; +import { Utils } from '../../utils/utils'; +import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Alert } from 'reactstrap'; const propTypes = { currentNode: PropTypes.object, onRename: PropTypes.func.isRequired, toggleCancel: PropTypes.func.isRequired, + checkDuplicatedName: PropTypes.func.isRequired, }; class Rename extends React.Component { @@ -14,34 +16,13 @@ class Rename extends React.Component { super(props); this.state = { newName: '', + errMessage: '', }; this.newInput = React.createRef(); } - - handleChange = (e) => { - this.setState({ - newName: e.target.value, - }); - } - - handleSubmit = () => { - this.props.onRename(this.state.newName); - } - - handleKeyPress = (e) => { - if (e.key === 'Enter') { - this.handleSubmit(); - } - } - - toggle = () => { - this.props.toggleCancel(); - } componentWillMount() { - this.setState({ - newName: this.props.currentNode.object.name - }); + this.setState({newName: this.props.currentNode.object.name}); } componentDidMount() { @@ -60,12 +41,66 @@ class Rename extends React.Component { componentWillReceiveProps(nextProps) { this.changeState(nextProps.currentNode); } + + handleChange = (e) => { + this.setState({newName: e.target.value}); + } - changeState(currentNode) { + handleSubmit = () => { + let { isValid, errMessage } = this.validateInput(); + if (!isValid) { + this.setState({errMessage : errMessage}); + } else { + let isDuplicated = this.checkDuplicatedName(); + if (isDuplicated) { + let errMessage = gettext('The name \'{name}\' is already occupied, please choose another name.'); + errMessage = errMessage.replace('{name}', Utils.HTMLescape(this.state.newName)); + this.setState({errMessage: errMessage}); + } else { + this.props.onRename(this.state.newName); + } + } + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.handleSubmit(); + } + } + + toggle = () => { + this.props.toggleCancel(); + } + + changeState = (currentNode) => { let name = currentNode.object.name; this.setState({newName: name}); } + validateInput = () => { + let newName = this.state.newName.trim(); + let isValid = true; + let errMessage = ''; + if (!newName) { + isValid = false; + errMessage = gettext('Name is required.'); + return { isValid, errMessage }; + } + + if (newName.indexOf('/') > -1) { + isValid = false; + errMessage = gettext('Name should not include ' + '\'/\'' + '.'); + return { isValid, errMessage }; + } + + return { isValid, errMessage }; + } + + checkDuplicatedName = () => { + let isDuplicated = this.props.checkDuplicatedName(this.state.newName); + return isDuplicated; + } + render() { let type = this.props.currentNode.object.type; return ( @@ -74,6 +109,7 @@ class Rename extends React.Component { {type === 'file' ? gettext('Enter the new file name:'): gettext('Enter the new folder name:')} {this.newInput = input;}} placeholder="newName" value={this.state.newName} onChange={this.handleChange} /> + {this.state.errMessage && {this.state.errMessage}} {gettext('Submit')} diff --git a/frontend/src/components/dir-view/dir-panel.js b/frontend/src/components/dir-view/dir-panel.js index dfba2a767e..21cfcdae56 100644 --- a/frontend/src/components/dir-view/dir-panel.js +++ b/frontend/src/components/dir-view/dir-panel.js @@ -157,6 +157,7 @@ class DirPanel extends React.Component { path={this.props.path} repoID={this.props.repoID} showShareBtn={this.props.showShareBtn} + direntList={this.props.direntList} onAddFile={this.props.onAddFile} onAddFolder={this.props.onAddFolder} onUploadFile={this.onUploadFile} diff --git a/frontend/src/components/dirent-list-view/dirent-list-view.js b/frontend/src/components/dirent-list-view/dirent-list-view.js index 703519212b..c2f52fc7f2 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-view.js +++ b/frontend/src/components/dirent-list-view/dirent-list-view.js @@ -11,6 +11,7 @@ import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; import '../../css/tip-for-new-md.css'; +import toaster from '../toast'; const propTypes = { path: PropTypes.string.isRequired, @@ -61,6 +62,19 @@ class DirentListView extends React.Component { this.setState({isItemFreezed: false}); } + onItemRename = (dirent, newName) => { + let isDuplicated = this.props.direntList.some(item => { + return item.name === newName; + }); + if (isDuplicated) { + let errMessage = gettext('The name {name} is already occupied, please choose another name.'); + errMessage = errMessage.replace('{name}', Utils.HTMLescape(newName)); + toaster.danger(errMessage); + return false; + } + this.props.onItemRename(dirent, newName); + } + onItemRenameToggle = () => { this.onFreezedItem(); } @@ -238,11 +252,12 @@ class DirentListView extends React.Component { repoID={this.props.repoID} currentRepoInfo={this.props.currentRepoInfo} isRepoOwner={this.props.isRepoOwner} + direntList={this.props.direntList} onItemClick={this.props.onItemClick} onItemRenameToggle={this.onItemRenameToggle} onItemSelected={this.props.onItemSelected} onItemDelete={this.props.onItemDelete} - onItemRename={this.props.onItemRename} + onItemRename={this.onItemRename} onItemMove={this.props.onItemMove} onItemCopy={this.props.onItemCopy} updateDirent={this.props.updateDirent} diff --git a/frontend/src/components/toolbar/dir-operation-toolbar.js b/frontend/src/components/toolbar/dir-operation-toolbar.js index 13a9c27014..05eea75043 100644 --- a/frontend/src/components/toolbar/dir-operation-toolbar.js +++ b/frontend/src/components/toolbar/dir-operation-toolbar.js @@ -20,6 +20,7 @@ const propTypes = { onUploadFolder: PropTypes.func.isRequired, isDraft: PropTypes.bool, hasDraft: PropTypes.bool, + direntList: PropTypes.array.isRequired, }; class DirOperationToolbar extends React.Component { @@ -167,6 +168,14 @@ class DirOperationToolbar extends React.Component { this.props.goDraftPage(); } + checkDuplicatedName = (newName) => { + let direntList = this.props.direntList; + let isDuplicated = direntList.some(object => { + return object.name === newName; + }); + return isDuplicated; + } + render() { let { path, isViewFile } = this.props; let itemType = isViewFile ? 'file' : 'dir'; @@ -220,8 +229,9 @@ class DirOperationToolbar extends React.Component { parentPath={this.props.path} fileType={this.state.fileType} onAddFile={this.onAddFile} + checkDuplicatedName={this.checkDuplicatedName} addFileCancel={this.onCreateFileToggle} - /> + /> )} {this.state.isCreateFolderDialogShow && ( @@ -229,6 +239,7 @@ class DirOperationToolbar extends React.Component { diff --git a/frontend/src/components/tree-view-2/tree-helper.js b/frontend/src/components/tree-view-2/tree-helper.js index 049de884f2..91841213e9 100644 --- a/frontend/src/components/tree-view-2/tree-helper.js +++ b/frontend/src/components/tree-view-2/tree-helper.js @@ -1,6 +1,7 @@ import { Utils } from '../../utils/utils'; import Tree from './tree'; import TreeNode from './tree-node'; +import Dirent from '../../models/dirent'; class TreeHelper { @@ -74,11 +75,12 @@ class TreeHelper { return treeCopy; } - moveNodeByPath(tree, nodePath, destPath) { + moveNodeByPath(tree, nodePath, destPath, nodeName) { let treeCopy = tree.clone(); let node = treeCopy.getNodeByPath(nodePath); let destNode = treeCopy.getNodeByPath(destPath); - if (destNode && node) { // node has loaded + if (destNode && node) { // node has loaded + node.object.name = nodeName; // need not update path treeCopy.moveNode(node, destNode); } if (!destNode && node){ @@ -94,7 +96,7 @@ class TreeHelper { nodePaths.forEach(nodePath => { let node = treeCopy.getNodeByPath(nodePath); treeCopy.moveNode(node, destNode); - }) + }); } else { nodePaths.forEach(nodePath=> { let node = treeCopy.getNodeByPath(nodePath); @@ -104,12 +106,13 @@ class TreeHelper { return treeCopy; } - copyNodeByPath(tree, nodePath, destPath) { + copyNodeByPath(tree, nodePath, destPath, nodeName) { let treeCopy = tree.clone(); - let node = treeCopy.getNodeByPath(nodePath); - node = node.clone(); // need a dup let destNode = treeCopy.getNodeByPath(destPath); + let treeNode = treeCopy.getNodeByPath(nodePath); if (destNode) { + let node = treeNode.clone(); // need a dup + node.object.name = nodeName; // need not update path treeCopy.copyNode(node, destNode); } return treeCopy; @@ -129,7 +132,7 @@ class TreeHelper { buildTree() { let tree = new Tree(); - let object = {name: '/'}; + let object = new Dirent({name: '/'}); let root = new TreeNode({object, isLoaded: false, isExpanded: true}); tree.setRoot(root); return tree; diff --git a/frontend/src/components/tree-view-2/tree-node.js b/frontend/src/components/tree-view-2/tree-node.js index b7aa3c2f34..0a404754d1 100644 --- a/frontend/src/components/tree-view-2/tree-node.js +++ b/frontend/src/components/tree-view-2/tree-node.js @@ -2,7 +2,7 @@ class TreeNode { constructor({ path, object, isLoaded, isPreload, isExpanded, parentNode }) { this.path = path || object.name, // The default setting is the object name, which is set to a relative path when the father is set. - this.object = object; + this.object = object.clone(); this.isLoaded = isLoaded || false; this.isPreload = isPreload || false; this.isExpanded = isExpanded || false; @@ -13,7 +13,7 @@ class TreeNode { clone() { let treeNode = new TreeNode({ path: this.path, - object: this.object, + object: this.object.clone(), isLoaded: this.isLoaded, isPreload: this.isPreload, isExpanded: this.isExpanded, @@ -102,7 +102,7 @@ class TreeNode { const treeNode = { path: this.path, - object: this.object, + object: this.object.clone(), isLoaded: this.isLoaded, isPreload: this.isPreload, isExpanded: this.isExpanded, @@ -115,6 +115,7 @@ class TreeNode { static deserializefromJson(json) { let { path, object, isLoaded, isPreload, isExpanded, parentNode, children = [] } = json; + object = object.clone(); const treeNode = new TreeNode({ path, diff --git a/frontend/src/models/dirent.js b/frontend/src/models/dirent.js index 3f8a264478..dc1ff245e0 100644 --- a/frontend/src/models/dirent.js +++ b/frontend/src/models/dirent.js @@ -34,6 +34,10 @@ class Dirent { } } + clone() { + return new Dirent(this); + } + isDir() { return this.type !== 'file'; } diff --git a/frontend/src/pages/repo-wiki-mode/main-panel.js b/frontend/src/pages/repo-wiki-mode/main-panel.js index b4055fb662..9cac07e99e 100644 --- a/frontend/src/pages/repo-wiki-mode/main-panel.js +++ b/frontend/src/pages/repo-wiki-mode/main-panel.js @@ -187,6 +187,7 @@ class MainPanel extends Component { repoID={repoID} isDraft={this.props.isDraft} hasDraft={this.props.hasDraft} + direntList={this.props.direntList} permission={this.props.permission} isViewFile={this.props.isViewFile} showShareBtn={this.props.showShareBtn} diff --git a/frontend/src/pages/repo-wiki-mode/side-panel.js b/frontend/src/pages/repo-wiki-mode/side-panel.js index 6dc1a0c311..22e1508791 100644 --- a/frontend/src/pages/repo-wiki-mode/side-panel.js +++ b/frontend/src/pages/repo-wiki-mode/side-panel.js @@ -1,10 +1,12 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { gettext } from '../../utils/constants'; import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; import TreeView from '../../components/tree-view-2/tree-view'; import Logo from '../../components/logo'; import Loading from '../../components/loading'; +import toaster from '../../components/toast'; import ModalPortal from '../../components/modal-portal'; import Delete from '../../components/dialog/delete-dialog'; import Rename from '../../components/dialog/rename-dialog'; @@ -141,6 +143,19 @@ class SidePanel extends Component { this.props.onDeleteNode(node); } + 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; + } + render() { return ( @@ -190,8 +205,9 @@ class SidePanel extends Component { + /> )} {this.state.isAddFileDialogShow && ( @@ -200,8 +216,9 @@ class SidePanel extends Component { fileType={'.md'} parentPath={this.state.opNode.path} onAddFile={this.onAddFileNode} + checkDuplicatedName={this.checkDuplicatedName} addFileCancel={this.onAddFileToggle} - /> + /> )} {this.state.isRenameDialogShow && ( @@ -209,6 +226,7 @@ class SidePanel extends Component { diff --git a/frontend/src/repo-wiki-mode.js b/frontend/src/repo-wiki-mode.js index 677880107a..590e2540cc 100644 --- a/frontend/src/repo-wiki-mode.js +++ b/frontend/src/repo-wiki-mode.js @@ -473,9 +473,9 @@ class Wiki extends Component { //just for view list state let dirName = dirent.name let direntPath = Utils.joinPath(this.state.path, dirName); - seafileAPI.moveDir(repoID, destRepo.repo_id,moveToDirentPath, this.state.path, dirName).then(() => { - - this.moveTreeNode(direntPath, moveToDirentPath, destRepo); + seafileAPI.moveDir(repoID, destRepo.repo_id,moveToDirentPath, this.state.path, dirName).then(res => { + let nodeName = res.data[0].obj_name; + this.moveTreeNode(direntPath, moveToDirentPath, destRepo, nodeName); this.moveDirent(direntPath); let message = gettext('Successfully moved %(name)s.'); @@ -492,8 +492,9 @@ class Wiki extends Component { //just for view list state let dirName = dirent.name; let direntPath = Utils.joinPath(this.state.path, dirName); - seafileAPI.copyDir(repoID, destRepo.repo_id, copyToDirentPath, this.state.path, dirName).then(() => { - this.copyTreeNode(direntPath, copyToDirentPath, destRepo); + seafileAPI.copyDir(repoID, destRepo.repo_id, copyToDirentPath, this.state.path, dirName).then(res => { + let nodeName = res.data[0].obj_name; + this.copyTreeNode(direntPath, copyToDirentPath, destRepo, nodeName); let message = gettext('Successfully copied %(name)s.'); message = message.replace('%(name)s', dirName); toaster.success(message); @@ -508,9 +509,12 @@ class Wiki extends Component { let direntPaths = this.getSelectedDirentPaths(); let dirNames = this.getSelectedDirentNames(); - seafileAPI.moveDir(repoID, destRepo.repo_id, destDirentPath, this.state.path, dirNames).then(() => { - direntPaths.forEach(direntPath => { - this.moveTreeNode(direntPath, destDirentPath, destRepo); + seafileAPI.moveDir(repoID, destRepo.repo_id, destDirentPath, this.state.path, dirNames).then(res => { + let names = res.data.map(item => { + return item.obj_name; + }); + direntPaths.forEach((direntPath, index) => { + this.moveTreeNode(direntPath, destDirentPath, destRepo, names[index]); this.moveDirent(direntPath); }); let message = gettext('Successfully moved %(name)s.'); @@ -527,9 +531,12 @@ class Wiki extends Component { let direntPaths = this.getSelectedDirentPaths(); let dirNames = this.getSelectedDirentNames(); - seafileAPI.copyDir(repoID, destRepo.repo_id, destDirentPath, this.state.path, dirNames).then(() => { - direntPaths.forEach(direntPath => { - this.copyTreeNode(direntPath, destDirentPath, destRepo); + seafileAPI.copyDir(repoID, destRepo.repo_id, destDirentPath, this.state.path, dirNames).then(res => { + let names = res.data.map(item => { + return item.obj_name; + }); + direntPaths.forEach((direntPath, index) => { + this.copyTreeNode(direntPath, destDirentPath, destRepo, names[index]); }); let message = gettext('Successfully copied %(name)s.'); message = message.replace('%(name)s', dirNames); @@ -911,21 +918,21 @@ class Wiki extends Component { this.setState({treeData: tree}); } - moveTreeNode = (nodePath, moveToPath, moveToRepo) => { + moveTreeNode = (nodePath, moveToPath, moveToRepo, nodeName) => { if (repoID !== moveToRepo.repo_id) { let tree = treeHelper.deleteNodeByPath(this.state.treeData, nodePath); this.setState({treeData: tree}); return } - let tree = treeHelper.moveNodeByPath(this.state.treeData, nodePath, moveToPath); + let tree = treeHelper.moveNodeByPath(this.state.treeData, nodePath, moveToPath, nodeName); this.setState({treeData: tree}); } - copyTreeNode = (nodePath, copyToPath, destRepo) => { + copyTreeNode = (nodePath, copyToPath, destRepo, nodeName) => { if (repoID !== destRepo.repo_id) { return; } - let tree = treeHelper.copyNodeByPath(this.state.treeData, nodePath, copyToPath); + let tree = treeHelper.copyNodeByPath(this.state.treeData, nodePath, copyToPath, nodeName); this.setState({treeData: tree}); }
{type === 'file' ? gettext('Enter the new file name:'): gettext('Enter the new folder name:')}