mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-01 23:20:51 +00:00
Support wiki page create/rename operations (#2295)
This commit is contained in:
committed by
Daniel Pan
parent
e4a255206f
commit
00388d3cda
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import Search from './search';
|
||||
import MarkdownViewer from './markdown-viewer';
|
||||
import Account from './account';
|
||||
import { repoID, serviceUrl, slug, siteRoot } from './constance';
|
||||
import { gettext, repoID, serviceUrl, slug, siteRoot } from './constance';
|
||||
|
||||
class MainPanel extends Component {
|
||||
|
||||
@@ -33,7 +33,7 @@ class MainPanel extends Component {
|
||||
<div className="main-panel-top panel-top">
|
||||
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu" onClick={this.onMenuClick}></span>
|
||||
<div className={`wiki-page-ops ${this.props.permission === 'rw' ? '' : 'hide'}`}>
|
||||
<a className="btn btn-secondary btn-topbar" onClick={this.onEditClick}>Edit Page</a>
|
||||
<a className="btn btn-secondary btn-topbar" onClick={this.onEditClick}>{gettext("Edit Page")}</a>
|
||||
</div>
|
||||
<div className="common-toolbar">
|
||||
<Search seafileAPI={this.props.seafileAPI} onSearchedClick={this.props.onSearchedClick}/>
|
||||
@@ -43,7 +43,7 @@ class MainPanel extends Component {
|
||||
<div className="cur-view-main">
|
||||
<div className="cur-view-path">
|
||||
<div className="path-containter">
|
||||
<a href={siteRoot + 'wikis/'} className="normal">Wikis</a>
|
||||
<a href={siteRoot + 'wikis/'} className="normal">{gettext("Wikis")}</a>
|
||||
<span className="path-split">/</span>
|
||||
<a href={siteRoot + 'wikis/' + slug} className="normal">{slug}</a>
|
||||
{pathElem}
|
||||
|
@@ -1,16 +1,219 @@
|
||||
import React, { Component } from 'react';
|
||||
import TreeView from './tree-view/tree-view';
|
||||
import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from './constance';
|
||||
import Tree from './tree-view/tree';
|
||||
import Node from './tree-view/node'
|
||||
import NodeMenu from './menu-component/node-menu';
|
||||
import MenuControl from './menu-component/node-menu-control';
|
||||
const gettext = window.gettext;
|
||||
|
||||
class SidePanel extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tree_data: new Tree(),
|
||||
currentNode: null,
|
||||
isNodeItemFrezee: false,
|
||||
isShowMenu: false,
|
||||
menuPosition: {
|
||||
left: 0,
|
||||
top: 0
|
||||
},
|
||||
isLoadFailed: false,
|
||||
isMenuIconShow: false
|
||||
}
|
||||
}
|
||||
|
||||
closeSide = () => {
|
||||
this.props.onCloseSide();
|
||||
}
|
||||
|
||||
onFileClick = (e, node) => {
|
||||
onMouseEnter = () => {
|
||||
this.setState({
|
||||
isMenuIconShow: true
|
||||
})
|
||||
}
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({
|
||||
isMenuIconShow: false
|
||||
})
|
||||
}
|
||||
|
||||
onNodeClick = (e, node) => {
|
||||
this.setState({
|
||||
currentNode: node
|
||||
})
|
||||
this.props.onFileClick(e, node)
|
||||
}
|
||||
|
||||
onShowContextMenu = (e, node) => {
|
||||
let left = e.clientX - 8*16;
|
||||
let top = e.clientY + 10;
|
||||
let position = Object.assign({},this.state.menuPosition, {left: left, top: top});
|
||||
this.setState({
|
||||
isShowMenu: !this.state.isShowMenu,
|
||||
currentNode: node,
|
||||
menuPosition: position,
|
||||
isNodeItemFrezee: true
|
||||
})
|
||||
}
|
||||
|
||||
onHeadingMenuClick = (e) => {
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
let node = this.state.tree_data.root;
|
||||
let left = e.clientX - 8*16;
|
||||
let top = e.clientY + 10;
|
||||
let position = Object.assign({},this.state.menuPosition, {left: left, top: top});
|
||||
this.setState({
|
||||
isShowMenu: !this.state.isShowMenu,
|
||||
currentNode: node,
|
||||
menuPosition: position
|
||||
})
|
||||
}
|
||||
|
||||
onHideContextMenu = () => {
|
||||
this.setState({
|
||||
isShowMenu: false,
|
||||
isNodeItemFrezee: false
|
||||
})
|
||||
}
|
||||
|
||||
onAddFolderNode = (dirPath) => {
|
||||
this.props.editorUtilities.createDir(dirPath).then(res => {
|
||||
this.initializeTreeData()
|
||||
})
|
||||
}
|
||||
|
||||
onAddFileNode = (filePath) => {
|
||||
this.props.editorUtilities.createFile(filePath).then(res => {
|
||||
this.initializeTreeData()
|
||||
})
|
||||
}
|
||||
|
||||
onRenameNode = (newName) => {
|
||||
var node = this.state.currentNode;
|
||||
let type = node.type;
|
||||
let filePath = node.path;
|
||||
if (type === 'file') {
|
||||
this.props.editorUtilities.renameFile(filePath, newName).then(res => {
|
||||
this.initializeTreeData()
|
||||
if (this.isModifyCurrentFile()) {
|
||||
node.name = newName;
|
||||
this.props.onFileClick(null, node);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'dir') {
|
||||
this.props.editorUtilities.renameDir(filePath, newName).then(res => {
|
||||
this.initializeTreeData();
|
||||
if (this.isModifyContainsCurrentFile()) {
|
||||
let currentNode = this.state.currentNode;
|
||||
let nodePath = encodeURI(currentNode.path);
|
||||
let pathname = window.location.pathname;
|
||||
let start = pathname.indexOf(nodePath);
|
||||
let node = currentNode.getNodeByPath(decodeURI(pathname.slice(start)));
|
||||
if(node){
|
||||
currentNode.name = newName;
|
||||
this.props.onFileClick(null, node);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onDeleteNode = () => {
|
||||
var currentNode = this.state.currentNode;
|
||||
let filePath = currentNode.path;
|
||||
let type = currentNode.type;
|
||||
if (type === 'file') {
|
||||
this.props.editorUtilities.deleteFile(filePath).then(res => {
|
||||
this.initializeTreeData();
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'dir') {
|
||||
this.props.editorUtilities.deleteDir(filePath).then(res => {
|
||||
this.initializeTreeData();
|
||||
})
|
||||
}
|
||||
|
||||
let isCurrentFile = false;
|
||||
if (this.state.currentNode.type === "dir") {
|
||||
isCurrentFile = this.isModifyContainsCurrentFile();
|
||||
} else {
|
||||
isCurrentFile = this.isModifyCurrentFile();
|
||||
}
|
||||
|
||||
if (isCurrentFile) {
|
||||
let homeNode = this.getHomeNode();
|
||||
this.props.onFileClick(null, homeNode);
|
||||
}
|
||||
}
|
||||
|
||||
isModifyCurrentFile() {
|
||||
let name = this.state.currentNode.name;
|
||||
let pathname = window.location.pathname;
|
||||
let currentName = pathname.slice(pathname.lastIndexOf("/") + 1);
|
||||
return name === currentName;
|
||||
}
|
||||
|
||||
isModifyContainsCurrentFile() {
|
||||
let pathname = window.location.pathname;
|
||||
let nodePath = this.state.currentNode.path;
|
||||
if (pathname.indexOf(nodePath)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
initializeTreeData() {
|
||||
this.props.editorUtilities.getFiles().then((files) => {
|
||||
// construct the tree object
|
||||
var rootObj = {
|
||||
name: '/',
|
||||
type: 'dir',
|
||||
isExpanded: true
|
||||
}
|
||||
var treeData = new Tree();
|
||||
treeData.parseFromList(rootObj, files);
|
||||
let homeNode = this.getHomeNode(treeData);
|
||||
this.setState({
|
||||
tree_data: treeData,
|
||||
currentNode: homeNode
|
||||
})
|
||||
}, () => {
|
||||
console.log("failed to load files");
|
||||
this.setState({
|
||||
isLoadFailed: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getHomeNode(treeData) {
|
||||
let root = null;
|
||||
if (treeData) {
|
||||
root = treeData.root;
|
||||
} else {
|
||||
root = this.state.tree_data.root;
|
||||
}
|
||||
let homeNode = root.getNodeByPath(decodeURI("/home.md"));
|
||||
return homeNode;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
//init treeview data
|
||||
this.initializeTreeData();
|
||||
|
||||
document.addEventListener('click', this.onHideContextMenu);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.onHideContextMenu);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`wiki-side-panel ${this.props.closeSideBar ? "": "left-zero"}`}>
|
||||
@@ -21,11 +224,39 @@ class SidePanel extends Component {
|
||||
<a title="Close" aria-label="Close" onClick={this.closeSide} className="sf2-icon-x1 sf-popover-close side-panel-close op-icon d-md-none "></a>
|
||||
</div>
|
||||
<div id="side-nav" className="wiki-side-nav" role="navigation">
|
||||
<h3 className="wiki-pages-heading">Pages</h3>
|
||||
<h3
|
||||
className="wiki-pages-heading"
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{gettext("Pages")}
|
||||
<div className="heading-icon">
|
||||
<MenuControl
|
||||
isShow={this.state.isMenuIconShow}
|
||||
onClick={this.onHeadingMenuClick}
|
||||
/>
|
||||
</div>
|
||||
</h3>
|
||||
<div className="wiki-pages-container">
|
||||
{this.state.tree_data &&
|
||||
<TreeView
|
||||
editorUtilities={this.props.editorUtilities}
|
||||
onClick={this.onFileClick}
|
||||
permission={this.props.permission}
|
||||
treeData={this.state.tree_data}
|
||||
currentNode={this.state.currentNode}
|
||||
isNodeItemFrezee={this.state.isNodeItemFrezee}
|
||||
onNodeClick={this.onNodeClick}
|
||||
onShowContextMenu={this.onShowContextMenu}
|
||||
/>
|
||||
}
|
||||
<NodeMenu
|
||||
isShowMenu={this.state.isShowMenu}
|
||||
menuPosition={this.state.menuPosition}
|
||||
currentNode={this.state.currentNode}
|
||||
onHideContextMenu={this.onHideContextMenu}
|
||||
onDeleteNode={this.onDeleteNode}
|
||||
onAddFileNode={this.onAddFileNode}
|
||||
onAddFolderNode={this.onAddFolderNode}
|
||||
onRenameNode={this.onRenameNode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import cookie from 'react-cookies';
|
||||
import { keyCodes, bytesToSize } from './utils';
|
||||
import { siteRoot, avatarInfo } from './constance';
|
||||
import { siteRoot, avatarInfo, gettext } from './constance';
|
||||
|
||||
|
||||
class Account extends Component {
|
||||
@@ -88,7 +88,7 @@ class Account extends Component {
|
||||
renderMenu = () => {
|
||||
if(this.state.isStaff){
|
||||
return (
|
||||
<a href={siteRoot + 'sys/useradmin/'} title="System Admin" className="item">System Admin</a>
|
||||
<a href={siteRoot + 'sys/useradmin/'} title={gettext("System Admin")} className="item">{gettext("System Admin")}</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -124,15 +124,15 @@ class Account extends Component {
|
||||
</div>
|
||||
<div id="space-traffic">
|
||||
<div className="item">
|
||||
<p>Used: {this.state.quotaUsage} / {this.state.quotaTotal}</p>
|
||||
<p>{gettext("Used")}: {this.state.quotaUsage} / {this.state.quotaTotal}</p>
|
||||
<div id="quota-bar">
|
||||
<span id="quota-usage" className="usage" style={{width: this.state.usageRate}}></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href={siteRoot + 'profile/'} className="item">Settings</a>
|
||||
<a href={siteRoot + 'profile/'} className="item">{gettext("Settings")}</a>
|
||||
{this.renderMenu()}
|
||||
<a href={siteRoot + 'accounts/logout/'} className="item">Log out</a>
|
||||
<a href={siteRoot + 'accounts/logout/'} className="item">{gettext("Log out")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export const dirPath = '/';
|
||||
export const gettext = window.gettext;
|
||||
|
||||
export const siteRoot = window.app.config.siteRoot;
|
||||
export const avatarInfo = window.app.config.avatarInfo;
|
||||
|
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Col, FormText } from 'reactstrap';
|
||||
const gettext = window.gettext;
|
||||
|
||||
class CreateFileForder extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
parentPath: '',
|
||||
childName: ''
|
||||
};
|
||||
this.newInput = React.createRef()
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
this.setState({
|
||||
childName: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
let path = this.state.parentPath + this.state.childName
|
||||
if (this.props.isFile) {
|
||||
this.props.onAddFile(path);
|
||||
} else {
|
||||
this.props.onAddFolder(path);
|
||||
}
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
if (this.props.isFile) {
|
||||
this.props.addFileCancel();
|
||||
} else {
|
||||
this.props.addFolderCancel();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.changeState(this.props.isFile);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.currentNode.path === "/") {
|
||||
this.setState({parentPath: this.props.currentNode.path});
|
||||
} else {
|
||||
this.setState({parentPath: this.props.currentNode.path + "/"});
|
||||
}
|
||||
this.newInput.focus();
|
||||
this.newInput.setSelectionRange(0,0);
|
||||
}
|
||||
|
||||
changeState(isFile) {
|
||||
if (isFile) {
|
||||
this.setState({childName: '.md'});
|
||||
} else{
|
||||
this.setState({childName: ""});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.changeState(nextProps.isFile);
|
||||
}
|
||||
|
||||
changeState(isFile) {
|
||||
if (isFile) {
|
||||
this.setState({childName: '.md'});
|
||||
} else{
|
||||
this.setState({childName: ""});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{this.props.isFile ? gettext("New File") : gettext("New Folder")}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup row>
|
||||
<Label sm={3}>Parent path: </Label>
|
||||
<Col sm={9} className="parent-path"><FormText>{this.state.parentPath}</FormText></Col>
|
||||
</FormGroup>
|
||||
<FormGroup row>
|
||||
<Label for="fileName" sm={3}>{gettext("Name")}: </Label>
|
||||
<Col sm={9}>
|
||||
<Input innerRef={input => {this.newInput = input}} id="fileName" placeholder={gettext("newName")} value={this.state.childName} onChange={this.handleChange}/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" onClick={this.handleSubmit}>{gettext("Submit")}</Button>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext("Cancel")}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CreateFileForder;
|
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
|
||||
const gettext = window.gettext;
|
||||
|
||||
class Delete extends React.Component {
|
||||
|
||||
toggle = () => {
|
||||
this.props.toggleCancel();
|
||||
}
|
||||
|
||||
render() {
|
||||
let name = this.props.currentNode.name;
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext("Delete")}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext("Are you sure to delete")}{' '}<b>{name}</b> ?</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button outline color="danger" onClick={this.props.handleSubmit}>{gettext("YES")}</Button>
|
||||
<Button outline color="secondary" onClick={this.toggle}>{gettext("NO")}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Delete;
|
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter } from 'reactstrap';
|
||||
const gettext = window.gettext;
|
||||
|
||||
class Rename extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
newName: '',
|
||||
};
|
||||
this.newInput = React.createRef();
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
this.setState({
|
||||
newName: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.onRename(this.state.newName);
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
this.props.toggleCancel();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
newName: this.props.currentNode.name
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.changeState(this.props.currentNode);
|
||||
this.newInput.focus();
|
||||
let type = this.props.currentNode.type;
|
||||
if (type === "file") {
|
||||
var endIndex = this.props.currentNode.name.lastIndexOf(".md");
|
||||
this.newInput.setSelectionRange(0, endIndex, "forward");
|
||||
} else {
|
||||
this.newInput.setSelectionRange(0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.changeState(nextProps.currentNode);
|
||||
}
|
||||
|
||||
changeState(currentNode) {
|
||||
this.setState({newName: currentNode.name});
|
||||
}
|
||||
|
||||
render() {
|
||||
let type = this.props.currentNode.type;
|
||||
let preName = this.props.currentNode.name;
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{type === 'file' ? gettext("Rename File") : gettext("Rename Folder") }</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{type === 'file' ? gettext("Enter the new file name:"): gettext("Enter the new folder name:")}</p>
|
||||
<Input innerRef={input => {this.newInput = input}} placeholder="newName" value={this.state.newName} onChange={this.handleChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" onClick={this.handleSubmit}>{gettext("Submit")}</Button>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext("Cancel")}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Rename;
|
21
frontend/src/components/menu-component/node-menu-control.js
Normal file
21
frontend/src/components/menu-component/node-menu-control.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
class NodeMenuControl extends React.Component {
|
||||
|
||||
onClick = (e) => {
|
||||
let node = this.props.currentNode;
|
||||
this.props.onClick(e, node);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<i
|
||||
className={`fas fa-ellipsis-v ${this.props.isShow ? "" : "hide"}`}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
</i>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NodeMenuControl;
|
166
frontend/src/components/menu-component/node-menu.js
Normal file
166
frontend/src/components/menu-component/node-menu.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import React from 'react'
|
||||
import Delete from './menu-dialog/delete-dialog';
|
||||
import CreateFlieFolder from './menu-dialog/create-fileforder-dialog';
|
||||
import Rename from './menu-dialog/rename-dialog';
|
||||
|
||||
const gettext = window.gettext;
|
||||
|
||||
class NodeMenu extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDelete: false,
|
||||
showAddFileFolder: false,
|
||||
showRename: false,
|
||||
isFile: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDelete = () => {
|
||||
this.setState({showDelete: !this.state.showDelete});
|
||||
this.props.onHideContextMenu();
|
||||
}
|
||||
|
||||
toggleAddFileFolder = (ev, flag) => {
|
||||
if(flag){
|
||||
this.setState({
|
||||
showAddFileFolder: !this.state.showAddFileFolder,
|
||||
isFile: true
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
showAddFileFolder: !this.state.showAddFileFolder,
|
||||
isFile: false
|
||||
})
|
||||
}
|
||||
this.props.onHideContextMenu();
|
||||
}
|
||||
|
||||
toggleRename = () => {
|
||||
this.setState({showRename: !this.state.showRename});
|
||||
this.props.onHideContextMenu();
|
||||
}
|
||||
|
||||
onDelete = () => {
|
||||
this.setState({showDelete: !this.state.showDelete});
|
||||
this.props.onDeleteNode();
|
||||
}
|
||||
|
||||
deleteCancel = () => {
|
||||
this.setState({showDelete: !this.state.showDelete});
|
||||
}
|
||||
|
||||
onAddFile = (filePath) => {
|
||||
this.setState({
|
||||
showAddFileFolder: !this.state.showAddFileFolder,
|
||||
isFile: false
|
||||
});
|
||||
this.props.onAddFileNode(filePath);
|
||||
}
|
||||
|
||||
addFileCancel = () => {
|
||||
this.setState({
|
||||
showAddFileFolder: !this.state.showAddFileFolder,
|
||||
isFile: false
|
||||
});
|
||||
}
|
||||
|
||||
onAddFolder = (dirPath) => {
|
||||
this.setState({
|
||||
showAddFileFolder: !this.state.showAddFileFolder,
|
||||
isFile: false
|
||||
});
|
||||
this.props.onAddFolderNode(dirPath);
|
||||
}
|
||||
|
||||
addFolderCancel = () => {
|
||||
this.setState({
|
||||
showAddFileFolder: !this.state.showAddFileFolder,
|
||||
isFile: false
|
||||
});
|
||||
}
|
||||
|
||||
onRename = (newName) => {
|
||||
this.setState({showRename: !this.state.showRename});
|
||||
this.props.onRenameNode(newName);
|
||||
}
|
||||
|
||||
renameCancel = () => {
|
||||
this.setState({showRename: !this.state.showRename});
|
||||
}
|
||||
|
||||
renderNodeMenu() {
|
||||
let style = null;
|
||||
let position = this.props.menuPosition;
|
||||
if (this.props.isShowMenu) {
|
||||
style = {position: "fixed",left: position.left, top: position.top, display: 'block'};
|
||||
}
|
||||
|
||||
if (this.props.currentNode.type === "dir") {
|
||||
|
||||
if (this.props.currentNode.name === "/") {
|
||||
return (
|
||||
<ul className="dropdown-menu" style={style}>
|
||||
<li className="dropdown-item" onClick={this.toggleAddFileFolder}>{gettext("New Folder")}</li>
|
||||
<li className="dropdown-item" onClick={(ev,flag) => this.toggleAddFileFolder(ev,true)}>{gettext("New File")}</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="dropdown-menu" style={style}>
|
||||
<li className="dropdown-item" onClick={this.toggleAddFileFolder}>{gettext("New Folder")}</li>
|
||||
<li className="dropdown-item" onClick={(ev,flag) => this.toggleAddFileFolder(ev,true)}>{gettext("New File")}</li>
|
||||
<li className="dropdown-item" onClick={this.toggleRename}>{gettext("Rename")}</li>
|
||||
<li className="dropdown-item" onClick={this.toggleDelete}>{gettext("Delete")}</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="dropdown-menu" style={style}>
|
||||
<li className="dropdown-item" onClick={this.toggleRename}>{gettext("Rename")}</li>
|
||||
<li className="dropdown-item" onClick={this.toggleDelete}>{gettext("Delete")}</li>
|
||||
</ul>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.currentNode) {
|
||||
return (<div className="node-menu-module"></div>)
|
||||
}
|
||||
return (
|
||||
<div className="node-menu-module">
|
||||
{this.renderNodeMenu()}
|
||||
{this.state.showDelete &&
|
||||
<Delete
|
||||
currentNode={this.props.currentNode}
|
||||
handleSubmit={this.onDelete}
|
||||
toggleCancel={this.deleteCancel}
|
||||
/>
|
||||
}
|
||||
{this.state.showAddFileFolder &&
|
||||
<CreateFlieFolder
|
||||
isFile={this.state.isFile}
|
||||
currentNode={this.props.currentNode}
|
||||
onAddFolder={this.onAddFolder}
|
||||
addFolderCancel={this.addFolderCancel}
|
||||
onAddFile={this.onAddFile}
|
||||
addFileCancel={this.addFileCancel}
|
||||
/>
|
||||
}
|
||||
{this.state.showRename &&
|
||||
<Rename
|
||||
currentNode={this.props.currentNode}
|
||||
onRename={this.onRename}
|
||||
toggleCancel={this.renameCancel}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NodeMenu;
|
@@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { repoID } from './constance';
|
||||
import { gettext, repoID } from './constance';
|
||||
import SearchResultItem from './SearchResultItem';
|
||||
|
||||
class Search extends Component {
|
||||
@@ -195,7 +195,7 @@ class Search extends Component {
|
||||
type="text"
|
||||
className="search-input"
|
||||
name="query"
|
||||
placeholder="Search files in this wiki"
|
||||
placeholder={gettext("Search files in this wiki")}
|
||||
style={style}
|
||||
value={this.state.value}
|
||||
onFocus={this.onFocusHandler}
|
||||
|
@@ -100,6 +100,33 @@ class Node {
|
||||
return this.type == "dir";
|
||||
}
|
||||
|
||||
getleafPaths() {
|
||||
let paths = new Map();
|
||||
function getleafPath(node){
|
||||
if (node.hasChildren()) {
|
||||
let children = node.children;
|
||||
children.forEach(child => {
|
||||
if (child.hasChildren()) {
|
||||
getleafPath(child);
|
||||
} else {
|
||||
let path = child.path;
|
||||
paths.set(path,child);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
getleafPath(this);
|
||||
return paths;
|
||||
}
|
||||
|
||||
getNodeByPath(path) {
|
||||
let paths = this.getleafPaths();
|
||||
if (paths.has(path)) {
|
||||
return paths.get(path);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a JSON representation of the node.
|
||||
*
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import MenuControl from '../menu-component/node-menu-control'
|
||||
|
||||
function sortByType(a, b) {
|
||||
if (a.type == "dir" && b.type != "dir") {
|
||||
@@ -12,6 +13,69 @@ function sortByType(a, b) {
|
||||
|
||||
class TreeNodeView extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isMenuIconShow: false
|
||||
}
|
||||
}
|
||||
|
||||
onClick = (e) => {
|
||||
// e.nativeEvent.stopImmediatePropagation();
|
||||
let { node } = this.props;
|
||||
this.props.treeView.onNodeClick(e, node);
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
if (!this.props.isNodeItemFrezee) {
|
||||
this.setState({
|
||||
isMenuIconShow: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMouseLeave = () => {
|
||||
if (!this.props.isNodeItemFrezee) {
|
||||
this.setState({
|
||||
isMenuIconShow: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleCollapse = (e) => {
|
||||
e.stopPropagation();
|
||||
const { node } = this.props;
|
||||
if (this.props.treeView.toggleCollapse) {
|
||||
this.props.treeView.toggleCollapse(node);
|
||||
}
|
||||
}
|
||||
|
||||
onDragStart = (e) => {
|
||||
const { node } = this.props;
|
||||
this.props.treeView.onDragStart(e, node);
|
||||
}
|
||||
|
||||
onMenuControlClick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
const { node } = this.props;
|
||||
this.props.treeView.onShowContextMenu(e, node);
|
||||
}
|
||||
|
||||
hideMenuIcon = () => {
|
||||
this.setState({
|
||||
isMenuIconShow: false
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.hideMenuIcon);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.hideMenuIcon);
|
||||
}
|
||||
|
||||
renderCollapse = () => {
|
||||
const { node } = this.props;
|
||||
|
||||
@@ -50,6 +114,8 @@ class TreeNodeView extends React.Component {
|
||||
key={child.path}
|
||||
paddingLeft={this.props.paddingLeft}
|
||||
treeView={this.props.treeView}
|
||||
isNodeItemFrezee={this.props.isNodeItemFrezee}
|
||||
permission={this.props.permission}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -60,16 +126,30 @@ class TreeNodeView extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node } = this.props;
|
||||
const styles = {};
|
||||
var icon, type;
|
||||
if (node.type === "dir") {
|
||||
renderMenuController() {
|
||||
if (this.props.permission === "rw") {
|
||||
return (
|
||||
<div className="right-icon">
|
||||
<MenuControl
|
||||
isShow={this.state.isMenuIconShow}
|
||||
onClick={this.onMenuControlClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
getNodeTypeAndIcon() {
|
||||
const node = this.props.node;
|
||||
let icon = '';
|
||||
let type = '';
|
||||
if (node.type === 'dir') {
|
||||
icon = <i className="far fa-folder"/>;
|
||||
type = 'dir';
|
||||
} else {
|
||||
} else {
|
||||
let index = node.name.lastIndexOf(".");
|
||||
if (index === -1) {
|
||||
if (index === -1) {
|
||||
icon = <i className="far fa-file"/>;
|
||||
type = 'file';
|
||||
} else {
|
||||
@@ -84,52 +164,37 @@ class TreeNodeView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
return { type, icon };
|
||||
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const styles = {};
|
||||
let node = this.props.node;
|
||||
let { type, icon } = this.getNodeTypeAndIcon();
|
||||
|
||||
return (
|
||||
<div type={type}
|
||||
className="tree-node"
|
||||
style={styles}
|
||||
>
|
||||
<div onMouseLeave={this.onMouseLeave} onMouseEnter={this.onMouseEnter}
|
||||
<div type={type} className="tree-node" style={styles}>
|
||||
<div
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onClick={this.onClick}
|
||||
type={type} className={`tree-node-inner text-nowrap ${node.name === '/'? 'hide': ''}`}>
|
||||
{this.renderCollapse()}
|
||||
<span type={type} className="tree-node-icon">
|
||||
{icon}
|
||||
</span>
|
||||
<span type={type} draggable="true" onDragStart={this.onDragStart}>{node.name}</span>
|
||||
type={type}
|
||||
className={`tree-node-inner text-nowrap ${node.name === '/'? 'hide': ''}`}
|
||||
>
|
||||
<div className="tree-node-text" type={type} draggable="true" onDragStart={this.onDragStart}>{node.name}</div>
|
||||
<div className="left-icon">
|
||||
{this.renderCollapse()}
|
||||
<i type={type} className="tree-node-icon">{icon}</i>
|
||||
</div>
|
||||
{this.renderMenuController()}
|
||||
</div>
|
||||
{node.isExpanded ? this.renderChildren() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onClick = e => {
|
||||
let { node } = this.props;
|
||||
this.props.treeView.onClick(e, node);
|
||||
}
|
||||
|
||||
onMouseEnter = e => {
|
||||
let { node } = this.props;
|
||||
this.props.treeView.showImagePreview(e, node);
|
||||
}
|
||||
|
||||
onMouseLeave = e => {
|
||||
this.props.treeView.hideImagePreview(e);
|
||||
}
|
||||
|
||||
handleCollapse = e => {
|
||||
e.stopPropagation();
|
||||
const { node } = this.props;
|
||||
if (this.props.treeView.toggleCollapse) {
|
||||
this.props.treeView.toggleCollapse(node);
|
||||
}
|
||||
}
|
||||
|
||||
onDragStart = e => {
|
||||
const { node } = this.props;
|
||||
this.props.treeView.onDragStart(e, node);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default TreeNodeView;
|
||||
|
@@ -1,110 +1,8 @@
|
||||
import React from 'react';
|
||||
import TreeNodeView from './tree-node-view';
|
||||
import Tree from './tree';
|
||||
|
||||
class TreeView extends React.PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
paddingLeft: 20
|
||||
};
|
||||
|
||||
imagePreviewTimeout = null
|
||||
|
||||
state = {
|
||||
tree: new Tree(),
|
||||
loadingFailed: false,
|
||||
imagePreviewPosition: {
|
||||
left: 10+'px',
|
||||
top: 10+'px'
|
||||
},
|
||||
isShowImagePreview: false,
|
||||
imagePreviewLoading: false,
|
||||
imageSrc: '',
|
||||
}
|
||||
|
||||
showImagePreview = (e, node) => {
|
||||
e.persist();
|
||||
|
||||
let type = e.target.getAttribute('type');
|
||||
if (type === 'image') {
|
||||
this.imagePreviewTimeout = setTimeout(() => {
|
||||
let X = e.clientX + 20;
|
||||
let Y = e.clientY - 55;
|
||||
if (e.view.innerHeight < e.clientY + 150) {
|
||||
Y = e.clientY - 219;
|
||||
}
|
||||
this.setState({
|
||||
isShowImagePreview: true,
|
||||
imagePreviewLoading: true,
|
||||
imageSrc: this.props.editorUtilities.getFileURL(node),
|
||||
imagePreviewPosition: {
|
||||
left: X + 'px',
|
||||
top: Y + 'px'
|
||||
}
|
||||
});
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
hideImagePreview = (e) => {
|
||||
clearTimeout(this.imagePreviewTimeout);
|
||||
this.setState({
|
||||
isShowImagePreview: false,
|
||||
imagePreviewLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
imageLoaded = () => {
|
||||
this.setState({
|
||||
imagePreviewLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.editorUtilities.getFiles().then((files) => {
|
||||
// construct the tree object
|
||||
var rootObj = {
|
||||
name: '/',
|
||||
type: 'dir',
|
||||
isExpanded: true
|
||||
}
|
||||
var treeData = new Tree();
|
||||
treeData.parseFromList(rootObj, files);
|
||||
this.setState({
|
||||
tree: treeData
|
||||
})
|
||||
}, () => {
|
||||
console.log("failed to load files");
|
||||
this.setState({
|
||||
loadingFailed: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const tree = this.state.tree;
|
||||
if (!tree.root) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tree-view tree">
|
||||
<TreeNodeView
|
||||
node={tree.root}
|
||||
paddingLeft={20}
|
||||
treeView={this}
|
||||
/>
|
||||
{ this.state.isShowImagePreview &&
|
||||
<div style={this.state.imagePreviewPosition} className={'image-view'}>
|
||||
{ this.state.imagePreviewLoading && <i className={'rotate fa fa-spinner'}/> }
|
||||
<img src={this.state.imageSrc} onLoad={this.imageLoaded} alt=""/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
change = (tree) => {
|
||||
/*
|
||||
this._updated = true;
|
||||
@@ -113,7 +11,7 @@ class TreeView extends React.PureComponent {
|
||||
}
|
||||
|
||||
toggleCollapse = (node) => {
|
||||
const tree = this.state.tree;
|
||||
const tree = this.props.treeData;
|
||||
node.isExpanded = !node.isExpanded;
|
||||
|
||||
// copy the tree to make PureComponent work
|
||||
@@ -130,12 +28,35 @@ class TreeView extends React.PureComponent {
|
||||
e.dataTransfer.setData("text/plain", url);
|
||||
}
|
||||
|
||||
onClick = (e, node) => {
|
||||
onNodeClick = (e, node) => {
|
||||
if (node.isDir()) {
|
||||
this.toggleCollapse(node);
|
||||
return;
|
||||
}
|
||||
this.props.onClick(e, node);
|
||||
this.props.onNodeClick(e, node);
|
||||
}
|
||||
|
||||
onShowContextMenu = (e, node) => {
|
||||
this.props.onShowContextMenu(e, node);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.treeData.root) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tree-view tree">
|
||||
<TreeNodeView
|
||||
paddingLeft={20}
|
||||
treeView={this}
|
||||
node={this.props.treeData.root}
|
||||
isNodeItemFrezee={this.props.isNodeItemFrezee}
|
||||
permission={this.props.permission}
|
||||
onShowContextMenu={this.props.onShowContextMenu}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,38 +1,8 @@
|
||||
/*tree view */
|
||||
.tree-node:not([type = 'dir']):hover {
|
||||
background-color: rgb(255,239,178);
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
min-width: -moz-max-content;
|
||||
min-width: -webkit-max-content;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.tree-node-inner {
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/*
|
||||
the main reason to icon can not be align is that .folder has a real width it take the place
|
||||
of .tree-node-inner causing tree-node-icon not aligned , use absolute can make sure .tree-node-icon
|
||||
is always at the far left of .tree-node-inner
|
||||
*/
|
||||
.folder-toggle-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tree-node-icon {
|
||||
margin-right: 0.4rem;
|
||||
margin-left: 0.1rem;
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
user-select: none;
|
||||
height:100%;
|
||||
@@ -119,7 +89,89 @@
|
||||
.outline-h3:hover {
|
||||
color: #eb8205;
|
||||
}
|
||||
|
||||
/*tree view */
|
||||
.tree-node:not([type = 'dir']):hover {
|
||||
background-color: rgb(255,239,178);
|
||||
}
|
||||
|
||||
.tree-view {
|
||||
padding-left: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tree-node-inner {
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.tree-node-inner .tree-node-text {
|
||||
padding-left: 1.2rem;
|
||||
width: calc(100% - 1.5rem);
|
||||
font-size: 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tree-node-inner .left-icon {
|
||||
position: absolute;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding-left: 0.7rem;
|
||||
}
|
||||
|
||||
.folder-toggle-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tree-node-icon {
|
||||
margin-right: 0.4rem;
|
||||
margin-left: 0.1rem;
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tree-node-inner .right-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 1.5rem;
|
||||
color: #888;
|
||||
z-index: 2;
|
||||
font-size: 0.8125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tree-node-inner .right-icon i {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.625 !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
.parent-path {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.parent-path .form-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-top: 0;
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,7 @@
|
||||
}
|
||||
|
||||
.wiki-pages-heading {
|
||||
position: relative;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
text-align:center;
|
||||
@@ -16,6 +17,13 @@
|
||||
line-height: 1.5;
|
||||
height:40px;
|
||||
}
|
||||
.heading-icon {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 25%;
|
||||
color: #888;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wiki-pages-container {
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
|
@@ -33,6 +33,31 @@ class EditorUtilities {
|
||||
return files;
|
||||
})
|
||||
}
|
||||
|
||||
createFile(filePath) {
|
||||
return seafileAPI.createFile(repoID, filePath)
|
||||
}
|
||||
|
||||
deleteFile(filePath) {
|
||||
return seafileAPI.deleteFile(repoID, filePath)
|
||||
}
|
||||
|
||||
renameFile(filePath, newFileName) {
|
||||
return seafileAPI.renameFile(repoID, filePath, newFileName)
|
||||
}
|
||||
|
||||
createDir(dirPath) {
|
||||
return seafileAPI.createDir(repoID, dirPath)
|
||||
}
|
||||
|
||||
deleteDir(dirPath) {
|
||||
return seafileAPI.deleteDir(repoID, dirPath)
|
||||
}
|
||||
|
||||
renameDir(dirPath, newDirName) {
|
||||
return seafileAPI.renameDir(repoID, dirPath, newDirName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const editorUtilities = new EditorUtilities();
|
||||
@@ -115,7 +140,7 @@ class Wiki extends Component {
|
||||
})
|
||||
})
|
||||
|
||||
let fileUrl = siteRoot + 'wikis/' + slug + filePath;
|
||||
let fileUrl = serviceUrl + '/wikis/' + slug + filePath;
|
||||
window.history.pushState({urlPath: fileUrl, filePath: filePath}, filePath, fileUrl);
|
||||
}
|
||||
|
||||
@@ -145,6 +170,7 @@ class Wiki extends Component {
|
||||
closeSideBar={this.state.closeSideBar}
|
||||
onCloseSide ={this.onCloseSide}
|
||||
editorUtilities={editorUtilities}
|
||||
permission={this.state.permission}
|
||||
/>
|
||||
<MainPanel
|
||||
content={this.state.content}
|
||||
|
Reference in New Issue
Block a user