1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-01 23:20:51 +00:00

view repo via wiki (#2354)

* view repo via wiki

* Modify details

* add list/grid mode

* [dist]
This commit is contained in:
C_Q
2018-09-12 10:32:31 +08:00
committed by Daniel Pan
parent dcf35018c1
commit 7bd50ad009
28 changed files with 877 additions and 18 deletions

View File

@@ -196,7 +196,7 @@ class Search extends Component {
type="text"
className="search-input"
name="query"
placeholder={gettext("Search files in this wiki")}
placeholder={this.props.placeholder}
style={style}
value={this.state.value}
onFocus={this.onFocusHandler}

View File

@@ -0,0 +1,99 @@
import React, { Component } from 'react';
import { gettext, repoID, serviceUrl, slug, siteRoot } from '../../components/constance';
import Search from '../../components/search';
import Account from '../../components/account';
import MarkdownViewer from '../../components/markdown-viewer';
import TreeDirView from '../../components/tree-dir-view/tree-dir-view';
// const repoName = window.repo.config.repo_name
class MainPanel extends Component {
onMenuClick = () => {
this.props.onMenuClick();
}
onEditClick = (e) => {
// const w=window.open('about:blank')
e.preventDefault();
window.location.href= serviceUrl + '/lib/' + repoID + '/file' + this.props.filePath + '?mode=edit';
}
onMainNavBarClick = (e) => {
this.props.onMainNavBarClick(e.target.dataset.path);
}
switchViewMode = (e) => {
e.preventDefault();
this.props.switchViewMode(e.target.id);
}
render() {
let filePathList = this.props.filePath.split('/');
let nodePath = "";
let pathElem = filePathList.map((item, index) => {
if (item === "") {
return;
}
if (index === (filePathList.length - 1)) {
return (
<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>
)
}
});
return (
<div className="wiki-main-panel o-hidden">
<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' &&
<a className="btn btn-secondary btn-topbar" title="Edit File" onClick={this.onEditClick}>{gettext("Edit")}</a>
}
<a className="btn btn-secondary btn-topbar sf2-icon-list-view" id='list' title={gettext("List")} onClick={this.switchViewMode}></a>
<a className="btn btn-secondary btn-topbar sf2-icon-grid-view" id='grid' title={gettext("Grid")} onClick={this.switchViewMode}></a>
</div>
<div className="common-toolbar">
<Search onSearchedClick={this.props.onSearchedClick}
placeholder={gettext("Search files in this library")}/>
<Account />
</div>
</div>
<div className="cur-view-main">
<div className="cur-view-path">
<div className="path-containter">
<a href={siteRoot + '#my-libs/'} className="normal">{gettext("Libraries")}</a>
<span className="path-split">/</span>
<a href={siteRoot + 'wiki/lib/' + repoID + '/'} className="normal">{slug}</a>
{pathElem}
</div>
</div>
<div className="cur-view-container">
{ this.props.isViewFileState && <MarkdownViewer
markdownContent={this.props.content}
latestContributor={this.props.latestContributor}
lastModified = {this.props.lastModified}
onLinkClick={this.props.onLinkClick}
isFileLoading={this.props.isFileLoading}
/>}
{ !this.props.isViewFileState &&
<TreeDirView
node={this.props.changedNode}
onMainNodeClick={this.props.onMainNodeClick}
>
</TreeDirView>
}
</div>
</div>
</div>
)
}
}
export default MainPanel;

View File

@@ -0,0 +1,164 @@
import React, { Component } from 'react';
import TreeView from '../../components/tree-view/tree-view';
import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from '../../components/constance';
import NodeMenu from '../../components/menu-component/node-menu';
import MenuControl from '../../components/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("Files")}
<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

@@ -49,7 +49,9 @@ class MainPanel extends Component {
<a className="btn btn-secondary btn-topbar" onClick={this.onEditClick}>{gettext("Edit Page")}</a>
</div>
<div className="common-toolbar">
<Search onSearchedClick={this.props.onSearchedClick}/>
<Search onSearchedClick={this.props.onSearchedClick}
placeholder={gettext("Search files in this wiki")}
/>
<Account />
</div>
</div>

View File

@@ -0,0 +1,502 @@
import React, { Component } from 'react';
import cookie from 'react-cookies';
import ReactDOM from 'react-dom';
import SidePanel from './pages/repo-wiki-mode/side-panel';
import MainPanel from './pages/repo-wiki-mode/main-panel';
import moment from 'moment';
import { slug, repoID, serviceUrl, initialFilePath } from './components/constance';
import editorUtilities from './utils/editor-utilties';
import { seafileAPI } from './utils/editor-utilties';
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-regular.css';
import './assets/css/fontawesome.css';
import './css/side-panel.css';
import './css/wiki.css';
import './css/search.css';
class Wiki extends Component {
constructor(props) {
super(props);
this.state = {
content: '',
tree_data: new Tree(),
closeSideBar: false,
filePath: '',
latestContributor: '',
lastModified: '',
permission: '',
isFileLoading: false,
changedNode: null,
isViewFileState: true
};
window.onpopstate = this.onpopstate;
}
componentDidMount() {
this.initWikiData(initialFilePath);
}
initWikiData(filePath){
this.setState({isFileLoading: true});
editorUtilities.listRepoDir().then((files) => {
// construct the tree object
var treeData = new Tree();
treeData.parseListToTree(files);
let node = treeData.getNodeByPath(filePath);
if (node.isDir()) {
this.exitViewFileState(treeData, node);
this.setState({isFileLoading: false});
} else {
seafileAPI.getFileInfo(repoID, filePath).then((res) => {
let { mtime, size, starred, permission, last_modifier_name } = res.data;
this.setState({
tree_data: treeData,
latestContributor: last_modifier_name,
lastModified: moment.unix(mtime).fromNow(),
permission: permission,
filePath: filePath,
})
seafileAPI.getFileDownloadLink(repoID, filePath).then((res) => {
const downLoadUrl = res.data;
seafileAPI.getFileContent(downLoadUrl).then((res) => {
this.setState({
content: res.data,
isFileLoading: false
})
})
});
})
let fileUrl = serviceUrl + '/wiki/lib/' + repoID + filePath;
window.history.pushState({urlPath: fileUrl, filePath: filePath}, filePath, fileUrl);
}
}, () => {
console.log("failed to load files");
this.setState({
isLoadFailed: true
})
})
}
initMainPanelData(filePath) {
this.setState({isFileLoading: true});
seafileAPI.getFileInfo(repoID, filePath).then((res) => {
let { mtime, size, starred, permission, last_modifier_name } = res.data;
this.setState({
latestContributor: last_modifier_name,
lastModified: moment.unix(mtime).fromNow(),
permission: permission,
filePath: filePath,
})
seafileAPI.getFileDownloadLink(repoID, filePath).then((res) => {
const downLoadUrl = res.data;
seafileAPI.getFileContent(downLoadUrl).then((res) => {
this.setState({
content: res.data,
isFileLoading: false
})
})
});
})
let fileUrl = serviceUrl + '/wiki/lib/' + repoID + filePath;
window.history.pushState({urlPath: fileUrl, filePath: filePath}, filePath, fileUrl);
}
switchViewMode = (mode) => {
let dirPath;
let tree = this.state.tree_data
let node = tree.getNodeByPath(this.state.filePath)
if (node.isDir()) {
dirPath = this.state.filePath
} else {
const index = this.state.filePath.lastIndexOf('/');
dirPath = this.state.filePath.substring(0, index);
}
cookie.save("view_mode", mode, { path: '/' })
window.location.href = serviceUrl + "/#common/lib/" + repoID + dirPath;
}
onLinkClick = (event) => {
const url = event.target.href;
if (this.isInternalMarkdownLink(url)) {
let path = this.getPathFromInternalMarkdownLink(url);
this.initMainPanelData(path);
} else {
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 + '/wiki/lib/' + repoID + 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 isPathEqual = (node.path === 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 (isPathEqual || 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 + '/wiki/lib/' + repoID + 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;
}
}
}
isInternalMarkdownLink(url) {
var re = new RegExp(serviceUrl + '/lib/' + repoID + '/file' + '.*\.md$');
return re.test(url);
}
getPathFromInternalMarkdownLink(url) {
var re = new RegExp(serviceUrl + '/lib/' + repoID + '/file' + "(.*\.md)");
var array = re.exec(url);
var path = decodeURIComponent(array[1]);
return path;
}
render() {
return (
<div id="main" className="wiki-main">
<SidePanel
onNodeClick={this.onNodeClick}
closeSideBar={this.state.closeSideBar}
onCloseSide ={this.onCloseSide}
treeData={this.state.tree_data}
permission={this.state.permission}
currentFilePath={this.state.filePath}
changedNode={this.state.changedNode}
onAddFolderNode={this.onAddFolderNode}
onAddFileNode={this.onAddFileNode}
onRenameNode={this.onRenameNode}
onDeleteNode={this.onDeleteNode}
onDirCollapse={this.onDirCollapse}
/>
<MainPanel
content={this.state.content}
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}
onMenuClick={this.onMenuClick}
onSearchedClick={this.onSearchedClick}
onMainNavBarClick={this.onMainNavBarClick}
onMainNodeClick={this.onMainNodeClick}
switchViewMode={this.switchViewMode}
/>
</div>
)
}
}
ReactDOM.render (
<Wiki />,
document.getElementById('wrapper')
)

View File

@@ -1,6 +1,7 @@
import { slug, repoID, siteRoot } from '../components/constance';
import { SeafileAPI } from 'seafile-js';
import cookie from 'react-cookies';
import { bytesToSize } from '../components/utils'
let seafileAPI = new SeafileAPI();
let xcsrfHeaders = cookie.load('sfcsrftoken');
@@ -17,13 +18,31 @@ class EditorUtilities {
isExpanded: item.type === 'dir' ? true : false,
parent_path: item.parent_dir,
last_update_time: item.last_update_time,
size: item.size
size: bytesToSize(item.size)
}
})
return files;
})
}
listRepoDir() {
return seafileAPI.listDir(repoID, "/",{recursive: true}).then(items => {
const files = items.data.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.mtime,
size: item.size ? bytesToSize(item.size) : '0 bytes'
}
})
return files;
})
}
createFile(filePath) {
return seafileAPI.createFile(repoID, filePath)
}
@@ -69,3 +88,4 @@ class EditorUtilities {
const editorUtilities = new EditorUtilities();
export default editorUtilities;
export { seafileAPI };