mirror of
https://github.com/haiwen/seahub.git
synced 2025-05-11 01:17:02 +00:00
view repo via wiki (#2354)
* view repo via wiki * Modify details * add list/grid mode * [dist]
This commit is contained in:
parent
dcf35018c1
commit
7bd50ad009
frontend
build/frontend
css
js
config
package-lock.jsonsrc
webpack-stats.pro.jsonmedia/css
seahub
16
frontend/build/frontend/css/repoview.css
Normal file
16
frontend/build/frontend/css/repoview.css
Normal file
File diff suppressed because one or more lines are too long
1
frontend/build/frontend/css/repoview.css.map
Normal file
1
frontend/build/frontend/css/repoview.css.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
frontend/build/frontend/js/repoview.js
Normal file
2
frontend/build/frontend/js/repoview.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/build/frontend/js/repoview.js.map
Normal file
1
frontend/build/frontend/js/repoview.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -60,6 +60,7 @@ module.exports = {
|
||||
entry: {
|
||||
main: [require.resolve('./polyfills'), paths.appIndexJs],
|
||||
wiki: [require.resolve('./polyfills'), paths.appSrc + "/wiki.js"],
|
||||
repoview: [require.resolve('./polyfills'), paths.appSrc + "/repo-wiki-mode.js"],
|
||||
},
|
||||
|
||||
output: {
|
||||
|
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
@ -48,7 +48,7 @@
|
||||
"remark-parse": "5.0.0",
|
||||
"remark-rehype": "3.0.0",
|
||||
"remark-slug": "5.0.0",
|
||||
"seafile-js": "0.2.12",
|
||||
"seafile-js": "0.2.13",
|
||||
"seafile-ui": "0.1.10",
|
||||
"slate": "0.34.2",
|
||||
"slate-drop-or-paste-images": "0.8.3",
|
||||
@ -10338,9 +10338,9 @@
|
||||
}
|
||||
},
|
||||
"seafile-js": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.12.tgz",
|
||||
"integrity": "sha512-sXt6L0S2ejPFemQ54jknYG6RQCzDrwArVCy59azD1d+5OChQFcaBQNGpGU6Yr3M0MONtaAYZJxvXXy8a7+/Qyg==",
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.13.tgz",
|
||||
"integrity": "sha512-NANdpSi75zaGANvrgCcdiamoFKzkyUyTmwhM/sp/oQt0+1zr+p57/oOduCRvL1+MvKQC0nK0Qf/Z/WdQqri/mA==",
|
||||
"requires": {
|
||||
"axios": "0.18.0",
|
||||
"form-data": "2.3.2"
|
||||
|
@ -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}
|
||||
|
99
frontend/src/pages/repo-wiki-mode/main-panel.js
Normal file
99
frontend/src/pages/repo-wiki-mode/main-panel.js
Normal 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;
|
164
frontend/src/pages/repo-wiki-mode/side-panel.js
Normal file
164
frontend/src/pages/repo-wiki-mode/side-panel.js
Normal 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;
|
@ -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>
|
||||
|
502
frontend/src/repo-wiki-mode.js
Normal file
502
frontend/src/repo-wiki-mode.js
Normal 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')
|
||||
)
|
@ -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 };
|
||||
|
@ -1 +1 @@
|
||||
{"status":"done","chunks":{"main":[{"name":"js/main.js","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/main.js"},{"name":"css/main.css","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/main.css"},{"name":"js/main.js.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/main.js.map"},{"name":"css/main.css.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/main.css.map"}],"wiki":[{"name":"js/wiki.js","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/wiki.js"},{"name":"css/wiki.css","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/wiki.css"},{"name":"js/wiki.js.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/wiki.js.map"},{"name":"css/wiki.css.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/wiki.css.map"}]}}
|
||||
{"status":"done","chunks":{"main":[{"name":"js/main.js","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/main.js"},{"name":"css/main.css","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/main.css"},{"name":"js/main.js.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/main.js.map"},{"name":"css/main.css.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/main.css.map"}],"wiki":[{"name":"js/wiki.js","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/wiki.js"},{"name":"css/wiki.css","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/wiki.css"},{"name":"js/wiki.js.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/wiki.js.map"},{"name":"css/wiki.css.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/wiki.css.map"}],"repoview":[{"name":"js/repoview.js","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/repoview.js"},{"name":"css/repoview.css","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/repoview.css"},{"name":"js/repoview.js.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/js/repoview.js.map"},{"name":"css/repoview.css.map","path":"/home/yxzhang/dev/seahub/frontend/build/frontend/css/repoview.css.map"}]}}
|
@ -4167,6 +4167,7 @@ img.thumbnail {
|
||||
.switch-mode {
|
||||
margin-left:15px;
|
||||
}
|
||||
.wiki-view-icon-btn,
|
||||
.grid-view-icon-btn,
|
||||
.list-view-icon-btn {
|
||||
display:inline-block;
|
||||
@ -4179,6 +4180,7 @@ img.thumbnail {
|
||||
cursor:pointer;
|
||||
border-radius:0;
|
||||
}
|
||||
|
||||
.grid-view-icon-btn.active,
|
||||
.list-view-icon-btn.active {
|
||||
color:#fff;
|
||||
|
@ -32,6 +32,8 @@
|
||||
.sf2-icon-wrench:before { content:"\e001"; }
|
||||
.sf2-icon-bell:before { content:"\e003"; }
|
||||
.sf2-icon-x3:before { content:"\e035"; }
|
||||
.sf2-icon-grid-view:before { content:"\e025"; }
|
||||
.sf2-icon-list-view:before { content:"\e026"; }
|
||||
|
||||
|
||||
/****** icon-xx ********/
|
||||
|
@ -118,6 +118,7 @@ def base(request):
|
||||
'enable_terms_and_conditions': config.ENABLE_TERMS_AND_CONDITIONS,
|
||||
'show_logout_icon': SHOW_LOGOUT_ICON,
|
||||
'is_pro': True if is_pro_version() else False,
|
||||
'enable_repo_wiki_mode': dj_settings.ENABLE_REPO_WIKI_MODE,
|
||||
}
|
||||
|
||||
if request.user.is_staff:
|
||||
|
@ -707,6 +707,9 @@ ENABLE_WIKI = False
|
||||
# Enable 'repo snapshot label' feature
|
||||
ENABLE_REPO_SNAPSHOT_LABEL = False
|
||||
|
||||
# Repo wiki mode
|
||||
ENABLE_REPO_WIKI_MODE = True
|
||||
|
||||
#####################
|
||||
# External settings #
|
||||
#####################
|
||||
|
@ -493,6 +493,9 @@
|
||||
|
||||
<div class="switch-mode hidden-sm-down">
|
||||
<button class="list-view-icon-btn sf2-icon-list-view <% if (mode == 'list') { %>active<% } %>" title="{% trans "List" %}" id="js-switch-list-view" aria-label="{% trans "list view" %}"></button><button class="grid-view-icon-btn sf2-icon-grid-view <% if (mode == 'grid') { %>active<% } %>" title="{% trans "Grid" %}" id="js-switch-grid-view" aria-label="{% trans "grid view" %}"></button>
|
||||
<% if (app.pageOptions.enable_repo_wiki_mode) { %>
|
||||
<a class="wiki-view-icon-btn sf2-icon-wiki" href="<%= site_root %>wiki/lib/<%- repo_id %><%- path %>" title="{% trans "Wiki" %}" aria-label="{% trans "Wiki view" %}"></a>
|
||||
<% } %>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/template" id="dir-view-toolbar2-tmpl">
|
||||
|
@ -303,8 +303,8 @@ app["pageOptions"] = {
|
||||
enable_group_discussion: {% if enable_group_discussion %} true {% else %} false {% endif %},
|
||||
enable_file_comment: {% if enable_file_comment %} true {% else %} false {% endif %},
|
||||
enable_office_web_app: {% if enable_office_web_app %} true {% else %} false {% endif %},
|
||||
enable_onlyoffice: {% if enable_onlyoffice %} true {% else %} false {% endif %}
|
||||
|
||||
enable_onlyoffice: {% if enable_onlyoffice %} true {% else %} false {% endif %},
|
||||
enable_repo_wiki_mode: {% if enable_repo_wiki_mode %} true {% else %} false {% endif %}
|
||||
};
|
||||
app.ui = {
|
||||
currentDropdown: null,
|
||||
|
20
seahub/templates/view_lib_as_wiki.html
Normal file
20
seahub/templates/view_lib_as_wiki.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends "base_for_react.html" %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% block extra_style %}
|
||||
{% render_bundle 'repoview' 'css' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script type="text/javascript">
|
||||
window.wiki = {
|
||||
config: {
|
||||
repoId: "{{ repo_id }}",
|
||||
serviceUrl: "{{ service_url}}",
|
||||
initial_file_path: "{{ file_path }}",
|
||||
slug: "{{ repo_name }}"
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{% render_bundle 'repoview' 'js' %}
|
||||
{% endblock %}
|
@ -14,7 +14,7 @@ from seahub.views.file import view_history_file, view_trash_file,\
|
||||
text_diff, view_raw_file, view_raw_shared_file, \
|
||||
download_file, view_lib_file, file_access, view_lib_file_via_smart_link
|
||||
from seahub.views.repo import repo_history_view, view_shared_dir, \
|
||||
view_shared_upload_link
|
||||
view_shared_upload_link, view_lib_as_wiki
|
||||
from notifications.views import notification_list
|
||||
from seahub.views.wiki import personal_wiki, personal_wiki_pages, \
|
||||
personal_wiki_create, personal_wiki_page_new, personal_wiki_page_edit, \
|
||||
@ -155,6 +155,7 @@ urlpatterns = [
|
||||
### lib (replace the old `repo` urls) ###
|
||||
# url(r'^lib/(?P<repo_id>[-0-9a-f]{36})/dir/(?P<path>.*)$', view_lib_dir, name='view_lib_dir'),
|
||||
url(r'^lib/(?P<repo_id>[-0-9a-f]{36})/file(?P<path>.*)$', view_lib_file, name='view_lib_file'),
|
||||
url(r'^wiki/lib/(?P<repo_id>[-0-9a-f]{36})/(?P<path>.*)$', view_lib_as_wiki, name='view_lib_as_wiki'),
|
||||
url(r'^smart-link/(?P<dirent_uuid>[-0-9a-f]{36})/(?P<dirent_name>.*)$', view_lib_file_via_smart_link, name="view_lib_file_via_smart_link"),
|
||||
url(r'^#common/lib/(?P<repo_id>[-0-9a-f]{36})/(?P<path>.*)$', fake_view, name='view_common_lib_dir'),
|
||||
url(r'^#group/(?P<group_id>\d+)/$', fake_view, name='group_info'),
|
||||
|
@ -24,7 +24,7 @@ from seahub.views import gen_path_link, get_repo_dirents, \
|
||||
|
||||
from seahub.utils import gen_dir_share_link, \
|
||||
gen_shared_upload_link, user_traffic_over_limit, render_error, \
|
||||
get_file_type_and_ext
|
||||
get_file_type_and_ext, get_service_url
|
||||
from seahub.settings import ENABLE_UPLOAD_FOLDER, \
|
||||
ENABLE_RESUMABLE_FILEUPLOAD, ENABLE_THUMBNAIL, \
|
||||
THUMBNAIL_ROOT, THUMBNAIL_DEFAULT_SIZE, THUMBNAIL_SIZE_FOR_GRID, \
|
||||
@ -152,6 +152,25 @@ def repo_history_view(request, repo_id):
|
||||
'referer': referer,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def view_lib_as_wiki(request, repo_id, path):
|
||||
|
||||
if not path.startswith('/'):
|
||||
path = '/' + path
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
|
||||
user_perm = check_folder_permission(request, repo.id, '/')
|
||||
if user_perm is None:
|
||||
return render_error(request, _(u'Permission denied'))
|
||||
|
||||
return render(request, 'view_lib_as_wiki.html', {
|
||||
'repo_id': repo_id,
|
||||
'service_url': get_service_url().rstrip('/'),
|
||||
'file_path': path,
|
||||
'repo_name': repo.name
|
||||
})
|
||||
|
||||
########## shared dir/uploadlink
|
||||
@share_link_audit
|
||||
def view_shared_dir(request, fileshare):
|
||||
|
Loading…
Reference in New Issue
Block a user