1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-02 07:27:04 +00:00

[Wiki] Support show folder (#2334)

This commit is contained in:
shanshuirenjia
2018-09-04 17:16:50 +08:00
committed by Daniel Pan
parent b8662376a1
commit 681a4235a4
22 changed files with 1019 additions and 785 deletions

1
.gitignore vendored
View File

@@ -49,5 +49,6 @@ tags
frontend/config/webpack.config.dev.js frontend/config/webpack.config.dev.js
frontend/webpack-stats.dev.json frontend/webpack-stats.dev.json
frontend/node_modules frontend/node_modules
frontend/package-lock.json
/.idea /.idea

View File

@@ -41,7 +41,7 @@
"remark-parse": "^5.0.0", "remark-parse": "^5.0.0",
"remark-rehype": "^3.0.0", "remark-rehype": "^3.0.0",
"remark-slug": "^5.0.0", "remark-slug": "^5.0.0",
"seafile-js": "^0.2.12", "seafile-js": "^0.2.13",
"seafile-ui": "^0.1.10", "seafile-ui": "^0.1.10",
"sw-precache-webpack-plugin": "0.11.4", "sw-precache-webpack-plugin": "0.11.4",
"unified": "^6.1.6", "unified": "^6.1.6",

View File

@@ -1,327 +0,0 @@
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
}
this.searchedPath = null;
}
closeSide = () => {
this.props.onCloseSide();
}
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 => {
let tree = this.state.tree_data.copy();
let index = dirPath.lastIndexOf("/");
let name = dirPath.substring(index+1);
let parentPath = dirPath.substring(0, index);
if (!parentPath) {
parentPath = "/";
}
let node = new Node({name : name, type: "dir", isExpanded: false, children: []});
let parentNode = tree.getNodeByPath(parentPath);
tree.addChildToNode(parentNode, node);
tree.setOneNodeToActived({node});
this.setState({
tree_data: tree
})
})
}
onAddFileNode = (filePath) => {
this.props.editorUtilities.createFile(filePath).then(res => {
let tree = this.state.tree_data.copy();
let index = filePath.lastIndexOf("/");
let name = filePath.substring(index+1);
let parentPath = filePath.substring(0, index);
if (!parentPath) {
parentPath = "/";
}
let node = new Node({name : name, type: "file", isExpanded: false, children: []});
let parentNode = tree.getNodeByPath(parentPath);
tree.addChildToNode(parentNode, node);
tree.setOneNodeToActived({node});
this.setState({
tree_data: tree
})
})
}
onRenameNode = (newName) => {
let tree = this.state.tree_data.copy();
let node = this.state.currentNode;
let type = node.type;
let filePath = node.path;
if (type === 'file') {
this.props.editorUtilities.renameFile(filePath, newName).then(res => {
if (this.isModifyCurrentFile()) {
tree.updateNodeParamValue(node, "name", newName);
node.name = newName; //repair current node
this.props.onFileClick(null, node);
tree.setOneNodeToActived({node});
this.setState({tree_data: tree});
} else {
tree.updateNodeParamValue(node, "name", newName);
this.setState({tree_data: tree});
}
})
}
if (type === 'dir') {
let _this = this;
this.props.editorUtilities.renameDir(filePath, newName).then(res => {
let tree = this.state.tree_data.copy();
let currentNode = this.state.currentNode;
if (this.isModifyContainsCurrentFile()) {
let nodePath = currentNode.path;
let filePath = _this.props.currentFilePath;
let start = filePath.indexOf(nodePath);
let node = currentNode.getNodeByPath(filePath.slice(start));
if (node) {
tree.updateNodeParamValue(currentNode, "name", newName);
currentNode.name = newName;
this.props.onFileClick(null, node);
tree.setOneNodeToActived({node});
this.setState({tree_data: tree});
}
} else {
tree.updateNodeParamValue(currentNode, "name", newName);
this.setState({tree_data: tree});
}
})
}
}
onDeleteNode = () => {
var currentNode = this.state.currentNode;
let filePath = currentNode.path;
let type = currentNode.type;
if (type === 'file') {
this.props.editorUtilities.deleteFile(filePath);
}
if (type === 'dir') {
this.props.editorUtilities.deleteDir(filePath);
}
let isCurrentFile = false;
if (this.state.currentNode.type === "dir") {
isCurrentFile = this.isModifyContainsCurrentFile();
} else {
isCurrentFile = this.isModifyCurrentFile();
}
let tree = this.state.tree_data.copy();
tree.removeNodeFromTree(currentNode);
if (isCurrentFile) {
let homeNode = this.getHomeNode();
this.props.onFileClick(null, homeNode);
tree.setNoneNodeActived();
this.setState({tree_data: tree})
} else {
this.setState({tree_data: tree})
}
}
isModifyCurrentFile() {
let nodeName = this.state.currentNode.name;
let filePath = this.props.currentFilePath;
let index = filePath.lastIndexOf("/");
let fileName = filePath.slice(index+1);
return nodeName === fileName;
}
isModifyContainsCurrentFile() {
let filePath = this.props.currentFilePath;
let nodePath = this.state.currentNode.path;
if (filePath.indexOf(nodePath) > -1) {
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);
}
componentWillReceiveProps(nextProps) {
let path = nextProps.searchedPath; //handle search module
if (path !== this.searchedPath) {
let node = this.state.tree_data.getNodeByPath(path);
this.searchedPath = path;
this.props.onFileClick(null, node);
let tree = this.state.tree_data.copy();
tree.setOneNodeToActived({node});
this.setState({
tree_data: tree
})
}
}
componentWillUnmount() {
document.removeEventListener('click', this.onHideContextMenu);
}
render() {
return (
<div className={`wiki-side-panel ${this.props.closeSideBar ? "": "left-zero"}`}>
<div className="side-panel-top panel-top">
<a href={siteRoot} id="logo">
<img src={mediaUrl + logoPath} title={siteTitle} alt="logo" width={logoWidth} height={logoHeight} />
</a>
<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"
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
permission={this.props.permission}
currentFilePath={this.props.currentFilePath}
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>
</div>
)
}
}
export default SidePanel;

View File

@@ -1,8 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import cookie from 'react-cookies';
import { keyCodes, bytesToSize } from './utils'; import { keyCodes, bytesToSize } from './utils';
import { siteRoot, avatarInfo, gettext } from './constance'; import { siteRoot, avatarInfo, gettext } from './constance';
import editorUtilities from '../utils/editor-utilties';
class Account extends Component { class Account extends Component {
@@ -72,7 +72,7 @@ class Account extends Component {
} }
getAccountInfo = () => { getAccountInfo = () => {
this.props.seafileAPI.getAccountInfo().then(resp => { editorUtilities.getAccountInfo().then(resp => {
this.setState({ this.setState({
userName: resp.data.name, userName: resp.data.name,
contactEmail: resp.data.email, contactEmail: resp.data.email,

View File

@@ -1,8 +1,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import Search from './search';
import MarkdownViewer from './markdown-viewer';
import Account from './account';
import { gettext, repoID, serviceUrl, slug, siteRoot } from './constance'; import { gettext, repoID, serviceUrl, slug, siteRoot } from './constance';
import Search from './search';
import Account from './account';
import MarkdownViewer from './markdown-viewer';
import TreeDirView from './tree-dir-view/tree-dir-view';
class MainPanel extends Component { class MainPanel extends Component {
@@ -16,15 +17,27 @@ class MainPanel extends Component {
window.location.href= serviceUrl + '/lib/' + repoID + '/file' + this.props.filePath + '?mode=edit'; window.location.href= serviceUrl + '/lib/' + repoID + '/file' + this.props.filePath + '?mode=edit';
} }
onMainNavBarClick = (e) => {
this.props.onMainNavBarClick(e.target.dataset.path);
}
render() { render() {
var filePathList = this.props.filePath.split('/');
var pathElem = filePathList.map((item, index) => { let filePathList = this.props.filePath.split('/');
if (item == "") { let nodePath = "";
let pathElem = filePathList.map((item, index) => {
if (item === "") {
return; return;
} else { }
if (index === (filePathList.length - 1)) {
return ( return (
<span key={index}><span className="path-split">/</span>{item}</span> <span key={index}><span className="path-split">/</span>{item}</span>
) )
} else {
nodePath += "/" + item;
return (
<a key={index} className="custom-link" data-path={nodePath} onClick={this.onMainNavBarClick}><span className="path-split">/</span>{item}</a>
)
} }
}); });
@@ -36,8 +49,8 @@ class MainPanel extends Component {
<a className="btn btn-secondary btn-topbar" onClick={this.onEditClick}>{gettext("Edit Page")}</a> <a className="btn btn-secondary btn-topbar" onClick={this.onEditClick}>{gettext("Edit Page")}</a>
</div> </div>
<div className="common-toolbar"> <div className="common-toolbar">
<Search seafileAPI={this.props.seafileAPI} onSearchedClick={this.props.onSearchedClick}/> <Search onSearchedClick={this.props.onSearchedClick}/>
<Account seafileAPI={this.props.seafileAPI} /> <Account />
</div> </div>
</div> </div>
<div className="cur-view-main"> <div className="cur-view-main">
@@ -50,13 +63,20 @@ class MainPanel extends Component {
</div> </div>
</div> </div>
<div className="cur-view-container"> <div className="cur-view-container">
<MarkdownViewer { this.props.isViewFileState && <MarkdownViewer
markdownContent={this.props.content} markdownContent={this.props.content}
latestContributor={this.props.latestContributor} latestContributor={this.props.latestContributor}
lastModified = {this.props.lastModified} lastModified = {this.props.lastModified}
onLinkClick={this.props.onLinkClick} onLinkClick={this.props.onLinkClick}
isFileLoading={this.props.isFileLoading} isFileLoading={this.props.isFileLoading}
/> />}
{ !this.props.isViewFileState &&
<TreeDirView
node={this.props.changedNode}
onMainNodeClick={this.props.onMainNodeClick}
>
</TreeDirView>
}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -55,14 +55,6 @@ class CreateFileForder extends React.Component {
this.newInput.setSelectionRange(0,0); this.newInput.setSelectionRange(0,0);
} }
changeState(isFile) {
if (isFile) {
this.setState({childName: '.md'});
} else{
this.setState({childName: ""});
}
}
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.changeState(nextProps.isFile); this.changeState(nextProps.isFile);
} }

View File

@@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { gettext, repoID } from './constance'; import { gettext, repoID } from './constance';
import SearchResultItem from './SearchResultItem'; import SearchResultItem from './search-result-item';
import editorUtilities from '../utils/editor-utilties';
class Search extends Component { class Search extends Component {
@@ -78,13 +79,13 @@ class Search extends Component {
isResultGetted: false isResultGetted: false
}) })
this.source = this.props.seafileAPI.getSource(); this.source = editorUtilities.getSource();
this.sendRequest(queryData, this.source.token); this.sendRequest(queryData, this.source.token);
} }
sendRequest(queryData, cancelToken) { sendRequest(queryData, cancelToken) {
var _this = this; var _this = this;
this.props.seafileAPI.searchFiles(queryData,cancelToken).then(res => { editorUtilities.searchFiles(queryData,cancelToken).then(res => {
if (!res.data.total) { if (!res.data.total) {
_this.setState({ _this.setState({
resultItems: [], resultItems: [],

View File

@@ -0,0 +1,164 @@
import React, { Component } from 'react';
import TreeView from './tree-view/tree-view';
import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from './constance';
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 = {
currentNode: null,
isNodeItemFrezee: false,
isShowMenu: false,
menuPosition: {
left: 0,
top: 0
},
isLoadFailed: false,
isMenuIconShow: false
}
this.searchedPath = null;
}
closeSide = () => {
this.props.onCloseSide();
}
onMouseEnter = () => {
this.setState({
isMenuIconShow: true
})
}
onMouseLeave = () => {
this.setState({
isMenuIconShow: false
})
}
onNodeClick = (e, node) => {
this.setState({currentNode: node})
this.props.onNodeClick(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.props.treeData.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.onAddFolderNode(dirPath);
}
onAddFileNode = (filePath) => {
this.props.onAddFileNode(filePath);
}
onRenameNode = (newName) => {
let node = this.state.currentNode;
this.props.onRenameNode(node, newName)
}
onDeleteNode = () => {
let node = this.state.currentNode;
this.props.onDeleteNode(node);
}
componentDidMount() {
document.addEventListener('click', this.onHideContextMenu);
}
componentWillReceiveProps(nextProps) {
this.setState({
currentNode: nextProps.changedNode
})
}
componentWillUnmount() {
document.removeEventListener('click', this.onHideContextMenu);
}
render() {
return (
<div className={`wiki-side-panel ${this.props.closeSideBar ? "": "left-zero"}`}>
<div className="side-panel-top panel-top">
<a href={siteRoot} id="logo">
<img src={mediaUrl + logoPath} title={siteTitle} alt="logo" width={logoWidth} height={logoHeight} />
</a>
<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"
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.props.treeData &&
<TreeView
permission={this.props.permission}
currentFilePath={this.props.currentFilePath}
treeData={this.props.treeData}
currentNode={this.state.currentNode}
isNodeItemFrezee={this.state.isNodeItemFrezee}
onNodeClick={this.onNodeClick}
onShowContextMenu={this.onShowContextMenu}
onDirCollapse={this.props.onDirCollapse}
/>
}
<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>
</div>
)
}
}
export default SidePanel;

View File

@@ -0,0 +1,33 @@
import React, { Component } from 'react';
class TreeDirList extends React.Component {
constructor(props) {
super(props);
this.state = {
isMourseEnter: false
}
}
onMainNodeClick = () => {
this.props.onMainNodeClick(this.props.node);
}
render() {
let node = this.props.node;
return (
<tr className='row' onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td className="dirent-icon" style={{width: "5%"}}>
<img src={node.type === "dir" ? "/media/img/folder-192.png" : "/media/img/file/192/txt.png"}></img>
</td>
<td style={{width: "60%"}}>
<a className="custom-link" onClick={this.onMainNodeClick}>{node.name}</a>
</td>
<td style={{width: "15%"}}>{node.size}</td>
<td style={{width: "20%"}} title={node.last_update_time}>{node.last_update_time}</td>
</tr>
)
}
}
export default TreeDirList;

View File

@@ -0,0 +1,33 @@
import React, { Component } from "react";
import TreeDirList from './tree-dir-list'
import "../../css/tree-dir-view.css";
const gettext = window.gettext;
class TreeDirView extends React.Component {
render() {
let node = this.props.node;
let children = node.hasChildren() ? node.children : null;
return (
<table className="doc-view-container">
<thead className="doc-view-header">
<tr className="row">
<th style={{width: "5%"}}></th>
<th style={{width: "60%"}}>{gettext('Name')}</th>
<th style={{width: "15%"}}>{gettext('Size')}</th>
<th style={{width: "20%"}}>{gettext('Last Update')}</th>
</tr>
</thead>
<tbody className="doc-view-body">
{children && children.map((node, index) => {
return (
<TreeDirList key={index} node={node} onMainNodeClick={this.props.onMainNodeClick}></TreeDirList>
)
})}
</tbody>
</table>
)
}
}
export default TreeDirView;

View File

@@ -1,85 +1,61 @@
class Node { class Node {
static create(attrs = {}) { static deserializefromJson(object) {
const {name, type, size, last_update_time, isExpanded = true, children = []} = object;
}
/**
* Create a `Node` from a JSON `object`.
*
* @param {Object} object
* @return {Node}
*/
static fromJSON(object) {
const {
name,
type,
isExpanded = true,
children = [],
} = object;
const node = new Node({ const node = new Node({
name, name,
type, type,
size,
last_update_time,
isExpanded, isExpanded,
children: children.map(Node.fromJSON), children: children.map(item => Node.deserializefromJson(item)),
}); });
return node; return node;
} }
constructor({name, type, size, last_update_time, isExpanded, children}) {
constructor({ name, type, isExpanded, children }) {
this.name = name; this.name = name;
this.type = type; this.type = type;
this.children = children ? children : []; this.size = size;
this.last_update_time = last_update_time;
this.isExpanded = isExpanded !== undefined ? isExpanded : true; this.isExpanded = isExpanded !== undefined ? isExpanded : true;
this.children = children ? children : [];
this.parent = null;
}
clone() {
var n = new Node({
name: this.name,
type: this.type,
size: this.size,
last_update_time: this.last_update_time,
isExpanded: this.isExpanded
});
n.children = this.children.map(child => {
var newChild = child.clone();
newChild.parent = n;
return newChild;
});
return n;
} }
get path() { get path() {
if (!this.parent) { if (!this.parent) {
return this.name; return this.name;
} else { } else {
var p = this.parent.path; let p = this.parent.path;
if (p === "/") return p === "/" ? (p + this.name) : (p + "/" + this.name);
return p + this.name;
else
return p + "/" + this.name;
} }
} }
copy() {
var n = new Node({
name: this.name,
type: this.type,
isExpanded: this.isExpanded
});
n.children = this.children.map(child => { var newChild = child.copy(); newChild.parent = n; return newChild; });
return n;
}
isRoot() {
return this.parent === undefined;
}
hasChildren() { hasChildren() {
return this.children.length > 0; return this.children.length > 0;
} }
isImage() { isRoot() {
let index = this.name.lastIndexOf("."); return this.parent === undefined;
if (index == -1) {
return false;
} else {
let type = this.name.substring(index).toLowerCase();
if (type == ".png" || type == ".jpg") {
return true;
} else {
return false;
}
}
} }
isMarkdown() { isMarkdown() {
@@ -100,39 +76,21 @@ class Node {
return this.type == "dir"; return this.type == "dir";
} }
getleafPaths() { isImage() {
let paths = new Map(); let index = this.name.lastIndexOf(".");
function getleafPath(node){ if (index == -1) {
if (node.hasChildren()) { return false;
let children = node.children; } else {
children.forEach(child => { let type = this.name.substring(index).toLowerCase();
if (child.hasChildren()) { if (type == ".png" || type == ".jpg") {
getleafPath(child); return true;
} else { } else {
let path = child.path; return false;
paths.set(path,child);
}
});
} }
} }
getleafPath(this);
return paths;
} }
getNodeByPath(path) { serializeToJson() {
let paths = this.getleafPaths();
if (paths.has(path)) {
return paths.get(path);
}
return null;
}
/**
* Return a JSON representation of the node.
*
* @return {Object}
*/
toJSON() {
var children = [] var children = []
if (this.hasChildren()) { if (this.hasChildren()) {
children = this.children.map(m => m.toJSON()); children = this.children.map(m => m.toJSON());
@@ -141,6 +99,8 @@ class Node {
const object = { const object = {
name: this.name, name: this.name,
type: this.type, type: this.type,
size: this.size,
last_update_time: this.last_update_time,
isExpanded: this.isExpanded, isExpanded: this.isExpanded,
children: children children: children
} }
@@ -150,5 +110,4 @@ class Node {
} }
export default Node;
export { Node }

View File

@@ -44,10 +44,7 @@ class TreeNodeView extends React.Component {
handleCollapse = (e) => { handleCollapse = (e) => {
e.stopPropagation(); e.stopPropagation();
const { node } = this.props; this.props.onDirCollapse(e, this.props.node);
if (this.props.treeView.toggleCollapse) {
this.props.treeView.toggleCollapse(node);
}
} }
onDragStart = (e) => { onDragStart = (e) => {
@@ -69,12 +66,12 @@ class TreeNodeView extends React.Component {
} }
componentDidMount() { componentDidMount() {
document.addEventListener('click', this.hideMenuIcon); document.addEventListener('click', this.hideMenuIcon);
} }
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener('click', this.hideMenuIcon); document.removeEventListener('click', this.hideMenuIcon);
} }
renderCollapse = () => { renderCollapse = () => {
const { node } = this.props; const { node } = this.props;
@@ -115,6 +112,7 @@ class TreeNodeView extends React.Component {
isNodeItemFrezee={this.props.isNodeItemFrezee} isNodeItemFrezee={this.props.isNodeItemFrezee}
permission={this.props.permission} permission={this.props.permission}
currentFilePath={this.props.currentFilePath} currentFilePath={this.props.currentFilePath}
onDirCollapse={this.props.onDirCollapse}
/> />
); );
})} })}
@@ -127,10 +125,11 @@ class TreeNodeView extends React.Component {
renderMenuController() { renderMenuController() {
if (this.props.permission === "rw") { if (this.props.permission === "rw") {
let isShow = (this.props.node.path === this.props.currentFilePath);
return ( return (
<div className="right-icon"> <div className="right-icon">
<MenuControl <MenuControl
isShow={this.state.isMenuIconShow} isShow={this.state.isMenuIconShow || isShow}
onClick={this.onMenuControlClick} onClick={this.onMenuControlClick}
/> />
</div> </div>
@@ -176,16 +175,15 @@ class TreeNodeView extends React.Component {
if (node.path === this.props.currentFilePath) { if (node.path === this.props.currentFilePath) {
hlClass = "tree-node-hight-light"; hlClass = "tree-node-hight-light";
} }
let customClass = "tree-node " + hlClass;
return ( return (
<div type={type} className={customClass} style={styles}> <div type={type} className="tree-node" style={styles}>
<div <div
onMouseLeave={this.onMouseLeave} onMouseLeave={this.onMouseLeave}
onMouseEnter={this.onMouseEnter} onMouseEnter={this.onMouseEnter}
onClick={this.onClick} onClick={this.onClick}
type={type} type={type}
className={`tree-node-inner text-nowrap ${node.name === '/'? 'hide': ''}`} className={`tree-node-inner text-nowrap ${hlClass} ${node.name === '/'? 'hide': ''}`}
> >
<div className="tree-node-text" type={type} draggable="true" onDragStart={this.onDragStart}>{node.name}</div> <div className="tree-node-text" type={type} draggable="true" onDragStart={this.onDragStart}>{node.name}</div>
<div className="left-icon"> <div className="left-icon">

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import TreeNodeView from './tree-node-view'; import TreeNodeView from './tree-node-view';
import editorUtilities from '../../utils/editor-utilties'
class TreeView extends React.PureComponent { class TreeView extends React.PureComponent {
@@ -10,29 +11,17 @@ class TreeView extends React.PureComponent {
*/ */
} }
toggleCollapse = (node) => { toggleCollapse = (e, node) => {
const tree = this.props.treeData; this.props.onDirCollapse(e, node);
node.isExpanded = !node.isExpanded;
// copy the tree to make PureComponent work
this.setState({
tree: tree.copy()
});
this.change(tree);
} }
onDragStart = (e, node) => { onDragStart = (e, node) => {
const url = this.props.editorUtilities.getFileURL(node); const url = editorUtilities.getFileURL(node);
e.dataTransfer.setData("text/uri-list", url); e.dataTransfer.setData("text/uri-list", url);
e.dataTransfer.setData("text/plain", url); e.dataTransfer.setData("text/plain", url);
} }
onNodeClick = (e, node) => { onNodeClick = (e, node) => {
if (node.isDir()) {
this.toggleCollapse(node);
return;
}
this.props.onNodeClick(e, node); this.props.onNodeClick(e, node);
} }
@@ -48,13 +37,14 @@ class TreeView extends React.PureComponent {
return ( return (
<div className="tree-view tree"> <div className="tree-view tree">
<TreeNodeView <TreeNodeView
paddingLeft={20} paddingLeft={12}
treeView={this} treeView={this}
node={this.props.treeData.root} node={this.props.treeData.root}
isNodeItemFrezee={this.props.isNodeItemFrezee} isNodeItemFrezee={this.props.isNodeItemFrezee}
permission={this.props.permission} permission={this.props.permission}
currentFilePath={this.props.currentFilePath} currentFilePath={this.props.currentFilePath}
onShowContextMenu={this.props.onShowContextMenu} onShowContextMenu={this.props.onShowContextMenu}
onDirCollapse={this.props.onDirCollapse}
/> />
</div> </div>
); );

View File

@@ -1,102 +1,145 @@
import { Node } from './node' import Node from './node';
import moment from 'moment';
class Tree { class Tree {
constructor() { constructor() {
this.root = null; this.root = null;
} }
copy() { clone() {
var t = new Tree(); var t = new Tree();
if (this.root) if (this.root)
t.root = this.root.copy(); t.root = this.root.clone();
return t; return t;
} }
setRoot(dir) { setRoot(node) {
this.root = dir; this.root = node;
} }
addChildToNode(node, child) { addNodeToParent(node, parentNode) {
child.parent = node; node.parent = parentNode;
node.children.push(child); parentNode.children.push(node);
return child; return node;
} }
addChild(node, child, insertIndex) { removeNodeFromParent(node, parentNode) {
if (!(child instanceof Node)) { let children = parentNode.children;
throw new TypeError('Child must be of type Node.');
}
if (insertIndex < 0 || insertIndex > node.children.length) {
throw new Error('Invalid index.');
}
child.parent = node;
node.children.splice(insertIndex, 0, child);
}
removeChildNode(node, child) {
let children = node.children;
let removeNode = null; let removeNode = null;
let index = null; let index = null;
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
if (child.path === children[i].path) { if (node.path === children[i].path) {
removeNode = children[i]; removeNode = children[i];
index = i; index = i;
break; break;
} }
} }
child.parent = null; node.parent = null;
node.children.splice(index, 1); parentNode.children.splice(index, 1);
return removeNode ? removeNode : null; return removeNode ? removeNode : null;
} }
addNodeToTree(node) { addNode(node) {
let parentNode = this.getNodeParentFromTree(node); let treeNodeParent = this.findNodeParentFromTree(node);
this.addChildToNode(parentNode, node); if (treeNodeParent) {
} this.addNodeToParent(node, treeNodeParent);
removeNodeFromTree(node) {
let parentNode = this.getNodeParentFromTree(node);
this.removeChildNode(parentNode, node);
}
getNodeParentFromTree(node) {
let parentNode = node.parent;
let findNode = null;
function cb(node) {
if(parentNode.path === node.path){
findNode = node;
return true;
}
return false;
}
this.traverseBF(cb);
return findNode;
}
getNodeByPath(path) {
let findNode = null;
function cb(node){
if (node.path === path) {
findNode = node;
return true;
}
return false;
}
this.traverseBF(cb);
return findNode;
}
updateNodeParamValue(node, param, value) {
let findNode = this.getNodeByPath(node.path);
if (findNode[param]) {
findNode[param] = value;
return true; return true;
} }
return false; return false;
} }
deleteNode(node) {
let treeNodeParent = this.findNodeParentFromTree(node);
if (treeNodeParent) {
this.removeNodeFromParent(node, treeNodeParent);
return true;
}
return false;
}
updateNodeParam(node, param, newValue) {
let treeNode = this.findNodeFromTree(node);
if (treeNode && treeNode[param]) {
treeNode[param] = newValue;
return true;
}
return false;
}
findNode(node) {
return this.findNodeFromTree(node);
}
findNodeFromTree(node) {
let findNode = this.getNodeByPath(node.path);
return findNode;
}
findNodeParentFromTree(node) {
let parentNode = node.parent;
let findNode = null;
function cb(treeNode) {
if (treeNode.path === parentNode.path) {
findNode = treeNode;
return true;
}
return false;
}
this.traverseDF(cb);
return findNode;
}
setNodeToActivated(node) {
this.setTreeToUnActivated();
let treeNode = this.findNodeFromTree(node);
if (treeNode) {
treeNode.isExpanded = true;
while (treeNode.parent) {
treeNode.parent.isExpanded = true;
treeNode = treeNode.parent;
}
return true;
}
return false;
}
setTreeToUnActivated() {
function cb(treeNode) {
treeNode.isExpanded = false;
return false;
}
this.traverseBF(cb);
this.root.isExpanded = true;
return true;
}
getNodeByPath(path) {
let findNode = null;
function cb(treeNode) {
if (treeNode.path === path) {
findNode = treeNode;
return true;
}
return false;
}
this.traverseBF(cb);
return findNode;
}
isNodeChild(parentNode, node) {
let isChild = false;
while(node.parent){
if(node.parent.path === parentNode.path){
isChild = true;
break;
}
node = node.parent;
}
return isChild;
}
traverseDF(callback) { traverseDF(callback) {
let stack = []; let stack = [];
let found = false; let found = false;
@@ -125,105 +168,46 @@ class Tree {
} }
} }
setOneNodeToActived({node}) { parseModelToTree(model) {
this.setNoneNodeActived();
let root = this.root;
root.isExpanded = true;
let layer2 = root.hasChildren() ? root.children : null; // direct to replace root child;
let isLayer2 = false;
for (let i = 0; i < layer2.length; i++) {
if (node.path === layer2[i].path) {
isLayer2 = true;
break;
}
}
if (isLayer2) {
return;
}
let replaceNode = null;
let needReplacedNode = null;
while (node.parent) {
let flag = false;
node.parent.isExpanded = true;
for (let i = 0; i < layer2.length; i++) {
if (node.parent.path === layer2[i].path) {
replaceNode = node.parent;
needReplacedNode = layer2[i];
flag = true;
break;
}
}
if (flag) {
break;
}
node = node.parent;
}
this.removeChildNode(root, needReplacedNode);
this.addChildToNode(root, replaceNode);
}
setNoneNodeActived() {
function setNodeToDeactived(node) {
if (node.isExpanded) {
node.isExpanded = false;
if (node.hasChildren()) {
let children = node.children;
children.forEach(function(child) {
setNodeToDeactived(child);
})
}
}
}
setNodeToDeactived(this.root);
this.root.isExpanded = true; // default to show;
return true;
}
/*
* parse tree from javascript object
*/
parse(model) {
var node = new Node({ var node = new Node({
name: model.name, name: model.name,
type: model.type, type: model.type,
isExpanded: model.isExpanded size: model.size,
last_update_time: moment.unix(model.last_update_time).fromNow(),
isExpanded: false
}); });
this.root = node; if (model.children instanceof Array) {
for (let child of model.children) { for (let child of model.children) {
this.addChildToNode(node, this.parseNode(child)); this.addNodeToParent(this.parseNodeToTree(child), node);
}
} }
return node;
} }
parseFromList(rootObj, nodeList) { parseListToTree(nodeList) {
var root = new Node({
name: rootObj.name, function getNodePath(parentPath, nodeName) {
type: rootObj.type, return parentPath === "/" ? (parentPath + nodeName) : (parentPath + "/" + nodeName);
isExpanded: rootObj.isExpanded
});
this.root = root;
var map = new Map();
map.set(root.name, root);
function joinPath(parent_path, name) {
if (parent_path === "/")
return parent_path + name;
else
return parent_path + "/" + name;
} }
var treeNodeList = [] let root = new Node({name: '/', type: 'dir', isExpanded: true});
this.root = root;
let map = new Map();
map.set(root.name, root);
let treeNodeList = [];
for (let nodeObj of nodeList) { for (let nodeObj of nodeList) {
var node = new Node({ let node = new Node({
name: nodeObj.name, name: nodeObj.name,
type: nodeObj.type, type: nodeObj.type,
size: nodeObj.size,
last_update_time: moment.unix(nodeObj.last_update_time).fromNow(),
isExpanded: false isExpanded: false
}); });
node.parent_path = nodeObj.parent_path; node.parent_path = nodeObj.parent_path;
treeNodeList.push(node); treeNodeList.push(node);
if (nodeObj.type === "dir") { if (node.isDir()) {
map.set(joinPath(nodeObj.parent_path, nodeObj.name), node); map.set(getNodePath(node.parent_path, node.name), node);
} }
} }
@@ -232,26 +216,28 @@ class Tree {
if (p === undefined) { if (p === undefined) {
console.log("warning: node " + node.parent_path + " not exist"); console.log("warning: node " + node.parent_path + " not exist");
} else { } else {
this.addChildToNode(p, node); this.addNodeToParent(node, p);
} }
} }
} }
parseNode(model) { parseNodeToTree(node) {
var node = new Node({ var node = new Node({
name: model.name, name: node.name,
type: model.type, type: node.type,
isExpanded: model.isExpanded size: node.size,
last_update_time: moment.unix(node.last_update_time).fromNow(),
isExpanded: false
}); });
if (model.children instanceof Array) { if (node.children instanceof Array) {
for (let child of model.children) { for (let child of node.children) {
this.addChildToNode(node, this.parseNode(child)); this.addNodeToParent(this.parseNodeToTree(child), node);
} }
} }
return node; return node;
} }
} }
export default Tree; export default Tree;

View File

@@ -102,13 +102,14 @@
.tree-node-inner { .tree-node-inner {
position: relative; position: relative;
padding-left: 12px; height: 26px;
height: 24px; cursor: pointer;
line-height: 1.625;
} }
.tree-node-inner .tree-node-text { .tree-node-inner .tree-node-text {
padding-left: 1.2rem; padding-left: 2.8rem;
width: calc(100% - 1.5rem); width: calc(100% - 2.8rem);
font-size: 15px; font-size: 15px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -122,27 +123,32 @@
align-items:center; align-items:center;
top: 0; top: 0;
left: 0; left: 0;
padding-left: 0.7rem; padding-left: 1.5rem;
} }
.folder-toggle-icon { .folder-toggle-icon {
position: absolute; position: absolute;
left: 0; left: 0;
line-height: 1.5; color: #c0c0c0;
line-height: 1.625;
width: 1.5rem;
display: flex;
align-content: center;
justify-content: center;
} }
.tree-node-icon { .tree-node-icon {
margin-right: 0.4rem;
margin-left: 0.1rem;
display: inline-block; display: inline-block;
width: 1rem; width: 1rem;
text-align: center; text-align: center;
color: #b0b0b0;
} }
.tree-node-inner .right-icon { .tree-node-inner .right-icon {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0;
width: 1.5rem; width: 1.5rem;
color: #888; color: #888;
z-index: 2; z-index: 2;
@@ -155,11 +161,16 @@
height: 100%; height: 100%;
vertical-align: middle; vertical-align: middle;
font-size: 0.8125rem; font-size: 0.8125rem;
line-height: 1.625 !important; line-height: 1.625rem !important;
} }
.tree-node-hight-light { .tree-node-hight-light {
background-color: rgb(255,239,178); color: #fff;
background-color: #feac74 !important;
}
.tree-node-hight-light i {
color:#fff;
} }
.dropdown-menu { .dropdown-menu {

View File

@@ -0,0 +1,50 @@
.doc-view-container {
width: 100%;
height: 100%;
margin: 10px 16px;
}
.doc-view-header .row,
.doc-view-body .row {
margin: 0;
}
.doc-view-body {
width: 100%;
height: 100%;
}
.doc-view-body tr:hover{
background-color: #f8f8f8;
}
.doc-view-header tr .dirent-icon,
.doc-view-body tr .dirent-icon {
display: flex;
align-items: center;
justify-content: center;
}
.doc-view-container td {
color: #333;
font-size: 14px;
word-break: break-all;
}
.doc-view-container th {
text-align: left;
font-weight: normal;
color: #9c9c9c;
}
.doc-view-container th, td {
padding-top: 5px;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
}
.doc-view-container .img-placeholder,
.doc-view-container tr img {
display: block;
width: 24px;
height: 24px;
}

View File

@@ -38,16 +38,7 @@
.wiki-md-viewer-rendered-content { .wiki-md-viewer-rendered-content {
padding: 30px 0 0; padding: 30px 0 0;
} }
.wiki-pages-container .tree-node-inner {
line-height: 1.625;
}
.wiki-pages-container .folder-toggle-icon {
color: #c0c0c0;
line-height: 1.625;
}
.wiki-pages-container .tree-node-icon {
color: #b0b0b0;
}
.wiki-main .cur-view-path { .wiki-main .cur-view-path {
border-bottom: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8;
margin-bottom: 8px; margin-bottom: 8px;
@@ -198,4 +189,13 @@ img[src=""] {
.wiki-md-viewer-rendered-content.article h1 { .wiki-md-viewer-rendered-content.article h1 {
margin-top: 0; margin-top: 0;
}
.custom-link {
color: #eb8205 !important;
cursor: pointer;
}
.custom-link:hover {
text-decoration: underline !important;
} }

View File

@@ -0,0 +1,71 @@
import { slug, repoID, siteRoot } from '../components/constance';
import { SeafileAPI } from 'seafile-js';
import cookie from 'react-cookies';
let seafileAPI = new SeafileAPI();
let xcsrfHeaders = cookie.load('sfcsrftoken');
seafileAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
class EditorUtilities {
getFiles() {
return seafileAPI.listWikiDir(slug, "/").then(items => {
const files = items.data.dir_file_list.map(item => {
return {
name: item.name,
type: item.type === 'dir' ? 'dir' : 'file',
isExpanded: item.type === 'dir' ? true : false,
parent_path: item.parent_dir,
last_update_time: item.last_update_time,
size: item.size
}
})
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)
}
getWikiFileContent(slug, filePath) {
return seafileAPI.getWikiFileContent(slug, filePath);
}
getSource() {
return seafileAPI.getSource();
}
searchFiles(queryData,cancelToken) {
return seafileAPI.searchFiles(queryData,cancelToken);
}
getAccountInfo() {
return seafileAPI.getAccountInfo();
}
}
const editorUtilities = new EditorUtilities();
export default editorUtilities;

View File

@@ -1,94 +1,396 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import SidePanel from './components/SidePanel';
import MainPanel from './components/MainPanel';
import moment from 'moment'; import moment from 'moment';
import cookie from 'react-cookies'; import { slug, repoID, serviceUrl, initialFilePath } from './components/constance';
import { SeafileAPI } from 'seafile-js'; import editorUtilities from './utils/editor-utilties';
import { slug, repoID, serviceUrl, initialFilePath, siteRoot } from './components/constance'; import SidePanel from './components/side-panel';
import MainPanel from './components/main-panel';
import Node from './components/tree-view/node'
import Tree from './components/tree-view/tree'
import 'seafile-ui';
import './assets/css/fa-solid.css'; import './assets/css/fa-solid.css';
import './assets/css/fa-regular.css'; import './assets/css/fa-regular.css';
import './assets/css/fontawesome.css'; import './assets/css/fontawesome.css';
import 'seafile-ui';
import './css/side-panel.css'; import './css/side-panel.css';
import './css/wiki.css'; import './css/wiki.css';
import './css/search.css'; import './css/search.css';
// init seafileAPI
let seafileAPI = new SeafileAPI();
let xcsrfHeaders = cookie.load('sfcsrftoken');
seafileAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
class EditorUtilities {
getFiles() {
return seafileAPI.listWikiDir(slug).then(items => {
const files = items.data.dir_file_list.map(item => {
return {
name: item.name,
type: item.type === 'dir' ? 'dir' : 'file',
isExpanded: item.type === 'dir' ? true : false,
parent_path: item.parent_dir,
}
})
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();
class Wiki extends Component { class Wiki extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
content: '', content: '',
tree_data: new Tree(),
closeSideBar: false, closeSideBar: false,
fileName: '',
filePath: '', filePath: '',
latestContributor: '', latestContributor: '',
lastModified: '', lastModified: '',
permission: '', permission: '',
isFileLoading: false, isFileLoading: false,
searchedPath: null, changedNode: null,
isViewFileState: true
}; };
window.onpopstate = this.onpopstate; window.onpopstate = this.onpopstate;
} }
componentDidMount() { componentDidMount() {
this.loadFile(initialFilePath); this.initSidePanelData();
this.initMainPanelData(initialFilePath);
} }
fileNameFromPath(filePath) { initMainPanelData(filePath) {
let index = filePath.lastIndexOf("/"); if (!this.isMarkdownFile(filePath)) {
if (index == -1) { filePath = "/home.md";
return ""; }
this.setState({isFileLoading: true});
editorUtilities.getWikiFileContent(slug, filePath)
.then(res => {
this.setState({
content: res.data.content,
latestContributor: res.data.latest_contributor,
lastModified: moment.unix(res.data.last_modified).fromNow(),
permission: res.data.permission,
filePath: filePath,
isFileLoading: false
})
})
const hash = window.location.hash;
let fileUrl = serviceUrl + '/wikis/' + slug + filePath + hash;
window.history.pushState({urlPath: fileUrl, filePath: filePath}, filePath, fileUrl);
}
initSidePanelData() {
editorUtilities.getFiles().then((files) => {
// construct the tree object
var treeData = new Tree();
treeData.parseListToTree(files);
let homeNode = this.getHomeNode(treeData);
this.setState({
tree_data: treeData,
changedNode: homeNode
});
}, () => {
console.log("failed to load files");
this.setState({
isLoadFailed: true
})
})
}
onLinkClick = (event) => {
const url = event.target.href;
if (this.isInternalMarkdownLink(url)) {
let path = this.getPathFromInternalMarkdownLink(url);
this.initMainPanelData(path);
} else { } else {
return filePath.substring(index + 1); window.location.href = url;
}
}
onpopstate = (event) => {
if (event.state && event.state.filePath) {
this.initMainPanelData(event.state.filePath);
}
}
onSearchedClick = (path) => {
if (this.state.currentFilePath !== path) {
this.initMainPanelData(path);
let tree = this.state.tree_data.clone();
let node = tree.getNodeByPath(path);
tree.setNodeToActivated(node);
let path = node.path; //node is file
this.enterViewFileState(tree, node, path);
}
}
onMainNavBarClick = (nodePath) => {
let tree = this.state.tree_data.clone();
let node = tree.getNodeByPath(nodePath);
tree.setNodeToActivated(node);
this.exitViewFileState(tree, node);
// update location url
let fileUrl = serviceUrl + '/wikis/' + slug + node.path;
window.history.pushState({urlPath: fileUrl, filePath: node.path},node.path, fileUrl);
}
onMainNodeClick = (node) => {
let tree = this.state.tree_data.clone();
tree.setNodeToActivated(node);
if (node.isMarkdown()) {
this.initMainPanelData(node.path);
this.enterViewFileState(tree, node, node.path);
} else if (node.isDir()){
this.exitViewFileState(tree, node);
} else {
const w=window.open('about:blank');
const url = serviceUrl + '/lib/' + repoID + '/file' + node.path;
w.location.href = url;
}
}
onNodeClick = (e, node) => {
if (node instanceof Node && node.isMarkdown()){
let tree = this.state.tree_data.clone();
this.initMainPanelData(node.path);
this.enterViewFileState(tree, node, node.path);
} else if(node instanceof Node && node.isDir()){
let tree = this.state.tree_data.clone();
this.exitViewFileState(tree, node);
} else {
const w=window.open('about:blank');
const url = serviceUrl + '/lib/' + repoID + '/file' + node.path;
w.location.href = url;
}
}
onDirCollapse = (e, node) => {
let tree = this.state.tree_data.clone();
let findNode = tree.getNodeByPath(node.path);
findNode.isExpanded = !findNode.isExpanded;
this.setState({tree_data: tree});
}
onMenuClick = () => {
this.setState({
closeSideBar: !this.state.closeSideBar,
})
}
onCloseSide = () => {
this.setState({
closeSideBar: !this.state.closeSideBar,
})
}
onAddFolderNode = (dirPath) => {
editorUtilities.createDir(dirPath).then(res => {
let tree = this.state.tree_data.clone();
let name = this.getFileNameByPath(dirPath);
let index = dirPath.lastIndexOf("/");
let parentPath = dirPath.substring(0, index);
if (!parentPath) {
parentPath = "/";
}
let node = this.buildNewNode(name, "dir");
let parentNode = tree.getNodeByPath(parentPath);
tree.addNodeToParent(node, parentNode);
if (this.state.isViewFileState) {
tree.setNodeToActivated(node);
this.setState({
tree_data: tree,
changedNode: node
})
} else {
this.exitViewFileState(tree, parentNode);
}
})
}
onAddFileNode = (filePath) => {
editorUtilities.createFile(filePath).then(res => {
let tree = this.state.tree_data.clone();
let name = this.getFileNameByPath(filePath);
let index = filePath.lastIndexOf("/");
let parentPath = filePath.substring(0, index);
if (!parentPath) {
parentPath = "/";
}
let node = this.buildNewNode(name, "file");
let parentNode = tree.getNodeByPath(parentPath);
tree.addNodeToParent(node, parentNode);
if (this.state.isViewFileState) {
tree.setNodeToActivated(node);
this.setState({
tree_data: tree,
changedNode: node
})
} else {
this.exitViewFileState(tree, parentNode);
}
})
}
onRenameNode = (node, newName) => {
let tree = this.state.tree_data.clone();
let filePath = node.path;
if (node.isMarkdown()) {
editorUtilities.renameFile(filePath, newName).then(res => {
let date = new Date().getTime()/1000;
tree.updateNodeParam(node, "name", newName);
node.name = newName;
tree.updateNodeParam(node, "last_update_time", moment.unix(date).fromNow());
node.last_update_time = moment.unix(date).fromNow();
if (this.state.isViewFileState) {
if (this.isModifyCurrentFile(node)) {
tree.setNodeToActivated(node);
this.setState({
tree_data: tree,
changedNode: node
});
this.initMainPanelData(node.path);
} else {
this.setState({tree_data: tree});
}
} else {
this.setState({tree_data: tree});
}
})
} else if (node.isDir()) {
editorUtilities.renameDir(filePath, newName).then(res => {
let currentFilePath = this.state.filePath;// the sequence is must right
let currentFileNode = tree.getNodeByPath(currentFilePath);
let date = new Date().getTime()/1000;
tree.updateNodeParam(node, "name", newName);
node.name = newName; // just synchronization node data && tree data;
tree.updateNodeParam(node, "last_update_time", moment.unix(date).fromNow());
node.last_update_time = moment.unix(date).fromNow();
if (this.state.isViewFileState) {
if (this.isModifyContainsCurrentFile(node)) {
tree.setNodeToActivated(currentFileNode);
this.setState({
tree_data: tree,
changedNode: currentFileNode
});
this.initMainPanelData(currentFileNode.path);
} else {
this.setState({tree_data: tree});
}
} else {
if (node.path.indexOf(currentFilePath) > -1) {
tree.setNodeToActivated(currentFileNode);
this.exitViewFileState(tree, currentFileNode);
} else {
this.setState({tree_data: tree});
}
}
})
}
}
onDeleteNode = (node) => {
let filePath = node.path;
if (node.isMarkdown()) {
editorUtilities.deleteFile(filePath);
} else if (node.isDir()) {
editorUtilities.deleteDir(filePath);
} else {
return false;
}
let isCurrentFile = false;
if (node.isDir()) {
isCurrentFile = this.isModifyContainsCurrentFile(node);
} else {
isCurrentFile = this.isModifyCurrentFile(node);
}
let tree = this.state.tree_data.clone();
if (this.state.isViewFileState) {
if (isCurrentFile) {
let homeNode = this.getHomeNode(tree);
tree.setNodeToActivated(homeNode);
this.setState({
tree_data: tree,
changedNode: homeNode
})
this.initMainPanelData(homeNode.path);
} else {
this.setState({tree_data: tree})
}
} else {
let parentNode = tree.getNodeByPath(this.state.filePath);
let isChild = tree.isNodeChild(parentNode, node);
if (isChild) {
this.exitViewFileState(tree, parentNode);
} else {
this.setState({tree_data: tree});
}
}
tree.deleteNode(node);
}
enterViewFileState(newTree, newNode, newPath) {
this.setState({
tree_data: newTree,
changedNode: newNode,
filePath: newPath,
isViewFileState: true
});
}
exitViewFileState(newTree, newNode) {
this.setState({
tree_data: newTree,
changedNode: newNode,
filePath: newNode.path,
isViewFileState: false
});
let fileUrl = serviceUrl + '/wikis/' + slug + newNode.path;
window.history.pushState({urlPath: fileUrl, filePath: newNode.path}, newNode.path, fileUrl);
}
getFileNameByPath(path) {
let index = path.lastIndexOf("/");
if (index === -1) {
return "";
}
return path.slice(index+1);
}
getHomeNode(treeData) {
return treeData.getNodeByPath("/home.md");
}
buildNewNode(name, type) {
let date = new Date().getTime()/1000;
let node = new Node({
name : name,
type: type,
size: '0',
last_update_time: moment.unix(date).fromNow(),
isExpanded: false,
children: []
});
return node;
}
isModifyCurrentFile(node) {
let nodeName = node.name;
let fileName = this.getFileNameByPath(this.state.filePath);
return nodeName === fileName;
}
isModifyContainsCurrentFile(node) {
let filePath = this.state.filePath;
let nodePath = node.path;
if (filePath.indexOf(nodePath) > -1) {
return true;
}
return false;
}
isMarkdownFile(filePath) {
let index = filePath.lastIndexOf(".");
if (index === -1) {
return false;
} else {
let type = filePath.substring(index).toLowerCase();
if (type === ".md" || type === ".markdown") {
return true;
} else {
return false;
}
} }
} }
@@ -104,94 +406,37 @@ class Wiki extends Component {
return path; return path;
} }
onLinkClick = (event) => {
const url = event.target.href;
if (this.isInternalMarkdownLink(url)) {
let path = this.getPathFromInternalMarkdownLink(url);
this.loadFile(path);
} else {
window.location.href = url;
}
}
onSearchedClick = (path) => {
if (this.state.currentFilePath !== path) {
this.setState({searchedPath : path});
}
}
onFileClick = (e, node) => {
if (node.isMarkdown()) {
this.loadFile(node.path);
} else {
const w=window.open('about:blank');
const url = serviceUrl + '/lib/' + repoID + '/file' + node.path;
w.location.href = url;
}
}
loadFile(filePath) {
this.setState({isFileLoading: true});
seafileAPI.getWikiFileContent(slug, filePath)
.then(res => {
this.setState({
content: res.data.content,
latestContributor: res.data.latest_contributor,
lastModified: moment.unix(res.data.last_modified).fromNow(),
permission: res.data.permission,
fileName: this.fileNameFromPath(filePath),
filePath: filePath,
isFileLoading: false,
})
})
const hash = window.location.hash;
let fileUrl = serviceUrl + '/wikis/' + slug + filePath + hash;
window.history.pushState({urlPath: fileUrl, filePath: filePath}, filePath, fileUrl);
}
onpopstate = (event) => {
if (event.state && event.state.filePath) {
this.loadFile(event.state.filePath);
}
}
onMenuClick = () => {
this.setState({
closeSideBar: !this.state.closeSideBar,
})
}
onCloseSide = () => {
this.setState({
closeSideBar: !this.state.closeSideBar,
})
}
render() { render() {
return ( return (
<div id="main" className="wiki-main"> <div id="main" className="wiki-main">
<SidePanel <SidePanel
onFileClick={this.onFileClick} onNodeClick={this.onNodeClick}
closeSideBar={this.state.closeSideBar} closeSideBar={this.state.closeSideBar}
onCloseSide ={this.onCloseSide} onCloseSide ={this.onCloseSide}
editorUtilities={editorUtilities} treeData={this.state.tree_data}
permission={this.state.permission} permission={this.state.permission}
currentFilePath={this.state.filePath} currentFilePath={this.state.filePath}
searchedPath={this.state.searchedPath} changedNode={this.state.changedNode}
onAddFolderNode={this.onAddFolderNode}
onAddFileNode={this.onAddFileNode}
onRenameNode={this.onRenameNode}
onDeleteNode={this.onDeleteNode}
onDirCollapse={this.onDirCollapse}
/> />
<MainPanel <MainPanel
content={this.state.content} content={this.state.content}
fileName={this.state.fileName}
filePath={this.state.filePath} filePath={this.state.filePath}
latestContributor={this.state.latestContributor}
lastModified={this.state.lastModified}
permission={this.state.permission}
isViewFileState={this.state.isViewFileState}
changedNode={this.state.changedNode}
isFileLoading={this.state.isFileLoading}
onLinkClick={this.onLinkClick} onLinkClick={this.onLinkClick}
onMenuClick={this.onMenuClick} onMenuClick={this.onMenuClick}
onSearchedClick={this.onSearchedClick} onSearchedClick={this.onSearchedClick}
latestContributor={this.state.latestContributor} onMainNavBarClick={this.onMainNavBarClick}
lastModified={this.state.lastModified} onMainNodeClick={this.onMainNodeClick}
seafileAPI={seafileAPI}
permission={this.state.permission}
isFileLoading={this.state.isFileLoading}
/> />
</div> </div>
) )

View File

@@ -219,6 +219,11 @@ class WikiPagesDirView(APIView):
error_msg = "Wiki not found." error_msg = "Wiki not found."
return api_error(status.HTTP_404_NOT_FOUND, error_msg) return api_error(status.HTTP_404_NOT_FOUND, error_msg)
path = request.GET.get("p", '')
if not path:
error_msg = "Folder not found."
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# perm check # perm check
if not wiki.check_access_wiki(request): if not wiki.check_access_wiki(request):
error_msg = "Permission denied" error_msg = "Permission denied"
@@ -233,12 +238,12 @@ class WikiPagesDirView(APIView):
error_msg = "Internal Server Error" error_msg = "Internal Server Error"
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
dir_id = seafile_api.get_dir_id_by_path(repo.repo_id, '/') dir_id = seafile_api.get_dir_id_by_path(repo.repo_id, path)
if not dir_id: if not dir_id:
error_msg = 'Folder %s not found.' % '/' error_msg = 'Folder %s not found.' % path
return api_error(status.HTTP_404_NOT_FOUND, error_msg) return api_error(status.HTTP_404_NOT_FOUND, error_msg)
all_dirs = get_wiki_dirs_by_path(repo.repo_id, '/', []) all_dirs = get_wiki_dirs_by_path(repo.repo_id, path, [])
return Response({ return Response({
"dir_file_list": all_dirs "dir_file_list": all_dirs

View File

@@ -239,6 +239,8 @@ def get_wiki_dirs_by_path(repo_id, path, all_dirs):
entry["parent_dir"] = path entry["parent_dir"] = path
entry["name"] = dirent.obj_name entry["name"] = dirent.obj_name
entry["size"] = dirent.size
entry["last_update_time"] = dirent.mtime
all_dirs.append(entry) all_dirs.append(entry)