mirror of
https://github.com/haiwen/seahub.git
synced 2025-04-27 19:05:16 +00:00
12.0 change add existing file in wiki edit (#6057)
* 12.0 change add existing file in wiki edit * 01 delete create wiki from existing library * 02 change click wiki name jump to edit page and delete edit icon * 03 delete select existing file to create new page * optimize edit wiki * 04 old wiki page use the early version 11.x features * optimize wiki permission * wiki add wiki2 * delete page file * fix wiki test --------- Co-authored-by: ‘JoinTyang’ <yangtong1009@163.com>
This commit is contained in:
parent
a4fa7f899a
commit
d7ac5688ef
@ -6,6 +6,7 @@ const entryFiles = {
|
||||
TCAccept: '/tc-accept.js',
|
||||
TCView: '/tc-view.js',
|
||||
wiki: '/wiki.js',
|
||||
wiki2: '/wiki2.js',
|
||||
fileHistory: '/file-history.js',
|
||||
fileHistoryOld: '/file-history-old.js',
|
||||
sdocFileHistory: '/pages/sdoc/sdoc-file-history/index.js',
|
||||
|
@ -1,34 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input } from 'reactstrap';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Label } from 'reactstrap';
|
||||
|
||||
const propTypes = {
|
||||
toggleCancel: PropTypes.func.isRequired,
|
||||
addWiki: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class NewWikiDialog extends React.Component {
|
||||
class AddWikiDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isExist: false,
|
||||
name: '',
|
||||
repoID: '',
|
||||
isSubmitBtnActive: false,
|
||||
};
|
||||
}
|
||||
|
||||
inputNewName = (e) => {
|
||||
if (!event.target.value.trim()) {
|
||||
this.setState({isSubmitBtnActive: false});
|
||||
} else {
|
||||
this.setState({isSubmitBtnActive: true});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
name: e.target.value,
|
||||
isSubmitBtnActive: !!e.target.value.trim(),
|
||||
});
|
||||
};
|
||||
|
||||
@ -39,8 +32,9 @@ class NewWikiDialog extends React.Component {
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
let { isExist, name, repoID } = this.state;
|
||||
this.props.addWiki(isExist, name, repoID);
|
||||
const wikiName = this.state.name.trim();
|
||||
if (!wikiName) return;
|
||||
this.props.addWiki(wikiName);
|
||||
this.props.toggleCancel();
|
||||
};
|
||||
|
||||
@ -51,9 +45,9 @@ class NewWikiDialog extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} autoFocus={false} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('New Wiki')}</ModalHeader>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('Add Wiki')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<label className="form-label">{gettext('Name')}</label>
|
||||
<Label>{gettext('Name')}</Label>
|
||||
<Input onKeyDown={this.handleKeyDown} autoFocus={true} value={this.state.name} onChange={this.inputNewName}/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
@ -65,6 +59,6 @@ class NewWikiDialog extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
NewWikiDialog.propTypes = propTypes;
|
||||
AddWikiDialog.propTypes = propTypes;
|
||||
|
||||
export default NewWikiDialog;
|
||||
export default AddWikiDialog;
|
@ -1,110 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import moment from 'moment';
|
||||
import Repo from '../../models/repo';
|
||||
import { Utils } from '../../utils/utils';
|
||||
|
||||
const propTypes = {
|
||||
toggleCancel: PropTypes.func.isRequired,
|
||||
addWiki: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class WikiSelectDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
repos: [],
|
||||
repoID: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.listRepos().then(res => {
|
||||
let repoList = res.data.repos
|
||||
.filter(item => {
|
||||
switch (item.type) {
|
||||
case 'mine': // my libraries
|
||||
return !item.encrypted;
|
||||
case 'shared': // libraries shared with me
|
||||
// 'is_admin': the library is shared with 'admin' permission
|
||||
return !item.encrypted && item.is_admin;
|
||||
case 'group':
|
||||
default:
|
||||
return !item.encrypted && !res.data.repos.some(repo => {
|
||||
// just remove the duplicated libraries
|
||||
return repo.type != item.type && repo.repo_id == item.repo_id;
|
||||
});
|
||||
}
|
||||
})
|
||||
.map(item => {
|
||||
let repo = new Repo(item);
|
||||
return repo;
|
||||
});
|
||||
repoList = Utils.sortRepos(repoList, 'name', 'asc');
|
||||
this.setState({repos: repoList});
|
||||
});
|
||||
}
|
||||
|
||||
onChange = (repo) => {
|
||||
this.setState({
|
||||
repoID: repo.repo_id,
|
||||
});
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
let { repoID } = this.state;
|
||||
this.props.addWiki(repoID);
|
||||
this.props.toggleCancel();
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.props.toggleCancel();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('Create Wiki from existing library')}</ModalHeader>
|
||||
<ModalBody className="dialog-list-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width='6%'>{/* select */}</th>
|
||||
<th width='9%'>{/* icon */}</th>
|
||||
<th width='55%'>{gettext('Name')}</th>
|
||||
<th width='30%'>{gettext('Last Update')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.repos.map((repo, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="text-center"><input type="radio" className="vam" name="repo" value={repo.repo_id} onChange={this.onChange.bind(this, repo)} /></td>
|
||||
<td className="text-center"><img src={Utils.getLibIconUrl(repo, false)} width="24" title={Utils.getLibIconTitle(repo)} alt={Utils.getLibIconTitle(repo)} /></td>
|
||||
<td>{repo.repo_name}</td>
|
||||
<td>{moment(repo.last_modified).fromNow()}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||
{this.state.repoID ?
|
||||
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>:
|
||||
<Button color="primary" disabled>{gettext('Submit')}</Button>
|
||||
}
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WikiSelectDialog.propTypes = propTypes;
|
||||
|
||||
export default WikiSelectDialog;
|
@ -2,17 +2,13 @@ import React, { Component } from 'react';
|
||||
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { siteRoot, gettext, username } from '../../utils/constants';
|
||||
import { siteRoot, gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
// import { seafileAPI } from '../../utils/seafile-api';
|
||||
// import Toast from '../toast';
|
||||
import ModalPortal from '../modal-portal';
|
||||
import WikiDeleteDialog from '../dialog/wiki-delete-dialog';
|
||||
// import Rename from '../rename';
|
||||
|
||||
const propTypes = {
|
||||
wiki: PropTypes.object.isRequired,
|
||||
// renameWiki: PropTypes.func.isRequired,
|
||||
deleteWiki: PropTypes.func.isRequired,
|
||||
isItemFreezed: PropTypes.bool.isRequired,
|
||||
onFreezedItem: PropTypes.func.isRequired,
|
||||
@ -25,9 +21,7 @@ class WikiListItem extends Component {
|
||||
this.state = {
|
||||
isOpMenuOpen: false, // for mobile
|
||||
isShowDeleteDialog: false,
|
||||
// isRenameing: false,
|
||||
highlight: false,
|
||||
// permission: this.props.wiki.permission,
|
||||
};
|
||||
}
|
||||
|
||||
@ -37,30 +31,6 @@ class WikiListItem extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
// clickMenuToggle = (e) => {
|
||||
// e.preventDefault();
|
||||
// this.onMenuToggle(e);
|
||||
// }
|
||||
|
||||
// onMenuToggle = (e) => {
|
||||
// let targetType = e.target.dataset.toggle;
|
||||
// if (targetType !== 'item') {
|
||||
// if (this.props.isItemFreezed) {
|
||||
// this.setState({
|
||||
// highlight: false,
|
||||
// isShowMenuControl: false,
|
||||
// isShowWikiMenu: !this.state.isShowWikiMenu
|
||||
// });
|
||||
// this.props.onUnfreezedItem();
|
||||
// } else {
|
||||
// this.setState({
|
||||
// isShowWikiMenu: !this.state.isShowWikiMenu
|
||||
// });
|
||||
// this.props.onFreezedItem();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
onMouseEnter = () => {
|
||||
if (!this.props.isItemFreezed) {
|
||||
this.setState({ highlight: true });
|
||||
@ -73,37 +43,6 @@ class WikiListItem extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
// changePerm = (permission) => {
|
||||
// let wiki = this.props.wiki;
|
||||
// seafileAPI.updateWikiPermission(wiki.slug, permission).then(() => {
|
||||
// this.setState({permission: permission});
|
||||
// }).catch((error) => {
|
||||
// if(error.response) {
|
||||
// let errorMsg = error.response.data.error_msg;
|
||||
// Toast.danger(errorMsg);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// onRenameToggle = (e) => {
|
||||
// this.props.onFreezedItem();
|
||||
// this.setState({
|
||||
// isShowWikiMenu: false,
|
||||
// isShowMenuControl: false,
|
||||
// isRenameing: true,
|
||||
// });
|
||||
// }
|
||||
|
||||
// onRenameConfirm = (newName) => {
|
||||
// this.renameWiki(newName);
|
||||
// this.onRenameCancel();
|
||||
// }
|
||||
|
||||
// onRenameCancel = () => {
|
||||
// this.props.onUnfreezedItem();
|
||||
// this.setState({isRenameing: false});
|
||||
// }
|
||||
|
||||
onDeleteToggle = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.onUnfreezedItem();
|
||||
@ -119,11 +58,6 @@ class WikiListItem extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
// renameWiki = (newName) => {
|
||||
// let wiki = this.props.wiki;
|
||||
// this.props.renameWiki(wiki, newName);
|
||||
// }
|
||||
|
||||
deleteWiki = () => {
|
||||
let wiki = this.props.wiki;
|
||||
this.props.deleteWiki(wiki);
|
||||
@ -136,7 +70,9 @@ class WikiListItem extends Component {
|
||||
let wiki = this.props.wiki;
|
||||
let userProfileURL = `${siteRoot}profile/${encodeURIComponent(wiki.owner)}/`;
|
||||
let fileIconUrl = Utils.getDefaultLibIconUrl(false);
|
||||
const isWikiOwner = wiki.owner === username;
|
||||
let isOldVersion = wiki.version !== 'v2';
|
||||
let publishedUrl = `${siteRoot}published/${encodeURIComponent(wiki.slug)}/`;
|
||||
let editUrl = `${siteRoot}edit-wiki/${wiki.id}/`;
|
||||
|
||||
const desktopItem = (
|
||||
<tr
|
||||
@ -147,24 +83,12 @@ class WikiListItem extends Component {
|
||||
>
|
||||
<td><img src={fileIconUrl} width="24" alt="" /></td>
|
||||
<td className="name">
|
||||
<a href={wiki.link}>{wiki.name}</a>
|
||||
{/*this.state.isRenameing ?
|
||||
<Rename wiki={wiki} name={wiki.name} onRenameConfirm={this.onRenameConfirm} onRenameCancel={this.onRenameCancel}/> :
|
||||
<a href={wiki.link}>{wiki.name}</a>
|
||||
*/}
|
||||
{isOldVersion && <a href={publishedUrl}>{wiki.name} (old version)</a>}
|
||||
{!isOldVersion && <a href={editUrl}>{wiki.name}</a>}
|
||||
</td>
|
||||
<td><a href={userProfileURL} target='_blank' rel="noreferrer">{wiki.owner_nickname}</a></td>
|
||||
<td>{moment(wiki.updated_at).fromNow()}</td>
|
||||
<td className="text-center cursor-pointer align-top">
|
||||
{isWikiOwner &&
|
||||
<span
|
||||
className={`iconfont icon-edit mr-4 action-icon ${this.state.highlight ? '' : 'invisible'}`}
|
||||
onClick={() => window.open(wiki.link.replace('/published/', '/edit-wiki/'))}
|
||||
title={gettext('Edit')}
|
||||
aria-label={gettext('Edit')}
|
||||
style={{color: '#999', fontSize: '20px'}}
|
||||
></span>
|
||||
}
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@ -181,7 +105,8 @@ class WikiListItem extends Component {
|
||||
<tr>
|
||||
<td><img src={fileIconUrl} width="24" alt="" /></td>
|
||||
<td>
|
||||
<a href={wiki.link}>{wiki.name}</a><br />
|
||||
{isOldVersion && <a href={publishedUrl}>{wiki.name} (old version)</a>}
|
||||
{!isOldVersion && <a href={editUrl}>{wiki.name}</a>}<br />
|
||||
<a href={userProfileURL} target='_blank' className="item-meta-info" rel="noreferrer">{wiki.owner_nickname}</a>
|
||||
<span className="item-meta-info">{moment(wiki.updated_at).fromNow()}</span>
|
||||
</td>
|
||||
|
@ -7,7 +7,6 @@ import LibsMobileThead from '../libs-mobile-thead';
|
||||
|
||||
const propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
renameWiki: PropTypes.func.isRequired,
|
||||
deleteWiki: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@ -57,7 +56,6 @@ class WikiListView extends Component {
|
||||
<WikiListItem
|
||||
key={index}
|
||||
wiki={wiki}
|
||||
renameWiki={this.props.renameWiki}
|
||||
deleteWiki={this.props.deleteWiki}
|
||||
isItemFreezed={this.state.isItemFreezed}
|
||||
onFreezedItem={this.onFreezedItem}
|
||||
|
@ -1,90 +0,0 @@
|
||||
.add-page-dialog {
|
||||
width: 506px;
|
||||
max-width: 506px;
|
||||
}
|
||||
|
||||
.add-page-dialog .modal-content {
|
||||
height: 100%;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.add-page-dialog .modal-content .modal-body {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-pages {
|
||||
margin: -8px 0;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-pages .app-select-page-item {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
width: 88px;
|
||||
flex-shrink: 0;
|
||||
margin: 8px 8px 8px 0px;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-pages .app-select-page-item:nth-child(5n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-pages .app-select-page-item:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item .app-select-page-item-image-container {
|
||||
height: 64px;
|
||||
width: 100%;
|
||||
border: 1px solid #e9e9e9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item .app-select-page-item-image-container .app-select-page-item-image {
|
||||
width: 86px;
|
||||
height: 62px;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item.selected .app-select-page-item-image-container,
|
||||
.add-page-dialog .app-select-page-item.selected .app-select-page-item-image-container:hover {
|
||||
border-color: #ff8000;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item .app-select-page-item-image-container:hover {
|
||||
border-color: #bdbdbd;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item .app-select-page-item-name {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item.selected .app-select-page-item-name {
|
||||
color: #ff8000;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover {
|
||||
width: 204px;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover .app-select-page-item-popover-container {
|
||||
padding-left: 13px;
|
||||
padding-right: 13px;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover .app-select-page-item-popover-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover .app-select-page-item-popover-tip {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover .app-select-page-item-popover-image-container {
|
||||
border: 2px solid #ff8000;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
@ -3,17 +3,13 @@ import moment from 'moment';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { Modal } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import wikiAPI from '../../utils/wiki-api';
|
||||
import { slug, siteRoot, initialPath, isDir, sharedToken, hasIndex, lang, isEditWiki } from '../../utils/constants';
|
||||
import { wikiId, slug, siteRoot, initialPath, isDir, sharedToken, hasIndex, lang } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import Dirent from '../../models/dirent';
|
||||
import WikiConfig from './models/wiki-config';
|
||||
import TreeNode from '../../components/tree-view/tree-node';
|
||||
import treeHelper from '../../components/tree-view/tree-helper';
|
||||
import toaster from '../../components/toast';
|
||||
import SidePanel from './side-panel';
|
||||
import MainPanel from './main-panel';
|
||||
import WikiLeftBar from './wiki-left-bar/wiki-left-bar';
|
||||
import PageUtils from './view-structure/page-utils';
|
||||
|
||||
import '../../css/layout.css';
|
||||
import '../../css/side-panel.css';
|
||||
@ -38,14 +34,10 @@ class Wiki extends Component {
|
||||
lastModified: '',
|
||||
latestContributor: '',
|
||||
isTreeDataLoading: true,
|
||||
isConfigLoading: true,
|
||||
treeData: treeHelper.buildTree(),
|
||||
currentNode: null,
|
||||
indexNode: null,
|
||||
indexContent: '',
|
||||
currentPageId: '',
|
||||
config: {},
|
||||
repoId: '',
|
||||
};
|
||||
|
||||
window.onpopstate = this.onpopstate;
|
||||
@ -61,7 +53,6 @@ class Wiki extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getWikiConfig();
|
||||
this.loadSidePanel(initialPath);
|
||||
this.loadWikiData(initialPath);
|
||||
|
||||
@ -73,40 +64,6 @@ class Wiki extends Component {
|
||||
this.links.forEach(link => link.removeEventListener('click', this.onConentLinkClick));
|
||||
}
|
||||
|
||||
handlePath = () => {
|
||||
return isEditWiki ? 'edit-wiki/' : 'published/';
|
||||
};
|
||||
|
||||
getWikiConfig = () => {
|
||||
wikiAPI.getWikiConfig(slug).then(res => {
|
||||
const { wiki_config, repo_id } = res.data.wiki;
|
||||
this.setState({
|
||||
config: new WikiConfig(JSON.parse(wiki_config) || {}),
|
||||
isConfigLoading: false,
|
||||
repoId: repo_id,
|
||||
});
|
||||
}).catch((error) => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
this.setState({
|
||||
isConfigLoading: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
saveWikiConfig = (wikiConfig, onSuccess, onError) => {
|
||||
wikiAPI.updateWikiConfig(slug, JSON.stringify(wikiConfig)).then(res => {
|
||||
this.setState({
|
||||
config: new WikiConfig(wikiConfig || {}),
|
||||
});
|
||||
onSuccess && onSuccess();
|
||||
}).catch((error) => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
onError && onError();
|
||||
});
|
||||
};
|
||||
|
||||
loadSidePanel = (initialPath) => {
|
||||
if (hasIndex) {
|
||||
this.loadIndexNode();
|
||||
@ -146,17 +103,17 @@ class Wiki extends Component {
|
||||
|
||||
if (isDir === 'None') {
|
||||
this.setState({pathExist: false});
|
||||
let fileUrl = siteRoot + this.handlePath() + slug + Utils.encodePath(initialPath);
|
||||
let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(initialPath);
|
||||
window.history.pushState({url: fileUrl, path: initialPath}, initialPath, fileUrl);
|
||||
}
|
||||
};
|
||||
|
||||
loadIndexNode = () => {
|
||||
wikiAPI.listWikiDir(slug, '/').then(res => {
|
||||
seafileAPI.listWikiDir(wikiId, '/').then(res => {
|
||||
let tree = this.state.treeData;
|
||||
this.addFirstResponseListToNode(res.data.dirent_list, tree.root);
|
||||
let indexNode = tree.getNodeByPath(this.indexPath);
|
||||
wikiAPI.getWikiFileContent(slug, indexNode.path).then(res => {
|
||||
seafileAPI.getWikiFileContent(wikiId, indexNode.path).then(res => {
|
||||
this.setState({
|
||||
treeData: tree,
|
||||
indexNode: indexNode,
|
||||
@ -164,7 +121,7 @@ class Wiki extends Component {
|
||||
isTreeDataLoading: false,
|
||||
});
|
||||
});
|
||||
}).catch(() => {
|
||||
}).catch((error) => {
|
||||
this.setState({isLoadFailed: true});
|
||||
});
|
||||
};
|
||||
@ -174,7 +131,7 @@ class Wiki extends Component {
|
||||
this.loadDirentList(dirPath);
|
||||
|
||||
// update location url
|
||||
let fileUrl = siteRoot + this.handlePath() + slug + Utils.encodePath(dirPath);
|
||||
let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(dirPath);
|
||||
window.history.pushState({url: fileUrl, path: dirPath}, dirPath, fileUrl);
|
||||
};
|
||||
|
||||
@ -186,7 +143,7 @@ class Wiki extends Component {
|
||||
});
|
||||
|
||||
this.removePythonWrapper();
|
||||
wikiAPI.getWikiFileContent(slug, filePath).then(res => {
|
||||
seafileAPI.getWikiFileContent(wikiId, filePath).then(res => {
|
||||
let data = res.data;
|
||||
this.setState({
|
||||
isDataLoading: false,
|
||||
@ -195,13 +152,10 @@ class Wiki extends Component {
|
||||
lastModified: moment.unix(data.last_modified).fromNow(),
|
||||
latestContributor: data.latest_contributor,
|
||||
});
|
||||
}).catch(error => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
|
||||
const hash = window.location.hash;
|
||||
let fileUrl = `${siteRoot}${this.handlePath()}${slug}${Utils.encodePath(filePath)}${hash}`;
|
||||
let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(filePath) + hash;
|
||||
if (filePath === '/home.md') {
|
||||
window.history.replaceState({url: fileUrl, path: filePath}, filePath, fileUrl);
|
||||
} else {
|
||||
@ -211,7 +165,7 @@ class Wiki extends Component {
|
||||
|
||||
loadDirentList = (dirPath) => {
|
||||
this.setState({isDataLoading: true});
|
||||
wikiAPI.listWikiDir(slug, dirPath).then(res => {
|
||||
seafileAPI.listWikiDir(wikiId, dirPath).then(res => {
|
||||
let direntList = res.data.dirent_list.map(item => {
|
||||
let dirent = new Dirent(item);
|
||||
return dirent;
|
||||
@ -241,7 +195,7 @@ class Wiki extends Component {
|
||||
let tree = this.state.treeData.clone();
|
||||
let node = tree.getNodeByPath(path);
|
||||
if (!node.isLoaded) {
|
||||
wikiAPI.listWikiDir(slug, node.path).then(res => {
|
||||
seafileAPI.listWikiDir(wikiId, node.path).then(res => {
|
||||
this.addResponseListToNode(res.data.dirent_list, node);
|
||||
let parentNode = tree.getNodeByPath(node.parentNode.path);
|
||||
parentNode.isExpanded = true;
|
||||
@ -262,7 +216,7 @@ class Wiki extends Component {
|
||||
if (Utils.isMarkdownFile(path)) {
|
||||
path = Utils.getDirName(path);
|
||||
}
|
||||
wikiAPI.listWikiDir(slug, path, true).then(res => {
|
||||
seafileAPI.listWikiDir(wikiId, path, true).then(res => {
|
||||
let direntList = res.data.dirent_list;
|
||||
let results = {};
|
||||
for (let i = 0; i < direntList.length; i++) {
|
||||
@ -425,7 +379,7 @@ class Wiki extends Component {
|
||||
if (!node.isLoaded) {
|
||||
let tree = this.state.treeData.clone();
|
||||
node = tree.getNodeByPath(node.path);
|
||||
wikiAPI.listWikiDir(slug, node.path).then(res => {
|
||||
seafileAPI.listWikiDir(wikiId, node.path).then(res => {
|
||||
this.addResponseListToNode(res.data.dirent_list, node);
|
||||
tree.collapseNode(node);
|
||||
this.setState({treeData: tree});
|
||||
@ -473,7 +427,7 @@ class Wiki extends Component {
|
||||
let tree = this.state.treeData.clone();
|
||||
node = tree.getNodeByPath(node.path);
|
||||
if (!node.isLoaded) {
|
||||
wikiAPI.listWikiDir(slug, node.path).then(res => {
|
||||
seafileAPI.listWikiDir(wikiId, node.path).then(res => {
|
||||
this.addResponseListToNode(res.data.dirent_list, node);
|
||||
this.setState({treeData: tree});
|
||||
});
|
||||
@ -518,45 +472,11 @@ class Wiki extends Component {
|
||||
node.addChildren(nodeList);
|
||||
};
|
||||
|
||||
setCurrentPage = (pageId, callback) => {
|
||||
const { currentPageId, config } = this.state;
|
||||
if (pageId === currentPageId) {
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
const { pages } = config;
|
||||
const currentPage = PageUtils.getPageById(pages, pageId);
|
||||
const path = currentPage.path;
|
||||
if (Utils.isMarkdownFile(path) || Utils.isSdocFile(path)) {
|
||||
if (path !== this.state.path) {
|
||||
this.showFile(path);
|
||||
}
|
||||
this.onCloseSide();
|
||||
} else {
|
||||
const w = window.open('about:blank');
|
||||
const url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(path);
|
||||
w.location.href = url;
|
||||
}
|
||||
this.setState({
|
||||
currentPageId: pageId,
|
||||
path: path,
|
||||
}, () => {
|
||||
callback && callback();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="main" className="wiki-main">
|
||||
{isEditWiki &&
|
||||
<WikiLeftBar
|
||||
config={this.state.config}
|
||||
repoId={this.state.repoId}
|
||||
updateConfig={(data) => this.saveWikiConfig(Object.assign({}, this.state.config, data))}
|
||||
/>
|
||||
}
|
||||
<SidePanel
|
||||
isLoading={this.state.isTreeDataLoading || this.state.isConfigLoading}
|
||||
isTreeDataLoading={this.state.isTreeDataLoading}
|
||||
closeSideBar={this.state.closeSideBar}
|
||||
currentPath={this.state.path}
|
||||
treeData={this.state.treeData}
|
||||
@ -567,10 +487,6 @@ class Wiki extends Component {
|
||||
onNodeCollapse={this.onNodeCollapse}
|
||||
onNodeExpanded={this.onNodeExpanded}
|
||||
onLinkClick={this.onLinkClick}
|
||||
config={this.state.config}
|
||||
saveWikiConfig={this.saveWikiConfig}
|
||||
setCurrentPage={this.setCurrentPage}
|
||||
currentPageId={this.state.currentPageId}
|
||||
/>
|
||||
<MainPanel
|
||||
path={this.state.path}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext, repoID, siteRoot, username, isPro, isEditWiki } from '../../utils/constants';
|
||||
import { gettext, repoID, siteRoot, username, isPro } from '../../utils/constants';
|
||||
import SeafileMarkdownViewer from '../../components/seafile-markdown-viewer';
|
||||
import WikiDirListView from '../../components/wiki-dir-list-view/wiki-dir-list-view';
|
||||
import Loading from '../../components/loading';
|
||||
@ -82,7 +82,7 @@ class MainPanel extends Component {
|
||||
const errMessage = (<div className="message err-tip">{gettext('Folder does not exist.')}</div>);
|
||||
const isViewingFile = this.props.pathExist && !this.props.isDataLoading && this.props.isViewFile;
|
||||
return (
|
||||
<div className="main-panel wiki-main-panel" style={{flex: isEditWiki ? '1 0 76%' : '1 0 80%'}}>
|
||||
<div className="main-panel wiki-main-panel">
|
||||
<div className="main-panel-hide hide">{this.props.content}</div>
|
||||
<div className={`main-panel-north panel-top ${this.props.permission === 'rw' ? 'border-left-show' : ''}`}>
|
||||
{!username &&
|
||||
|
@ -1,26 +1,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import deepCopy from 'deep-copy';
|
||||
import { gettext, siteRoot, repoID, slug, username, permission, isEditWiki } from '../../utils/constants';
|
||||
import toaster from '../../components/toast';
|
||||
import { gettext, siteRoot, repoID, slug, username, permission } from '../../utils/constants';
|
||||
import Logo from '../../components/logo';
|
||||
import Loading from '../../components/loading';
|
||||
// import TreeView from '../../components/tree-view/tree-view';
|
||||
import ViewStructure from './view-structure';
|
||||
import TreeView from '../../components/tree-view/tree-view';
|
||||
import IndexMdViewer from './index-md-viewer';
|
||||
import PageUtils from './view-structure/page-utils';
|
||||
import NewFolderDialog from './view-structure/new-folder-dialog';
|
||||
import AddPageDialog from './view-structure/add-page-dialog';
|
||||
import ViewStructureFooter from './view-structure/view-structure-footer';
|
||||
import { generateUniqueId, getIconURL, isObjectNotEmpty } from './utils';
|
||||
import Folder from './models/folder';
|
||||
import Page from './models/page';
|
||||
|
||||
export const FOLDER = 'folder';
|
||||
export const PAGE = 'page';
|
||||
|
||||
const propTypes = {
|
||||
closeSideBar: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
isTreeDataLoading: PropTypes.bool.isRequired,
|
||||
treeData: PropTypes.object.isRequired,
|
||||
indexNode: PropTypes.object,
|
||||
indexContent: PropTypes.string.isRequired,
|
||||
@ -30,10 +18,6 @@ const propTypes = {
|
||||
onNodeCollapse: PropTypes.func.isRequired,
|
||||
onNodeExpanded: PropTypes.func.isRequired,
|
||||
onLinkClick: PropTypes.func.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
saveWikiConfig: PropTypes.func.isRequired,
|
||||
setCurrentPage: PropTypes.func.isRequired,
|
||||
currentPageId: PropTypes.string,
|
||||
};
|
||||
|
||||
class SidePanel extends Component {
|
||||
@ -41,10 +25,6 @@ class SidePanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.isNodeMenuShow = false;
|
||||
this.state = {
|
||||
isShowNewFolderDialog: false,
|
||||
isShowAddPageDialog: false,
|
||||
};
|
||||
}
|
||||
|
||||
renderIndexView = () => {
|
||||
@ -62,7 +42,7 @@ class SidePanel extends Component {
|
||||
renderTreeView = () => {
|
||||
return (
|
||||
<div className="wiki-pages-container">
|
||||
{/* {this.props.treeData && (
|
||||
{this.props.treeData && (
|
||||
<TreeView
|
||||
treeData={this.props.treeData}
|
||||
currentPath={this.props.currentPath}
|
||||
@ -71,336 +51,22 @@ class SidePanel extends Component {
|
||||
onNodeCollapse={this.props.onNodeCollapse}
|
||||
onNodeExpanded={this.props.onNodeExpanded}
|
||||
/>
|
||||
)} */}
|
||||
{isEditWiki &&
|
||||
<ViewStructureFooter
|
||||
onToggleAddView={this.openAddPageDialog.bind(this, null)}
|
||||
onToggleAddFolder={this.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowNewFolderDialog &&
|
||||
<NewFolderDialog
|
||||
onAddFolder={this.addPageFolder}
|
||||
onToggleAddFolderDialog={this.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowAddPageDialog &&
|
||||
<AddPageDialog
|
||||
toggle={this.closeAddPageDialog}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
confirmDeletePage = (pageId) => {
|
||||
const config = deepCopy(this.props.config);
|
||||
const { pages, navigation } = config;
|
||||
const index = PageUtils.getPageIndexById(pageId, pages);
|
||||
config.pages.splice(index, 1);
|
||||
PageUtils.deletePage(navigation, pageId);
|
||||
this.props.saveWikiConfig(config);
|
||||
if (config.pages.length > 0) {
|
||||
this.props.setCurrentPage(config.pages[0].id);
|
||||
} else {
|
||||
this.props.setCurrentPage('');
|
||||
}
|
||||
};
|
||||
|
||||
onAddNewPage = async ({name, icon, path, successCallback, errorCallback}) => {
|
||||
const { config } = this.props;
|
||||
const navigation = config.navigation;
|
||||
const pageId = generateUniqueId(navigation);
|
||||
const newPage = new Page({ id: pageId, name, icon, path});
|
||||
this.addPage(newPage, successCallback, errorCallback);
|
||||
};
|
||||
|
||||
duplicatePage = async (fromPageConfig, successCallback, errorCallback) => {
|
||||
const { config } = this.props;
|
||||
const { name, from_page_id } = fromPageConfig;
|
||||
const { navigation, pages } = config;
|
||||
const fromPage = PageUtils.getPageById(pages, from_page_id);
|
||||
const newPageId = generateUniqueId(navigation);
|
||||
let newPageConfig = {
|
||||
...fromPage,
|
||||
id: newPageId,
|
||||
name,
|
||||
};
|
||||
const newPage = new Page({ ...newPageConfig });
|
||||
this.addPage(newPage, successCallback, errorCallback);
|
||||
};
|
||||
|
||||
addPage = (page, successCallback, errorCallback) => {
|
||||
const { config } = this.props;
|
||||
const navigation = config.navigation;
|
||||
const pageId = page.id;
|
||||
config.pages.push(page);
|
||||
PageUtils.addPage(navigation, pageId, this.current_folder_id);
|
||||
config.navigation = navigation;
|
||||
const onSuccess = () => {
|
||||
this.props.setCurrentPage(pageId, successCallback);
|
||||
successCallback();
|
||||
};
|
||||
this.props.saveWikiConfig(config, onSuccess, errorCallback);
|
||||
};
|
||||
|
||||
onUpdatePage = (pageId, newPage) => {
|
||||
if (newPage.name === '') {
|
||||
toaster.danger(gettext('Page name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
const { config } = this.props;
|
||||
let pages = config.pages;
|
||||
let currentPage = pages.find(page => page.id === pageId);
|
||||
Object.assign(currentPage, newPage);
|
||||
config.pages = pages;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
movePage = ({ moved_view_id, target_view_id, source_view_folder_id, target_view_folder_id, move_position }) => {
|
||||
let config = deepCopy(this.props.config);
|
||||
let { navigation } = config;
|
||||
PageUtils.movePage(navigation, moved_view_id, target_view_id, source_view_folder_id, target_view_folder_id, move_position);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
movePageOut = (moved_page_id, source_folder_id, target_folder_id, move_position) => {
|
||||
let config = deepCopy(this.props.config);
|
||||
let { navigation } = config;
|
||||
PageUtils.movePageOut(navigation, moved_page_id, source_folder_id, target_folder_id, move_position);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
// Create a new folder in the root directory (not supported to create a new subfolder in the folder)
|
||||
addPageFolder = (folder_data, parent_folder_id) => {
|
||||
const { config } = this.props;
|
||||
const { navigation } = config;
|
||||
let folder_id = generateUniqueId(navigation);
|
||||
let newFolder = new Folder({ id: folder_id, ...folder_data });
|
||||
// No parent folder, directly add to the root directory
|
||||
if (!parent_folder_id) {
|
||||
config.navigation.push(newFolder);
|
||||
} else { // Recursively find the parent folder and add
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._addFolder(item, newFolder, parent_folder_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
_addFolder(folder, newFolder, parent_folder_id) {
|
||||
if (folder.id === parent_folder_id) {
|
||||
folder.children.push(newFolder);
|
||||
return;
|
||||
}
|
||||
folder.children.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._addFolder(item, newFolder, parent_folder_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onModifyFolder = (folder_id, folder_data) => {
|
||||
const { config } = this.props;
|
||||
const { navigation } = config;
|
||||
PageUtils.modifyFolder(navigation, folder_id, folder_data);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
onDeleteFolder = (page_folder_id) => {
|
||||
const { config } = this.props;
|
||||
const { navigation, pages } = config;
|
||||
PageUtils.deleteFolder(navigation, pages, page_folder_id);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
// Drag a folder to the front and back of another folder
|
||||
onMoveFolder = (moved_folder_id, target_folder_id, move_position) => {
|
||||
const { config } = this.props;
|
||||
const { navigation } = config;
|
||||
let updatedNavigation = deepCopy(navigation);
|
||||
|
||||
// Get the moved folder first and delete the original location
|
||||
let moved_folder;
|
||||
let moved_folder_index = PageUtils.getFolderIndexById(updatedNavigation, moved_folder_id);
|
||||
if (moved_folder_index === -1) {
|
||||
updatedNavigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
moved_folder_index = PageUtils.getFolderIndexById(item.children, moved_folder_id);
|
||||
if (moved_folder_index > -1) {
|
||||
moved_folder = item.children[moved_folder_index];
|
||||
item.children.splice(moved_folder_index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
moved_folder = updatedNavigation[moved_folder_index];
|
||||
updatedNavigation.splice(moved_folder_index, 1);
|
||||
}
|
||||
let indexOffset = 0;
|
||||
if (move_position === 'move_below') {
|
||||
indexOffset++;
|
||||
}
|
||||
// Get the location of the release
|
||||
let target_folder_index = PageUtils.getFolderIndexById(updatedNavigation, target_folder_id);
|
||||
if (target_folder_index === -1) {
|
||||
updatedNavigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
target_folder_index = PageUtils.getFolderIndexById(item.children, target_folder_id);
|
||||
if (target_folder_index > -1) {
|
||||
item.children.splice(target_folder_index + indexOffset, 0, moved_folder);
|
||||
}
|
||||
} else {
|
||||
// not changed
|
||||
updatedNavigation = navigation;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updatedNavigation.splice(target_folder_index + indexOffset, 0, moved_folder);
|
||||
}
|
||||
config.navigation = updatedNavigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
// Not support yet: Move a folder into another folder
|
||||
moveFolderToFolder = (moved_folder_id, target_folder_id) => {
|
||||
let { config } = this.props;
|
||||
let { navigation } = config;
|
||||
|
||||
// Find the folder and move it to this new folder
|
||||
let target_folder = PageUtils.getFolderById(navigation, target_folder_id);
|
||||
if (!target_folder) {
|
||||
toaster.danger('Only_support_two_level_folders');
|
||||
return;
|
||||
}
|
||||
|
||||
let moved_folder;
|
||||
let moved_folder_index = PageUtils.getFolderIndexById(navigation, moved_folder_id);
|
||||
|
||||
// The original directory is in the root directory
|
||||
if (moved_folder_index > -1) {
|
||||
moved_folder = PageUtils.getFolderById(navigation, moved_folder_id);
|
||||
// If moved folder There are other directories under the ID, and dragging is not supported
|
||||
if (moved_folder.children.some(item => item.type === FOLDER)) {
|
||||
toaster.danger('Only_support_two_level_folders');
|
||||
return;
|
||||
}
|
||||
target_folder.children.push(moved_folder);
|
||||
navigation.splice(moved_folder_index, 1);
|
||||
} else { // The original directory is not in the root directory
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
let moved_folder_index = PageUtils.getFolderIndexById(item.children, moved_folder_id);
|
||||
if (moved_folder_index > -1) {
|
||||
moved_folder = item.children[moved_folder_index];
|
||||
target_folder.children.push(moved_folder);
|
||||
item.children.splice(moved_folder_index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
onToggleAddFolder = () => {
|
||||
this.setState({ isShowNewFolderDialog: !this.state.isShowNewFolderDialog });
|
||||
};
|
||||
|
||||
openAddPageDialog = (folder_id) => {
|
||||
this.current_folder_id = folder_id;
|
||||
this.setState({ isShowAddPageDialog: true });
|
||||
};
|
||||
|
||||
closeAddPageDialog = () => {
|
||||
this.current_folder_id = null;
|
||||
this.setState({ isShowAddPageDialog: false });
|
||||
};
|
||||
|
||||
onSetFolderId = (folder_id) => {
|
||||
this.current_folder_id = folder_id;
|
||||
};
|
||||
|
||||
renderFolderView = () => {
|
||||
const { config } = this.props;
|
||||
const { pages, navigation } = config;
|
||||
return (
|
||||
<div className="wiki-pages-container">
|
||||
<ViewStructure
|
||||
isEditMode={isEditWiki}
|
||||
navigation={navigation}
|
||||
views={pages}
|
||||
onToggleAddView={this.openAddPageDialog}
|
||||
onDeleteView={this.confirmDeletePage}
|
||||
onUpdatePage={this.onUpdatePage}
|
||||
onSelectView={this.props.setCurrentPage}
|
||||
onMoveView={this.movePage}
|
||||
movePageOut={this.movePageOut}
|
||||
onToggleAddFolder={this.onToggleAddFolder}
|
||||
onModifyFolder={this.onModifyFolder}
|
||||
onDeleteFolder={this.onDeleteFolder}
|
||||
onMoveFolder={this.onMoveFolder}
|
||||
moveFolderToFolder={this.moveFolderToFolder}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
duplicatePage={this.duplicatePage}
|
||||
onSetFolderId={this.onSetFolderId}
|
||||
currentPageId={this.props.currentPageId}
|
||||
/>
|
||||
{this.state.isShowNewFolderDialog &&
|
||||
<NewFolderDialog
|
||||
onAddFolder={this.addPageFolder}
|
||||
onToggleAddFolderDialog={this.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowAddPageDialog &&
|
||||
<AddPageDialog
|
||||
toggle={this.closeAddPageDialog}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderContent = () => {
|
||||
const { isLoading, indexNode, config } = this.props;
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (indexNode) {
|
||||
return this.renderIndexView();
|
||||
}
|
||||
if (isObjectNotEmpty(config)) {
|
||||
return this.renderFolderView();
|
||||
}
|
||||
return this.renderTreeView();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { wiki_name, wiki_icon } = this.props.config;
|
||||
const src = getIconURL(repoID, wiki_icon);
|
||||
return (
|
||||
<div className={`side-panel wiki-side-panel ${this.props.closeSideBar ? '': 'left-zero'}`}>
|
||||
<div className="side-panel-top panel-top">
|
||||
{src && <img src={src} width="32" height="32" alt='' className='mr-2' />}
|
||||
<h4 className="ml-0 mb-0">{wiki_name || slug}</h4>
|
||||
<Logo onCloseSidePanel={this.props.onCloseSide} />
|
||||
</div>
|
||||
<div id="side-nav" className="wiki-side-nav" role="navigation">
|
||||
{this.renderContent() }
|
||||
{(username && permission) && (
|
||||
<div className="text-left p-2">
|
||||
<a href={siteRoot + 'library/' + repoID + '/' + slug + '/'} className="text-dark text-decoration-underline">
|
||||
{gettext('Go to Library')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{this.props.isTreeDataLoading && <Loading /> }
|
||||
{!this.props.isTreeDataLoading && this.props.indexNode && this.renderIndexView() }
|
||||
{!this.props.isTreeDataLoading && !this.props.indexNode && this.renderTreeView() }
|
||||
{(username && permission) && <div className="text-left p-2"><a href={siteRoot + 'library/' + repoID + '/' + slug + '/'} className="text-dark text-decoration-underline">{gettext('Go to Library')}</a></div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,233 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input, Button } from 'reactstrap';
|
||||
import { gettext, repoID } from '../../../utils/constants';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import toaster from '../../../components/toast';
|
||||
import Loading from '../../../components/loading';
|
||||
import { SeahubSelect } from '../../../components/common/select';
|
||||
import FileChooser from '../../../components/file-chooser/file-chooser';
|
||||
|
||||
import '../css/add-page-dialog.css';
|
||||
|
||||
const propTypes = {
|
||||
toggle: PropTypes.func.isRequired,
|
||||
onAddNewPage: PropTypes.func,
|
||||
};
|
||||
|
||||
const DIALOG_MAX_HEIGHT = window.innerHeight - 56; // Dialog margin is 3.5rem (56px)
|
||||
|
||||
class AddPageDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.options = this.getOptions();
|
||||
this.state = {
|
||||
pageName: '',
|
||||
iconClassName: '',
|
||||
isLoading: false,
|
||||
repo: null,
|
||||
selectedPath: '',
|
||||
errMessage: '',
|
||||
newFileName: '',
|
||||
selectedOption: this.options[0],
|
||||
};
|
||||
}
|
||||
|
||||
getOptions = () => {
|
||||
return (
|
||||
[
|
||||
{
|
||||
value: 'existing',
|
||||
label: gettext('Select an existing file'),
|
||||
},
|
||||
{
|
||||
value: '.md',
|
||||
label: gettext('Create a markdown file'),
|
||||
},
|
||||
{
|
||||
value: '.sdoc',
|
||||
label: gettext('Create a sdoc file'),
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
handleChange = (event) => {
|
||||
let value = event.target.value;
|
||||
if (value === this.state.pageName) {
|
||||
return;
|
||||
}
|
||||
this.setState({ pageName: value });
|
||||
};
|
||||
|
||||
onFileNameChange = (event) => {
|
||||
this.setState({ newFileName: event.target.value });
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.props.toggle();
|
||||
};
|
||||
|
||||
onIconChange = (className) => {
|
||||
this.setState({ iconClassName: className });
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
let {
|
||||
iconClassName,
|
||||
selectedPath,
|
||||
selectedOption,
|
||||
} = this.state;
|
||||
const pageName = this.state.pageName.trim();
|
||||
if (pageName === '') {
|
||||
toaster.danger(gettext('Page name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
if (selectedOption.value === 'existing') {
|
||||
if (selectedPath.endsWith('.sdoc') === false && selectedPath.endsWith('.md') === false) {
|
||||
toaster.danger(gettext('Please select an existing sdoc or markdown file'));
|
||||
return;
|
||||
}
|
||||
this.props.onAddNewPage({
|
||||
name: pageName,
|
||||
icon: iconClassName,
|
||||
path: selectedPath,
|
||||
successCallback: this.onSuccess,
|
||||
errorCallback: this.onError,
|
||||
});
|
||||
this.setState({ isLoading: true });
|
||||
}
|
||||
else {
|
||||
const newFileName = this.state.newFileName.trim();
|
||||
if (newFileName === '') {
|
||||
toaster.danger(gettext('New file name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
if (newFileName.includes('/')) {
|
||||
toaster.danger(gettext('Name cannot contain slash'));
|
||||
return;
|
||||
}
|
||||
if (newFileName.includes('\\')) {
|
||||
toaster.danger(gettext('Name cannot contain backslash'));
|
||||
return;
|
||||
}
|
||||
this.setState({ isLoading: true });
|
||||
seafileAPI.createFile(repoID, `${selectedPath}/${newFileName}${selectedOption.value}`).then(res => {
|
||||
const { obj_name, parent_dir } = res.data;
|
||||
this.props.onAddNewPage({
|
||||
name: pageName,
|
||||
icon: iconClassName,
|
||||
path: parent_dir === '/' ? `/${obj_name}` : `${parent_dir}/${obj_name}`,
|
||||
successCallback: this.onSuccess,
|
||||
errorCallback: this.onError,
|
||||
});
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
this.onError();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSuccess = () => {
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
this.setState({ isLoading: false });
|
||||
};
|
||||
|
||||
onDirentItemClick = (repo, selectedPath) => {
|
||||
this.setState({
|
||||
repo: repo,
|
||||
selectedPath: selectedPath,
|
||||
errMessage: ''
|
||||
});
|
||||
};
|
||||
|
||||
onRepoItemClick = (repo) => {
|
||||
this.setState({
|
||||
repo: repo,
|
||||
selectedPath: '/',
|
||||
errMessage: ''
|
||||
});
|
||||
};
|
||||
|
||||
handleSelectChange = (selectedOption) => {
|
||||
this.setState({
|
||||
selectedOption,
|
||||
selectedPath: '',
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle} autoFocus={false} className="add-page-dialog" style={{ maxHeight: DIALOG_MAX_HEIGHT }}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('Add page')}</ModalHeader>
|
||||
<ModalBody className={'pr-4'}>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Page name')}</Label>
|
||||
<Input value={this.state.pageName} onChange={this.handleChange} autoFocus={true} />
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>{gettext('The file corresponding to this page')}</Label>
|
||||
<SeahubSelect
|
||||
value={this.state.selectedOption}
|
||||
options={this.options}
|
||||
onChange={this.handleSelectChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
{this.state.selectedOption.value !== 'existing' &&
|
||||
<>
|
||||
<FormGroup>
|
||||
<Label>{gettext('New file name')}</Label>
|
||||
<Input value={this.state.newFileName} onChange={this.onFileNameChange} autoFocus={true} />
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Select a directory to save new file')}</Label>
|
||||
<FileChooser
|
||||
isShowFile={false}
|
||||
repoID={repoID}
|
||||
currentPath={this.state.selectedPath}
|
||||
onDirentItemClick={this.onDirentItemClick}
|
||||
onRepoItemClick={this.onRepoItemClick}
|
||||
mode={'only_current_library'}
|
||||
hideLibraryName={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
}
|
||||
{this.state.selectedOption.value === 'existing' &&
|
||||
<FormGroup>
|
||||
<Label>{gettext('Select an existing file')}</Label>
|
||||
<FileChooser
|
||||
isShowFile={true}
|
||||
repoID={repoID}
|
||||
currentPath={this.state.selectedPath}
|
||||
onDirentItemClick={this.onDirentItemClick}
|
||||
onRepoItemClick={this.onRepoItemClick}
|
||||
mode={'only_current_library'}
|
||||
hideLibraryName={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||
{this.state.isLoading ?
|
||||
<Button color="primary" disabled><Loading/></Button> :
|
||||
<Button color="primary" onClick={this.onSubmit}>{gettext('Submit')}</Button>
|
||||
}
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddPageDialog.propTypes = propTypes;
|
||||
|
||||
export default AddPageDialog;
|
@ -1,7 +1,5 @@
|
||||
.wiki-side-panel .panel-top {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wiki-side-nav {
|
||||
|
@ -203,10 +203,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.add-view-dropdown-menu {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.view-structure-footer .dropdown button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -359,6 +355,13 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dtable-dropdown-menu.large .dropdown-item {
|
||||
min-height: 32px;
|
||||
padding: 3px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* dark mode */
|
||||
.view-structure-dark.view-structure,
|
||||
.view-structure-dark.view-structure .view-folder .icon-expand-folder {
|
114
frontend/src/pages/wiki2/index-md-viewer/index.js
Normal file
114
frontend/src/pages/wiki2/index-md-viewer/index.js
Normal file
@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { mdStringToSlate } from '@seafile/seafile-editor';
|
||||
import { isPublicWiki, repoID, serviceURL, slug } from '../../../utils/constants';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { generateNavItems } from '../utils/generate-navs';
|
||||
import NavItem from './nav-item';
|
||||
|
||||
import'./style.css';
|
||||
|
||||
const viewerPropTypes = {
|
||||
indexContent: PropTypes.string.isRequired,
|
||||
onLinkClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class IndexMdViewer extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.links = [];
|
||||
this.state = {
|
||||
currentPath: '',
|
||||
treeRoot: { name: '', href: '', children: [], isRoot: true },
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { indexContent } = this.props;
|
||||
const slateNodes = mdStringToSlate(indexContent);
|
||||
const newSlateNodes = Utils.changeMarkdownNodes(slateNodes, this.changeInlineNode);
|
||||
const treeRoot = generateNavItems(newSlateNodes);
|
||||
this.setState({
|
||||
treeRoot: treeRoot,
|
||||
});
|
||||
}
|
||||
|
||||
onLinkClick = (node) => {
|
||||
const { currentPath } = this.state;
|
||||
if (node.path === currentPath) return;
|
||||
if (node.path) {
|
||||
this.setState({ currentPath: node.path });
|
||||
}
|
||||
if (node.href) this.props.onLinkClick(node.href);
|
||||
};
|
||||
|
||||
changeInlineNode = (item) => {
|
||||
if (item.type == 'link' || item.type === 'image') {
|
||||
let url;
|
||||
|
||||
// change image url
|
||||
if (item.type == 'image' && isPublicWiki) {
|
||||
url = item.data.src;
|
||||
const re = new RegExp(serviceURL + '/lib/' + repoID +'/file.*raw=1');
|
||||
// different repo
|
||||
if (!re.test(url)) {
|
||||
return;
|
||||
}
|
||||
// get image path
|
||||
let index = url.indexOf('/file');
|
||||
let index2 = url.indexOf('?');
|
||||
const imagePath = url.substring(index + 5, index2);
|
||||
// replace url
|
||||
item.data.src = serviceURL + '/view-image-via-public-wiki/?slug=' + slug + '&path=' + imagePath;
|
||||
}
|
||||
|
||||
else if (item.type == 'link') {
|
||||
url = item.url;
|
||||
/* eslint-disable */
|
||||
let expression = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/
|
||||
/* eslint-enable */
|
||||
let re = new RegExp(expression);
|
||||
|
||||
// Solving relative paths
|
||||
if (!re.test(url)) {
|
||||
if (url.startsWith('./')) {
|
||||
url = url.slice(2);
|
||||
}
|
||||
item.url = serviceURL + '/published/' + slug + '/' + url;
|
||||
}
|
||||
// change file url
|
||||
else if (Utils.isInternalMarkdownLink(url, repoID)) {
|
||||
let path = Utils.getPathFromInternalMarkdownLink(url, repoID);
|
||||
// replace url
|
||||
item.url = serviceURL + '/published/' + slug + path;
|
||||
}
|
||||
// change dir url
|
||||
else if (Utils.isInternalDirLink(url, repoID)) {
|
||||
let path = Utils.getPathFromInternalDirLink(url, repoID);
|
||||
// replace url
|
||||
item.url = serviceURL + '/published/' + slug + path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { treeRoot, currentPath } = this.state;
|
||||
return (
|
||||
<div className="mx-4 o-hidden">
|
||||
{treeRoot.children.map(node => {
|
||||
return (
|
||||
<NavItem key={node.path} node={node} currentPath={currentPath} onLinkClick={this.onLinkClick} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexMdViewer.propTypes = viewerPropTypes;
|
||||
|
||||
export default IndexMdViewer;
|
91
frontend/src/pages/wiki2/index-md-viewer/nav-item.js
Normal file
91
frontend/src/pages/wiki2/index-md-viewer/nav-item.js
Normal file
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const propTypes = {
|
||||
node: PropTypes.object.isRequired,
|
||||
currentPath: PropTypes.string,
|
||||
onLinkClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class NavItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
expanded: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleExpanded = () => {
|
||||
const { expanded } = this.state;
|
||||
this.setState({ expanded: !expanded });
|
||||
};
|
||||
|
||||
onLinkClick = (event) => {
|
||||
event.preventDefault();
|
||||
const { node } = this.props;
|
||||
const { expanded } = this.state;
|
||||
if (node.children && node.children.length > 0 && !expanded) {
|
||||
this.setState({expanded: !expanded});
|
||||
return;
|
||||
}
|
||||
this.props.onLinkClick(node);
|
||||
};
|
||||
|
||||
itemClick = () => {
|
||||
const { node } = this.props;
|
||||
const { expanded } = this.state;
|
||||
if (node.children && node.children.length > 0) {
|
||||
this.setState({expanded: !expanded});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
renderLink = ({ href, name, path, children }) => {
|
||||
const { currentPath } = this.props;
|
||||
const className = classNames('wiki-nav-content', {
|
||||
'no-children': !children || children.length === 0,
|
||||
'wiki-nav-content-highlight': currentPath === path,
|
||||
});
|
||||
if (href && name) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<a href={href} data-path={path} onClick={this.onLinkClick} title={name}>{name}</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (name) {
|
||||
return <div className={className} onClick={this.itemClick}><span title={name}>{name}</span></div>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { node } = this.props;
|
||||
const { expanded } = this.state;
|
||||
if (node.children.length > 0) {
|
||||
return (
|
||||
<div className="pl-4 position-relative">
|
||||
<span className="switch-btn" onClick={this.toggleExpanded}>
|
||||
{<i className={`fa fa-caret-${expanded ? 'down': 'right'}`}></i>}
|
||||
</span>
|
||||
{this.renderLink(node)}
|
||||
{expanded && node.children.map((child, index) => {
|
||||
return (
|
||||
<NavItem key={index} node={child} currentPath={this.props.currentPath} onLinkClick={this.props.onLinkClick} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.renderLink(node);
|
||||
}
|
||||
}
|
||||
|
||||
NavItem.propTypes = propTypes;
|
||||
|
||||
export default NavItem;
|
37
frontend/src/pages/wiki2/index-md-viewer/style.css
Normal file
37
frontend/src/pages/wiki2/index-md-viewer/style.css
Normal file
@ -0,0 +1,37 @@
|
||||
.wiki-nav-content {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.wiki-nav-content.no-children {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.wiki-nav-content a,
|
||||
.wiki-nav-content span {
|
||||
color: #4d5156;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wiki-nav-content a:hover {
|
||||
text-decoration: none;
|
||||
color: #eb8205;
|
||||
}
|
||||
|
||||
.wiki-nav-content-highlight a {
|
||||
text-decoration: none;
|
||||
color: #eb8205;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 2px;
|
||||
color: #c0c0c0;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding-right: 10px;
|
||||
}
|
599
frontend/src/pages/wiki2/index.js
Normal file
599
frontend/src/pages/wiki2/index.js
Normal file
@ -0,0 +1,599 @@
|
||||
import React, { Component } from 'react';
|
||||
import moment from 'moment';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { Modal } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import wikiAPI from '../../utils/wiki-api';
|
||||
import { slug, wikiId, siteRoot, initialPath, isDir, sharedToken, hasIndex, lang, isEditWiki } from '../../utils/constants';
|
||||
import Dirent from '../../models/dirent';
|
||||
import WikiConfig from './models/wiki-config';
|
||||
import TreeNode from '../../components/tree-view/tree-node';
|
||||
import treeHelper from '../../components/tree-view/tree-helper';
|
||||
import toaster from '../../components/toast';
|
||||
import SidePanel from './side-panel';
|
||||
import MainPanel from './main-panel';
|
||||
import WikiLeftBar from './wiki-left-bar/wiki-left-bar';
|
||||
import PageUtils from './view-structure/page-utils';
|
||||
|
||||
import '../../css/layout.css';
|
||||
import '../../css/side-panel.css';
|
||||
import '../../css/toolbar.css';
|
||||
import '../../css/search.css';
|
||||
import './wiki.css';
|
||||
|
||||
moment.locale(lang);
|
||||
|
||||
class Wiki extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
path: '',
|
||||
pathExist: true,
|
||||
closeSideBar: false,
|
||||
isViewFile: true,
|
||||
isDataLoading: false,
|
||||
direntList: [],
|
||||
content: '',
|
||||
permission: '',
|
||||
lastModified: '',
|
||||
latestContributor: '',
|
||||
isTreeDataLoading: true,
|
||||
isConfigLoading: true,
|
||||
treeData: treeHelper.buildTree(),
|
||||
currentNode: null,
|
||||
indexNode: null,
|
||||
indexContent: '',
|
||||
currentPageId: '',
|
||||
config: {},
|
||||
repoId: '',
|
||||
};
|
||||
|
||||
window.onpopstate = this.onpopstate;
|
||||
this.indexPath = '/index.md';
|
||||
this.homePath = '/home.md';
|
||||
this.pythonWrapper = null;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
if (!Utils.isDesktop()) {
|
||||
this.setState({ closeSideBar: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getWikiConfig();
|
||||
this.loadSidePanel(initialPath);
|
||||
this.loadWikiData(initialPath);
|
||||
|
||||
this.links = document.querySelectorAll('#wiki-file-content a');
|
||||
this.links.forEach(link => link.addEventListener('click', this.onConentLinkClick));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.links.forEach(link => link.removeEventListener('click', this.onConentLinkClick));
|
||||
}
|
||||
|
||||
handlePath = () => {
|
||||
return isEditWiki ? 'edit-wiki/' : 'published/';
|
||||
};
|
||||
|
||||
getWikiConfig = () => {
|
||||
wikiAPI.getWiki2Config(wikiId).then(res => {
|
||||
const { wiki_config, repo_id } = res.data.wiki;
|
||||
this.setState({
|
||||
config: new WikiConfig(JSON.parse(wiki_config) || {}),
|
||||
isConfigLoading: false,
|
||||
repoId: repo_id,
|
||||
});
|
||||
}).catch((error) => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
this.setState({
|
||||
isConfigLoading: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
saveWikiConfig = (wikiConfig, onSuccess, onError) => {
|
||||
wikiAPI.updateWiki2Config(wikiId, JSON.stringify(wikiConfig)).then(res => {
|
||||
this.setState({
|
||||
config: new WikiConfig(wikiConfig || {}),
|
||||
});
|
||||
onSuccess && onSuccess();
|
||||
}).catch((error) => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
onError && onError();
|
||||
});
|
||||
};
|
||||
|
||||
loadSidePanel = (initialPath) => {
|
||||
if (hasIndex) {
|
||||
this.loadIndexNode();
|
||||
return;
|
||||
}
|
||||
|
||||
// load dir list
|
||||
initialPath = (isDir === 'None' || Utils.isSdocFile(initialPath)) ? '/' : initialPath;
|
||||
this.loadNodeAndParentsByPath(initialPath);
|
||||
};
|
||||
|
||||
loadWikiData = (initialPath) => {
|
||||
this.pythonWrapper = document.getElementById('wiki-file-content');
|
||||
if (isDir === 'False' && Utils.isSdocFile(initialPath)) {
|
||||
this.showDir('/');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDir === 'False') {
|
||||
// this.showFile(initialPath);
|
||||
this.setState({path: initialPath});
|
||||
return;
|
||||
}
|
||||
|
||||
// if it is a file list, remove the template content provided by python
|
||||
this.removePythonWrapper();
|
||||
|
||||
if (isDir === 'True') {
|
||||
this.showDir(initialPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDir === 'None' && initialPath === '/home.md') {
|
||||
this.showDir('/');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDir === 'None') {
|
||||
this.setState({pathExist: false});
|
||||
let fileUrl = siteRoot + this.handlePath() + wikiId + Utils.encodePath(initialPath);
|
||||
window.history.pushState({url: fileUrl, path: initialPath}, initialPath, fileUrl);
|
||||
}
|
||||
};
|
||||
|
||||
loadIndexNode = () => {
|
||||
wikiAPI.listWiki2Dir(wikiId, '/').then(res => {
|
||||
let tree = this.state.treeData;
|
||||
this.addFirstResponseListToNode(res.data.dirent_list, tree.root);
|
||||
let indexNode = tree.getNodeByPath(this.indexPath);
|
||||
wikiAPI.getWiki2FileContent(wikiId, indexNode.path).then(res => {
|
||||
this.setState({
|
||||
treeData: tree,
|
||||
indexNode: indexNode,
|
||||
indexContent: res.data.content,
|
||||
isTreeDataLoading: false,
|
||||
});
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setState({isLoadFailed: true});
|
||||
});
|
||||
};
|
||||
|
||||
showDir = (dirPath) => {
|
||||
this.removePythonWrapper();
|
||||
this.loadDirentList(dirPath);
|
||||
|
||||
// update location url
|
||||
let fileUrl = siteRoot + this.handlePath() + wikiId + Utils.encodePath(dirPath);
|
||||
window.history.pushState({url: fileUrl, path: dirPath}, dirPath, fileUrl);
|
||||
};
|
||||
|
||||
showFile = (filePath) => {
|
||||
this.setState({
|
||||
isDataLoading: true,
|
||||
isViewFile: true,
|
||||
path: filePath,
|
||||
});
|
||||
|
||||
this.removePythonWrapper();
|
||||
wikiAPI.getWiki2FileContent(wikiId, filePath).then(res => {
|
||||
let data = res.data;
|
||||
this.setState({
|
||||
isDataLoading: false,
|
||||
content: data.content,
|
||||
permission: data.permission,
|
||||
lastModified: moment.unix(data.last_modified).fromNow(),
|
||||
latestContributor: data.latest_contributor,
|
||||
});
|
||||
}).catch(error => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
|
||||
const hash = window.location.hash;
|
||||
let fileUrl = `${siteRoot}${this.handlePath()}${wikiId}${Utils.encodePath(filePath)}${hash}`;
|
||||
if (filePath === '/home.md') {
|
||||
window.history.replaceState({url: fileUrl, path: filePath}, filePath, fileUrl);
|
||||
} else {
|
||||
window.history.pushState({url: fileUrl, path: filePath}, filePath, fileUrl);
|
||||
}
|
||||
};
|
||||
|
||||
loadDirentList = (dirPath) => {
|
||||
this.setState({isDataLoading: true});
|
||||
wikiAPI.listWiki2Dir(wikiId, dirPath).then(res => {
|
||||
let direntList = res.data.dirent_list.map(item => {
|
||||
let dirent = new Dirent(item);
|
||||
return dirent;
|
||||
});
|
||||
if (dirPath === '/') {
|
||||
direntList = direntList.filter(item => {
|
||||
if (item.type === 'dir') {
|
||||
let name = item.name.toLowerCase();
|
||||
return name !== 'drafts' && name !== 'images' && name !== 'downloads';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
direntList = Utils.sortDirents(direntList, 'name', 'asc');
|
||||
this.setState({
|
||||
path: dirPath,
|
||||
isViewFile: false,
|
||||
direntList: direntList,
|
||||
isDataLoading: false,
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setState({isLoadFailed: true});
|
||||
});
|
||||
};
|
||||
|
||||
loadTreeNodeByPath = (path) => {
|
||||
let tree = this.state.treeData.clone();
|
||||
let node = tree.getNodeByPath(path);
|
||||
if (!node.isLoaded) {
|
||||
wikiAPI.listWiki2Dir(wikiId, node.path).then(res => {
|
||||
this.addResponseListToNode(res.data.dirent_list, node);
|
||||
let parentNode = tree.getNodeByPath(node.parentNode.path);
|
||||
parentNode.isExpanded = true;
|
||||
this.setState({
|
||||
treeData: tree,
|
||||
currentNode: node
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let parentNode = tree.getNodeByPath(node.parentNode.path);
|
||||
parentNode.isExpanded = true;
|
||||
this.setState({treeData: tree, currentNode: node}); //tree
|
||||
}
|
||||
};
|
||||
|
||||
loadNodeAndParentsByPath = (path) => {
|
||||
let tree = this.state.treeData.clone();
|
||||
if (Utils.isMarkdownFile(path)) {
|
||||
path = Utils.getDirName(path);
|
||||
}
|
||||
wikiAPI.listWiki2Dir(wikiId, path, true).then(res => {
|
||||
let direntList = res.data.dirent_list;
|
||||
let results = {};
|
||||
for (let i = 0; i < direntList.length; i++) {
|
||||
let object = direntList[i];
|
||||
let key = object.parent_dir;
|
||||
if (!results[key]) {
|
||||
results[key] = [];
|
||||
}
|
||||
results[key].push(object);
|
||||
}
|
||||
for (let key in results) {
|
||||
let node = tree.getNodeByPath(key);
|
||||
if (!node.isLoaded && node.path === '/') {
|
||||
this.addFirstResponseListToNode(results[key], node);
|
||||
} else if (!node.isLoaded) {
|
||||
this.addResponseListToNode(results[key], node);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
isTreeDataLoading: false,
|
||||
treeData: tree
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setState({isLoadFailed: true});
|
||||
});
|
||||
};
|
||||
|
||||
removePythonWrapper = () => {
|
||||
if (this.pythonWrapper) {
|
||||
document.body.removeChild(this.pythonWrapper);
|
||||
this.pythonWrapper = null;
|
||||
}
|
||||
};
|
||||
|
||||
onConentLinkClick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let link = '';
|
||||
if (event.target.tagName !== 'A') {
|
||||
let target = event.target.parentNode;
|
||||
while (target.tagName !== 'A') {
|
||||
target = target.parentNode;
|
||||
}
|
||||
link = target.href;
|
||||
} else {
|
||||
link = event.target.href;
|
||||
}
|
||||
this.onLinkClick(link);
|
||||
};
|
||||
|
||||
onLinkClick = (link) => {
|
||||
const url = link;
|
||||
if (Utils.isWikiInternalMarkdownLink(url, slug)) {
|
||||
let path = Utils.getPathFromWikiInternalMarkdownLink(url, slug);
|
||||
this.showFile(path);
|
||||
} else if (Utils.isWikiInternalDirLink(url, slug)) {
|
||||
let path = Utils.getPathFromWikiInternalDirLink(url, slug);
|
||||
this.showDir(path);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
if (!this.state.closeSideBar) {
|
||||
this.setState({ closeSideBar: true });
|
||||
}
|
||||
};
|
||||
|
||||
onpopstate = (event) => {
|
||||
if (event.state && event.state.path) {
|
||||
let path = event.state.path;
|
||||
if (Utils.isMarkdownFile(path)) {
|
||||
this.showFile(path);
|
||||
} else {
|
||||
this.loadDirentList(path);
|
||||
this.setState({
|
||||
path: path,
|
||||
isViewFile: false
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onSearchedClick = (item) => {
|
||||
let path = item.is_dir ? item.path.slice(0, item.path.length - 1) : item.path;
|
||||
if (this.state.currentPath === path) {
|
||||
return;
|
||||
}
|
||||
|
||||
// load sidePanel
|
||||
let index = -1;
|
||||
let paths = Utils.getPaths(path);
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
let node = this.state.treeData.getNodeByPath(node);
|
||||
if (!node) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1) { // all the data has been loaded already.
|
||||
let tree = this.state.treeData.clone();
|
||||
let node = tree.getNodeByPath(item.path);
|
||||
treeHelper.expandNode(node);
|
||||
this.setState({treeData: tree});
|
||||
} else {
|
||||
this.loadNodeAndParentsByPath(path);
|
||||
}
|
||||
|
||||
// load mainPanel
|
||||
if (item.is_dir) {
|
||||
this.showDir(path);
|
||||
} else {
|
||||
if (Utils.isMarkdownFile(path)) {
|
||||
this.showFile(path);
|
||||
} else {
|
||||
let url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(path);
|
||||
let newWindow = window.open('about:blank');
|
||||
newWindow.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMenuClick = () => {
|
||||
this.setState({closeSideBar: !this.state.closeSideBar});
|
||||
};
|
||||
|
||||
onMainNavBarClick = (nodePath) => {
|
||||
let tree = this.state.treeData.clone();
|
||||
let node = tree.getNodeByPath(nodePath);
|
||||
tree.expandNode(node);
|
||||
this.setState({treeData: tree, currentNode: node});
|
||||
this.showDir(node.path);
|
||||
};
|
||||
|
||||
onDirentClick = (dirent) => {
|
||||
let direntPath = Utils.joinPath(this.state.path, dirent.name);
|
||||
if (dirent.isDir()) { // is dir
|
||||
this.loadTreeNodeByPath(direntPath);
|
||||
this.showDir(direntPath);
|
||||
} else { // is file
|
||||
if (Utils.isMarkdownFile(direntPath)) {
|
||||
this.showFile(direntPath);
|
||||
} else {
|
||||
const w=window.open('about:blank');
|
||||
const url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(direntPath);
|
||||
w.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onCloseSide = () => {
|
||||
this.setState({closeSideBar: !this.state.closeSideBar});
|
||||
};
|
||||
|
||||
onNodeClick = (node) => {
|
||||
if (!this.state.pathExist) {
|
||||
this.setState({pathExist: true});
|
||||
}
|
||||
|
||||
if (node.object.isDir()) {
|
||||
if (!node.isLoaded) {
|
||||
let tree = this.state.treeData.clone();
|
||||
node = tree.getNodeByPath(node.path);
|
||||
wikiAPI.listWiki2Dir(wikiId, node.path).then(res => {
|
||||
this.addResponseListToNode(res.data.dirent_list, node);
|
||||
tree.collapseNode(node);
|
||||
this.setState({treeData: tree});
|
||||
});
|
||||
}
|
||||
if (node.path === this.state.path) {
|
||||
if (node.isExpanded) {
|
||||
let tree = treeHelper.collapseNode(this.state.treeData, node);
|
||||
this.setState({treeData: tree});
|
||||
} else {
|
||||
let tree = this.state.treeData.clone();
|
||||
node = tree.getNodeByPath(node.path);
|
||||
tree.expandNode(node);
|
||||
this.setState({treeData: tree});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.path === this.state.path ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.object.isDir()) { // isDir
|
||||
this.showDir(node.path);
|
||||
} else {
|
||||
if (Utils.isMarkdownFile(node.path) || Utils.isSdocFile(node.path)) {
|
||||
if (node.path !== this.state.path) {
|
||||
this.showFile(node.path);
|
||||
}
|
||||
this.onCloseSide();
|
||||
} else {
|
||||
const w = window.open('about:blank');
|
||||
const url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(node.path);
|
||||
w.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onNodeCollapse = (node) => {
|
||||
let tree = treeHelper.collapseNode(this.state.treeData, node);
|
||||
this.setState({treeData: tree});
|
||||
};
|
||||
|
||||
onNodeExpanded = (node) => {
|
||||
let tree = this.state.treeData.clone();
|
||||
node = tree.getNodeByPath(node.path);
|
||||
if (!node.isLoaded) {
|
||||
wikiAPI.listWiki2Dir(wikiId, node.path).then(res => {
|
||||
this.addResponseListToNode(res.data.dirent_list, node);
|
||||
this.setState({treeData: tree});
|
||||
});
|
||||
} else {
|
||||
tree.expandNode(node);
|
||||
this.setState({treeData: tree});
|
||||
}
|
||||
};
|
||||
|
||||
addFirstResponseListToNode = (list, node) => {
|
||||
node.isLoaded = true;
|
||||
node.isExpanded = true;
|
||||
let direntList = list.map(item => {
|
||||
return new Dirent(item);
|
||||
});
|
||||
direntList = direntList.filter(item => {
|
||||
if (item.type === 'dir') {
|
||||
let name = item.name.toLowerCase();
|
||||
return name !== 'drafts' && name !== 'images' && name !== 'downloads';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
direntList = Utils.sortDirents(direntList, 'name', 'asc');
|
||||
|
||||
let nodeList = direntList.map(object => {
|
||||
return new TreeNode({object});
|
||||
});
|
||||
node.addChildren(nodeList);
|
||||
};
|
||||
|
||||
addResponseListToNode = (list, node) => {
|
||||
node.isLoaded = true;
|
||||
node.isExpanded = true;
|
||||
let direntList = list.map(item => {
|
||||
return new Dirent(item);
|
||||
});
|
||||
direntList = Utils.sortDirents(direntList, 'name', 'asc');
|
||||
|
||||
let nodeList = direntList.map(object => {
|
||||
return new TreeNode({object});
|
||||
});
|
||||
node.addChildren(nodeList);
|
||||
};
|
||||
|
||||
setCurrentPage = (pageId, callback) => {
|
||||
const { currentPageId, config } = this.state;
|
||||
if (pageId === currentPageId) {
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
const { pages } = config;
|
||||
const currentPage = PageUtils.getPageById(pages, pageId);
|
||||
const path = currentPage.path;
|
||||
if (Utils.isMarkdownFile(path) || Utils.isSdocFile(path)) {
|
||||
if (path !== this.state.path) {
|
||||
this.showFile(path);
|
||||
}
|
||||
this.onCloseSide();
|
||||
} else {
|
||||
const w = window.open('about:blank');
|
||||
const url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(path);
|
||||
w.location.href = url;
|
||||
}
|
||||
this.setState({
|
||||
currentPageId: pageId,
|
||||
path: path,
|
||||
}, () => {
|
||||
callback && callback();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="main" className="wiki-main">
|
||||
{isEditWiki &&
|
||||
<WikiLeftBar
|
||||
config={this.state.config}
|
||||
repoId={this.state.repoId}
|
||||
updateConfig={(data) => this.saveWikiConfig(Object.assign({}, this.state.config, data))}
|
||||
/>
|
||||
}
|
||||
<SidePanel
|
||||
isLoading={this.state.isTreeDataLoading || this.state.isConfigLoading}
|
||||
closeSideBar={this.state.closeSideBar}
|
||||
currentPath={this.state.path}
|
||||
treeData={this.state.treeData}
|
||||
indexNode={this.state.indexNode}
|
||||
indexContent={this.state.indexContent}
|
||||
onCloseSide={this.onCloseSide}
|
||||
onNodeClick={this.onNodeClick}
|
||||
onNodeCollapse={this.onNodeCollapse}
|
||||
onNodeExpanded={this.onNodeExpanded}
|
||||
onLinkClick={this.onLinkClick}
|
||||
config={this.state.config}
|
||||
saveWikiConfig={this.saveWikiConfig}
|
||||
setCurrentPage={this.setCurrentPage}
|
||||
currentPageId={this.state.currentPageId}
|
||||
/>
|
||||
<MainPanel
|
||||
path={this.state.path}
|
||||
pathExist={this.state.pathExist}
|
||||
isViewFile={this.state.isViewFile}
|
||||
isDataLoading={this.state.isDataLoading}
|
||||
content={this.state.content}
|
||||
permission={this.state.permission}
|
||||
lastModified={this.state.lastModified}
|
||||
latestContributor={this.state.latestContributor}
|
||||
direntList={this.state.direntList}
|
||||
onLinkClick={this.onLinkClick}
|
||||
onMenuClick={this.onMenuClick}
|
||||
onSearchedClick={this.onSearchedClick}
|
||||
onMainNavBarClick={this.onMainNavBarClick}
|
||||
onDirentClick={this.onDirentClick}
|
||||
/>
|
||||
<MediaQuery query="(max-width: 767.8px)">
|
||||
<Modal isOpen={!this.state.closeSideBar} toggle={this.onCloseSide} contentClassName="d-none"></Modal>
|
||||
</MediaQuery>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Wiki;
|
164
frontend/src/pages/wiki2/main-panel.js
Normal file
164
frontend/src/pages/wiki2/main-panel.js
Normal file
@ -0,0 +1,164 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext, repoID, siteRoot, username, isPro, isEditWiki } from '../../utils/constants';
|
||||
import SeafileMarkdownViewer from '../../components/seafile-markdown-viewer';
|
||||
import WikiDirListView from '../../components/wiki-dir-list-view/wiki-dir-list-view';
|
||||
import Loading from '../../components/loading';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import Search from '../../components/search/search';
|
||||
import Notification from '../../components/common/notification';
|
||||
import Account from '../../components/common/account';
|
||||
import SdocWikiPageViewer from '../../components/sdoc-wiki-page-viewer';
|
||||
|
||||
const propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
pathExist: PropTypes.bool.isRequired,
|
||||
isViewFile: PropTypes.bool.isRequired,
|
||||
isDataLoading: PropTypes.bool.isRequired,
|
||||
content: PropTypes.string,
|
||||
permission: PropTypes.string,
|
||||
lastModified: PropTypes.string,
|
||||
latestContributor: PropTypes.string,
|
||||
direntList: PropTypes.array.isRequired,
|
||||
onMenuClick: PropTypes.func.isRequired,
|
||||
onSearchedClick: PropTypes.func.isRequired,
|
||||
onMainNavBarClick: PropTypes.func.isRequired,
|
||||
onDirentClick: PropTypes.func.isRequired,
|
||||
onLinkClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class MainPanel extends Component {
|
||||
|
||||
onMenuClick = () => {
|
||||
this.props.onMenuClick();
|
||||
};
|
||||
|
||||
onEditClick = (e) => {
|
||||
e.preventDefault();
|
||||
let url = siteRoot + 'lib/' + repoID + '/file' + this.props.path + '?mode=edit';
|
||||
window.open(url);
|
||||
};
|
||||
|
||||
onMainNavBarClick = (e) => {
|
||||
let path = Utils.getEventData(e, 'path');
|
||||
this.props.onMainNavBarClick(path);
|
||||
};
|
||||
|
||||
renderNavPath = () => {
|
||||
let paths = this.props.path.split('/');
|
||||
let nodePath = '';
|
||||
let pathElem = paths.map((item, index) => {
|
||||
if (item === '') {
|
||||
return null;
|
||||
}
|
||||
if (index === (paths.length - 1)) {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-file-name">{item}</span>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
nodePath += '/' + item;
|
||||
return (
|
||||
<Fragment key={index} >
|
||||
<span className="path-split">/</span>
|
||||
<a
|
||||
className="path-link"
|
||||
data-path={nodePath}
|
||||
onClick={this.onMainNavBarClick}>
|
||||
{item}
|
||||
</a>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
});
|
||||
return pathElem;
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
let { onSearchedClick } = this.props;
|
||||
const errMessage = (<div className="message err-tip">{gettext('Folder does not exist.')}</div>);
|
||||
const isViewingFile = this.props.pathExist && !this.props.isDataLoading && this.props.isViewFile;
|
||||
return (
|
||||
<div className="main-panel wiki-main-panel" style={{flex: isEditWiki ? '1 0 76%' : '1 0 80%'}}>
|
||||
<div className="main-panel-hide hide">{this.props.content}</div>
|
||||
<div className={`main-panel-north panel-top ${this.props.permission === 'rw' ? 'border-left-show' : ''}`}>
|
||||
{!username &&
|
||||
<Fragment>
|
||||
<div className="cur-view-toolbar">
|
||||
<span className="sf2-icon-menu hidden-md-up d-md-none side-nav-toggle" title="Side Nav Menu" onClick={this.onMenuClick}></span>
|
||||
</div>
|
||||
<div className="common-toolbar">
|
||||
{isPro && (
|
||||
<Search isPublic={true} repoID={repoID} onSearchedClick={onSearchedClick} placeholder={gettext('Search files')}/>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
{username && (
|
||||
<Fragment>
|
||||
<div className="cur-view-toolbar">
|
||||
<span className="sf2-icon-menu hidden-md-up d-md-none side-nav-toggle" title="Side Nav Menu" onClick={this.onMenuClick}></span>
|
||||
{this.props.permission == 'rw' && (
|
||||
Utils.isDesktop() ?
|
||||
<button className="btn btn-secondary operation-item" title={gettext('Edit')} onClick={this.onEditClick}>{gettext('Edit')}</button> :
|
||||
<span className="fa fa-pencil-alt mobile-toolbar-icon" title={gettext('Edit')} onClick={this.onEditClick} style={{'fontSize': '1.1rem'}}></span>
|
||||
)}
|
||||
</div>
|
||||
<div className="common-toolbar">
|
||||
{isPro && (
|
||||
<Search isPublic={true} repoID={repoID} onSearchedClick={onSearchedClick} placeholder={gettext('Search files')}/>
|
||||
)}
|
||||
<Notification />
|
||||
<Account />
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
<div className="main-panel-center">
|
||||
<div className={`cur-view-content ${isViewingFile ? 'o-hidden' : ''}`}>
|
||||
{!this.props.pathExist && errMessage}
|
||||
{this.props.pathExist && this.props.isDataLoading && <Loading />}
|
||||
{isViewingFile && Utils.isMarkdownFile(this.props.path) && (
|
||||
<SeafileMarkdownViewer
|
||||
isWiki={true}
|
||||
path={this.props.path}
|
||||
repoID={repoID}
|
||||
markdownContent={this.props.content}
|
||||
isFileLoading={this.props.isDataLoading}
|
||||
lastModified = {this.props.lastModified}
|
||||
latestContributor={this.props.latestContributor}
|
||||
onLinkClick={this.props.onLinkClick}
|
||||
/>
|
||||
)}
|
||||
{isViewingFile && Utils.isSdocFile(this.props.path) && (
|
||||
<SdocWikiPageViewer
|
||||
isWiki={true}
|
||||
path={this.props.path}
|
||||
repoID={repoID}
|
||||
markdownContent={this.props.content}
|
||||
isFileLoading={this.props.isDataLoading}
|
||||
lastModified = {this.props.lastModified}
|
||||
latestContributor={this.props.latestContributor}
|
||||
onLinkClick={this.props.onLinkClick}
|
||||
/>
|
||||
)}
|
||||
{(!this.props.isDataLoading && !this.props.isViewFile) && (
|
||||
<WikiDirListView
|
||||
path={this.props.path}
|
||||
direntList={this.props.direntList}
|
||||
onDirentClick={this.props.onDirentClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MainPanel.propTypes = propTypes;
|
||||
|
||||
export default MainPanel;
|
417
frontend/src/pages/wiki2/side-panel.js
Normal file
417
frontend/src/pages/wiki2/side-panel.js
Normal file
@ -0,0 +1,417 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import deepCopy from 'deep-copy';
|
||||
import { gettext, siteRoot, repoID, username, permission, isEditWiki } from '../../utils/constants';
|
||||
import toaster from '../../components/toast';
|
||||
import Loading from '../../components/loading';
|
||||
// import TreeView from '../../components/tree-view/tree-view';
|
||||
import ViewStructure from './view-structure';
|
||||
import IndexMdViewer from './index-md-viewer';
|
||||
import PageUtils from './view-structure/page-utils';
|
||||
import NewFolderDialog from './view-structure/new-folder-dialog';
|
||||
import AddNewPageDialog from './view-structure/add-new-page-dialog';
|
||||
import ViewStructureFooter from './view-structure/view-structure-footer';
|
||||
import { generateUniqueId, getIconURL, isObjectNotEmpty } from './utils';
|
||||
import Folder from './models/folder';
|
||||
import Page from './models/page';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
|
||||
export const FOLDER = 'folder';
|
||||
export const PAGE = 'page';
|
||||
|
||||
const propTypes = {
|
||||
closeSideBar: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
treeData: PropTypes.object.isRequired,
|
||||
indexNode: PropTypes.object,
|
||||
indexContent: PropTypes.string.isRequired,
|
||||
currentPath: PropTypes.string.isRequired,
|
||||
onCloseSide: PropTypes.func.isRequired,
|
||||
onNodeClick: PropTypes.func.isRequired,
|
||||
onNodeCollapse: PropTypes.func.isRequired,
|
||||
onNodeExpanded: PropTypes.func.isRequired,
|
||||
onLinkClick: PropTypes.func.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
saveWikiConfig: PropTypes.func.isRequired,
|
||||
setCurrentPage: PropTypes.func.isRequired,
|
||||
currentPageId: PropTypes.string,
|
||||
};
|
||||
|
||||
class SidePanel extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.isNodeMenuShow = false;
|
||||
this.state = {
|
||||
isShowNewFolderDialog: false,
|
||||
isShowAddNewPageDialog: false,
|
||||
};
|
||||
}
|
||||
|
||||
renderIndexView = () => {
|
||||
return (
|
||||
<div className="wiki-pages-container">
|
||||
<div style={{marginTop: '2px'}}></div>
|
||||
<IndexMdViewer
|
||||
indexContent={this.props.indexContent}
|
||||
onLinkClick={this.props.onLinkClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderTreeView = () => {
|
||||
return (
|
||||
<div className="wiki-pages-container">
|
||||
{/* {this.props.treeData && (
|
||||
<TreeView
|
||||
treeData={this.props.treeData}
|
||||
currentPath={this.props.currentPath}
|
||||
isNodeMenuShow={this.isNodeMenuShow}
|
||||
onNodeClick={this.props.onNodeClick}
|
||||
onNodeCollapse={this.props.onNodeCollapse}
|
||||
onNodeExpanded={this.props.onNodeExpanded}
|
||||
/>
|
||||
)} */}
|
||||
{isEditWiki &&
|
||||
<ViewStructureFooter
|
||||
onToggleAddView={this.openAddPageDialog}
|
||||
onToggleAddFolder={this.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowNewFolderDialog &&
|
||||
<NewFolderDialog
|
||||
onAddFolder={this.addPageFolder}
|
||||
onToggleAddFolderDialog={this.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowAddNewPageDialog &&
|
||||
<AddNewPageDialog
|
||||
toggle={this.closeAddNewPageDialog}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
confirmDeletePage = (pageId) => {
|
||||
const config = deepCopy(this.props.config);
|
||||
const { pages, navigation } = config;
|
||||
const index = PageUtils.getPageIndexById(pageId, pages);
|
||||
const pageIndex = pages.findIndex(item => item.id === pageId);
|
||||
let path = pages[pageIndex].path
|
||||
|
||||
config.pages.splice(index, 1);
|
||||
PageUtils.deletePage(navigation, pageId);
|
||||
this.props.saveWikiConfig(config);
|
||||
seafileAPI.deleteFile(repoID, path);
|
||||
if (config.pages.length > 0) {
|
||||
this.props.setCurrentPage(config.pages[0].id);
|
||||
} else {
|
||||
this.props.setCurrentPage('');
|
||||
}
|
||||
};
|
||||
|
||||
onAddNewPage = async ({name, icon, path, successCallback, errorCallback}) => {
|
||||
const { config } = this.props;
|
||||
const navigation = config.navigation;
|
||||
const pageId = generateUniqueId(navigation);
|
||||
const newPage = new Page({ id: pageId, name, icon, path});
|
||||
this.addPage(newPage, successCallback, errorCallback);
|
||||
};
|
||||
|
||||
duplicatePage = async (fromPageConfig, successCallback, errorCallback) => {
|
||||
const { config } = this.props;
|
||||
const { name, from_page_id } = fromPageConfig;
|
||||
const { navigation, pages } = config;
|
||||
const fromPage = PageUtils.getPageById(pages, from_page_id);
|
||||
const newPageId = generateUniqueId(navigation);
|
||||
let newPageConfig = {
|
||||
...fromPage,
|
||||
id: newPageId,
|
||||
name,
|
||||
};
|
||||
const newPage = new Page({ ...newPageConfig });
|
||||
this.addPage(newPage, successCallback, errorCallback);
|
||||
};
|
||||
|
||||
addPage = (page, successCallback, errorCallback) => {
|
||||
const { config } = this.props;
|
||||
const navigation = config.navigation;
|
||||
const pageId = page.id;
|
||||
config.pages.push(page);
|
||||
PageUtils.addPage(navigation, pageId, this.current_folder_id);
|
||||
config.navigation = navigation;
|
||||
const onSuccess = () => {
|
||||
this.props.setCurrentPage(pageId, successCallback);
|
||||
successCallback();
|
||||
};
|
||||
this.props.saveWikiConfig(config, onSuccess, errorCallback);
|
||||
};
|
||||
|
||||
onUpdatePage = (pageId, newPage) => {
|
||||
if (newPage.name === '') {
|
||||
toaster.danger(gettext('Page name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
const { config } = this.props;
|
||||
let pages = config.pages;
|
||||
let currentPage = pages.find(page => page.id === pageId);
|
||||
Object.assign(currentPage, newPage);
|
||||
config.pages = pages;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
movePage = ({ moved_view_id, target_view_id, source_view_folder_id, target_view_folder_id, move_position }) => {
|
||||
let config = deepCopy(this.props.config);
|
||||
let { navigation } = config;
|
||||
PageUtils.movePage(navigation, moved_view_id, target_view_id, source_view_folder_id, target_view_folder_id, move_position);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
movePageOut = (moved_page_id, source_folder_id, target_folder_id, move_position) => {
|
||||
let config = deepCopy(this.props.config);
|
||||
let { navigation } = config;
|
||||
PageUtils.movePageOut(navigation, moved_page_id, source_folder_id, target_folder_id, move_position);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
// Create a new folder in the root directory (not supported to create a new subfolder in the folder)
|
||||
addPageFolder = (folder_data, parent_folder_id) => {
|
||||
const { config } = this.props;
|
||||
const { navigation } = config;
|
||||
let folder_id = generateUniqueId(navigation);
|
||||
let newFolder = new Folder({ id: folder_id, ...folder_data });
|
||||
// No parent folder, directly add to the root directory
|
||||
if (!parent_folder_id) {
|
||||
config.navigation.push(newFolder);
|
||||
} else { // Recursively find the parent folder and add
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._addFolder(item, newFolder, parent_folder_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
_addFolder(folder, newFolder, parent_folder_id) {
|
||||
if (folder.id === parent_folder_id) {
|
||||
folder.children.push(newFolder);
|
||||
return;
|
||||
}
|
||||
folder.children.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._addFolder(item, newFolder, parent_folder_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onModifyFolder = (folder_id, folder_data) => {
|
||||
const { config } = this.props;
|
||||
const { navigation } = config;
|
||||
PageUtils.modifyFolder(navigation, folder_id, folder_data);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
onDeleteFolder = (page_folder_id) => {
|
||||
const { config } = this.props;
|
||||
const { navigation, pages } = config;
|
||||
PageUtils.deleteFolder(navigation, pages, page_folder_id);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
// Drag a folder to the front and back of another folder
|
||||
onMoveFolder = (moved_folder_id, target_folder_id, move_position) => {
|
||||
const { config } = this.props;
|
||||
const { navigation } = config;
|
||||
let updatedNavigation = deepCopy(navigation);
|
||||
|
||||
// Get the moved folder first and delete the original location
|
||||
let moved_folder;
|
||||
let moved_folder_index = PageUtils.getFolderIndexById(updatedNavigation, moved_folder_id);
|
||||
if (moved_folder_index === -1) {
|
||||
updatedNavigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
moved_folder_index = PageUtils.getFolderIndexById(item.children, moved_folder_id);
|
||||
if (moved_folder_index > -1) {
|
||||
moved_folder = item.children[moved_folder_index];
|
||||
item.children.splice(moved_folder_index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
moved_folder = updatedNavigation[moved_folder_index];
|
||||
updatedNavigation.splice(moved_folder_index, 1);
|
||||
}
|
||||
let indexOffset = 0;
|
||||
if (move_position === 'move_below') {
|
||||
indexOffset++;
|
||||
}
|
||||
// Get the location of the release
|
||||
let target_folder_index = PageUtils.getFolderIndexById(updatedNavigation, target_folder_id);
|
||||
if (target_folder_index === -1) {
|
||||
updatedNavigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
target_folder_index = PageUtils.getFolderIndexById(item.children, target_folder_id);
|
||||
if (target_folder_index > -1) {
|
||||
item.children.splice(target_folder_index + indexOffset, 0, moved_folder);
|
||||
}
|
||||
} else {
|
||||
// not changed
|
||||
updatedNavigation = navigation;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updatedNavigation.splice(target_folder_index + indexOffset, 0, moved_folder);
|
||||
}
|
||||
config.navigation = updatedNavigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
// Not support yet: Move a folder into another folder
|
||||
moveFolderToFolder = (moved_folder_id, target_folder_id) => {
|
||||
let { config } = this.props;
|
||||
let { navigation } = config;
|
||||
|
||||
// Find the folder and move it to this new folder
|
||||
let target_folder = PageUtils.getFolderById(navigation, target_folder_id);
|
||||
if (!target_folder) {
|
||||
toaster.danger('Only_support_two_level_folders');
|
||||
return;
|
||||
}
|
||||
|
||||
let moved_folder;
|
||||
let moved_folder_index = PageUtils.getFolderIndexById(navigation, moved_folder_id);
|
||||
|
||||
// The original directory is in the root directory
|
||||
if (moved_folder_index > -1) {
|
||||
moved_folder = PageUtils.getFolderById(navigation, moved_folder_id);
|
||||
// If moved folder There are other directories under the ID, and dragging is not supported
|
||||
if (moved_folder.children.some(item => item.type === FOLDER)) {
|
||||
toaster.danger('Only_support_two_level_folders');
|
||||
return;
|
||||
}
|
||||
target_folder.children.push(moved_folder);
|
||||
navigation.splice(moved_folder_index, 1);
|
||||
} else { // The original directory is not in the root directory
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
let moved_folder_index = PageUtils.getFolderIndexById(item.children, moved_folder_id);
|
||||
if (moved_folder_index > -1) {
|
||||
moved_folder = item.children[moved_folder_index];
|
||||
target_folder.children.push(moved_folder);
|
||||
item.children.splice(moved_folder_index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
onToggleAddFolder = () => {
|
||||
this.setState({ isShowNewFolderDialog: !this.state.isShowNewFolderDialog });
|
||||
};
|
||||
|
||||
openAddPageDialog = (folder_id) => {
|
||||
this.current_folder_id = folder_id;
|
||||
this.setState({ isShowAddNewPageDialog: true });
|
||||
};
|
||||
|
||||
closeAddNewPageDialog = () => {
|
||||
this.current_folder_id = null;
|
||||
this.setState({ isShowAddNewPageDialog: false });
|
||||
};
|
||||
|
||||
onSetFolderId = (folder_id) => {
|
||||
this.current_folder_id = folder_id;
|
||||
};
|
||||
|
||||
renderFolderView = () => {
|
||||
const { config } = this.props;
|
||||
const { pages, navigation } = config;
|
||||
return (
|
||||
<div className="wiki-pages-container">
|
||||
<ViewStructure
|
||||
isEditMode={isEditWiki}
|
||||
navigation={navigation}
|
||||
views={pages}
|
||||
onToggleAddView={this.openAddPageDialog}
|
||||
onDeleteView={this.confirmDeletePage}
|
||||
onUpdatePage={this.onUpdatePage}
|
||||
onSelectView={this.props.setCurrentPage}
|
||||
onMoveView={this.movePage}
|
||||
movePageOut={this.movePageOut}
|
||||
onToggleAddFolder={this.onToggleAddFolder}
|
||||
onModifyFolder={this.onModifyFolder}
|
||||
onDeleteFolder={this.onDeleteFolder}
|
||||
onMoveFolder={this.onMoveFolder}
|
||||
moveFolderToFolder={this.moveFolderToFolder}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
duplicatePage={this.duplicatePage}
|
||||
onSetFolderId={this.onSetFolderId}
|
||||
currentPageId={this.props.currentPageId}
|
||||
/>
|
||||
{this.state.isShowNewFolderDialog &&
|
||||
<NewFolderDialog
|
||||
onAddFolder={this.addPageFolder}
|
||||
onToggleAddFolderDialog={this.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowAddNewPageDialog &&
|
||||
<AddNewPageDialog
|
||||
toggle={this.closeAddNewPageDialog}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderContent = () => {
|
||||
const { isLoading, indexNode, config } = this.props;
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (indexNode) {
|
||||
return this.renderIndexView();
|
||||
}
|
||||
if (isObjectNotEmpty(config)) {
|
||||
return this.renderFolderView();
|
||||
}
|
||||
return this.renderTreeView();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { wiki_name, wiki_icon } = this.props.config;
|
||||
const src = getIconURL(repoID, wiki_icon);
|
||||
return (
|
||||
<div className={`side-panel wiki-side-panel ${this.props.closeSideBar ? '': 'left-zero'}`}>
|
||||
<div className="side-panel-top panel-top">
|
||||
{src && <img src={src} width="32" height="32" alt='' className='mr-2' />}
|
||||
<h4 className="ml-0 mb-0">{wiki_name}</h4>
|
||||
</div>
|
||||
<div id="side-nav" className="wiki-side-nav" role="navigation">
|
||||
{this.renderContent() }
|
||||
{(username && permission) && (
|
||||
<div className="text-left p-2">
|
||||
<a href={siteRoot + 'library/' + repoID + '/'} className="text-dark text-decoration-underline">
|
||||
{gettext('Go to Library')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SidePanel.propTypes = propTypes;
|
||||
|
||||
export default SidePanel;
|
91
frontend/src/pages/wiki2/utils/generate-navs.js
Normal file
91
frontend/src/pages/wiki2/utils/generate-navs.js
Normal file
@ -0,0 +1,91 @@
|
||||
class TreeNode {
|
||||
|
||||
constructor({ name, href, parentNode }) {
|
||||
this.name = name;
|
||||
this.href = href;
|
||||
this.parentNode = parentNode || null;
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
setParent(parentNode) {
|
||||
this.parentNode = parentNode;
|
||||
}
|
||||
|
||||
addChildren(nodeList) {
|
||||
// nodeList.forEach((node) => {
|
||||
// node.setParent(this);
|
||||
// });
|
||||
this.children = nodeList;
|
||||
}
|
||||
}
|
||||
|
||||
const setNodePath = (node, parentNodePath) => {
|
||||
let name = node.name;
|
||||
let path = parentNodePath === '/' ? parentNodePath + name : parentNodePath + '/' + name;
|
||||
node.path = path;
|
||||
if (node.children.length > 0) {
|
||||
node.children.forEach(child => {
|
||||
setNodePath(child, path);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// translate slate_paragraph_node to treeNode
|
||||
const transParagraph = (paragraphNode) => {
|
||||
let treeNode;
|
||||
if (paragraphNode.children[1] && paragraphNode.children[1].type === 'link') {
|
||||
// paragraph node is a link node
|
||||
const linkNode = paragraphNode.children[1];
|
||||
const textNode = linkNode.children[0];
|
||||
const name = textNode ? textNode.text : '';
|
||||
treeNode = new TreeNode({ name: name, href: linkNode.url });
|
||||
} else if (paragraphNode.children[0]) {
|
||||
// paragraph first child node is a text node, then get node name
|
||||
const textNode = paragraphNode.children[0];
|
||||
const name = textNode.text ? textNode.text : '';
|
||||
treeNode = new TreeNode({ name: name, href: '' });
|
||||
} else {
|
||||
treeNode = new TreeNode({ name: '', href: '' });
|
||||
}
|
||||
return treeNode;
|
||||
};
|
||||
|
||||
// slateNodes is list items of an unordered list or ordered list, translate them to treeNode and add to parentTreeNode
|
||||
const transSlateToTree = (slateNodes, parentTreeNode) => {
|
||||
let treeNodes = slateNodes.map((slateNode) => {
|
||||
// item has children(unordered list)
|
||||
if (slateNode.children.length === 2 && (slateNode.children[1].type === 'unordered_list' || slateNode.children[1].type === 'ordered_list')) {
|
||||
// slateNode.nodes[0] is paragraph, create TreeNode, set name and href
|
||||
const paragraphNode = slateNode.children[0];
|
||||
const treeNode = transParagraph(paragraphNode);
|
||||
// slateNode.nodes[1] is list, set it as TreeNode's children
|
||||
const listNode = slateNode.children[1];
|
||||
// Add sub list items to the tree node
|
||||
return transSlateToTree(listNode.children, treeNode);
|
||||
} else {
|
||||
// item doesn't have children list
|
||||
if (slateNode.children[0] && (slateNode.children[0].type === 'paragraph')) {
|
||||
return transParagraph(slateNode.children[0]);
|
||||
} else {
|
||||
// list item contain table/code_block/blockqupta
|
||||
return new TreeNode({ name: '', href: '' });
|
||||
}
|
||||
}
|
||||
});
|
||||
parentTreeNode.addChildren(treeNodes);
|
||||
return parentTreeNode;
|
||||
};
|
||||
|
||||
export const generateNavItems = (slateNodes) => {
|
||||
let treeRoot = new TreeNode({ name: '', href: '' });
|
||||
slateNodes.forEach((node) => {
|
||||
if (node.type === 'unordered_list' || node.type === 'ordered_list') {
|
||||
treeRoot = transSlateToTree(node.children, treeRoot);
|
||||
setNodePath(treeRoot, '/');
|
||||
}
|
||||
});
|
||||
return treeRoot;
|
||||
};
|
||||
|
||||
|
||||
|
138
frontend/src/pages/wiki2/view-structure/add-new-page-dialog.js
Normal file
138
frontend/src/pages/wiki2/view-structure/add-new-page-dialog.js
Normal file
@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input, Button } from 'reactstrap';
|
||||
import { gettext, repoID } from '../../../utils/constants';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import toaster from '../../../components/toast';
|
||||
import Loading from '../../../components/loading';
|
||||
|
||||
const propTypes = {
|
||||
toggle: PropTypes.func.isRequired,
|
||||
onAddNewPage: PropTypes.func,
|
||||
};
|
||||
|
||||
const NEW_WIKI_FILE_PATH = '/wiki-pages/';
|
||||
|
||||
class AddNewPageDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
pageName: '',
|
||||
isLoading: true,
|
||||
errMessage: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.getDirInfo(repoID, NEW_WIKI_FILE_PATH).then((res) => {
|
||||
if (res.data.path === NEW_WIKI_FILE_PATH) {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (error.response.data.error_msg === 'Folder /wiki-pages/ not found.') {
|
||||
seafileAPI.createDir(repoID, NEW_WIKI_FILE_PATH).then((res) => {
|
||||
if (res.data === 'success') {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
this.setState({ isLoading: false });
|
||||
});
|
||||
} else {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleChange = (event) => {
|
||||
let value = event.target.value;
|
||||
if (value !== this.state.pageName) {
|
||||
this.setState({ pageName: value });
|
||||
}
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.props.toggle();
|
||||
};
|
||||
|
||||
checkName = (newName) => {
|
||||
if (newName === '') {
|
||||
toaster.danger(gettext('Name cannot be empty'));
|
||||
return false;
|
||||
}
|
||||
if (newName.includes('/')) {
|
||||
toaster.danger(gettext('Name cannot contain slash'));
|
||||
return false;
|
||||
}
|
||||
if (newName.includes('\\')) {
|
||||
toaster.danger(gettext('Name cannot contain backslash'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const pageName = this.state.pageName.trim();
|
||||
if (this.checkName(pageName)) {
|
||||
this.setState({ isLoading: true });
|
||||
this.createFile(pageName, `${NEW_WIKI_FILE_PATH}${pageName}.sdoc`);
|
||||
}
|
||||
};
|
||||
|
||||
createFile = (pageName, filePath) => {
|
||||
seafileAPI.createFile(repoID, filePath).then(res => {
|
||||
const { obj_name, parent_dir } = res.data;
|
||||
this.props.onAddNewPage({
|
||||
name: pageName,
|
||||
icon: '',
|
||||
path: parent_dir === '/' ? `/${obj_name}` : `${parent_dir}/${obj_name}`,
|
||||
successCallback: this.onSuccess,
|
||||
errorCallback: this.onError,
|
||||
});
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
this.onError();
|
||||
});
|
||||
};
|
||||
|
||||
onSuccess = () => {
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
this.setState({ isLoading: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle} autoFocus={false}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('Add page')}</ModalHeader>
|
||||
<ModalBody className='pr-4'>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Page name')}</Label>
|
||||
<Input value={this.state.pageName} onChange={this.handleChange} autoFocus={true} />
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||
{this.state.isLoading ?
|
||||
<Button color="primary" disabled><Loading/></Button> :
|
||||
<Button color="primary" onClick={this.onSubmit}>{gettext('Submit')}</Button>
|
||||
}
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewPageDialog.propTypes = propTypes;
|
||||
|
||||
export default AddNewPageDialog;
|
@ -8,12 +8,12 @@ class AddViewDropdownMenu extends Component {
|
||||
|
||||
toggle = event => {
|
||||
this.onStopPropagation(event);
|
||||
this.props.onToggleAddViewDropdown();
|
||||
this.props.toggleDropdown();
|
||||
};
|
||||
|
||||
onToggleAddView = event => {
|
||||
addPage = event => {
|
||||
this.onStopPropagation(event);
|
||||
this.props.onToggleAddView();
|
||||
this.props.onToggleAddView(null);
|
||||
};
|
||||
|
||||
onToggleAddFolder = event => {
|
||||
@ -29,9 +29,9 @@ class AddViewDropdownMenu extends Component {
|
||||
return (
|
||||
<Dropdown isOpen toggle={this.toggle}>
|
||||
<DropdownToggle caret></DropdownToggle>
|
||||
<DropdownMenu container="body" className='dtable-dropdown-menu large add-view-dropdown-menu' style={{ zIndex: 1061 }}>
|
||||
<DropdownItem onClick={this.onToggleAddView}>
|
||||
<Icon symbol={'main-view'}/>
|
||||
<DropdownMenu container="body" className='dtable-dropdown-menu large mt-0'>
|
||||
<DropdownItem onClick={this.addPage}>
|
||||
<Icon symbol={'file'}/>
|
||||
<span className='item-text'>{gettext('Add page')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this.onToggleAddFolder}>
|
||||
@ -45,7 +45,7 @@ class AddViewDropdownMenu extends Component {
|
||||
}
|
||||
|
||||
AddViewDropdownMenu.propTypes = {
|
||||
onToggleAddViewDropdown: PropTypes.func,
|
||||
toggleDropdown: PropTypes.func,
|
||||
onToggleAddView: PropTypes.func,
|
||||
onToggleAddFolder: PropTypes.func,
|
||||
};
|
@ -82,7 +82,7 @@ export default class FolderOperationDropdownMenu extends Component {
|
||||
style={{ zIndex: 1051 }}
|
||||
>
|
||||
<DropdownItem onClick={this.props.onToggleAddView.bind(this, this.props.folderId)}>
|
||||
<Icon symbol={'main-view'}/>
|
||||
<Icon symbol={'file'}/>
|
||||
<span className="item-text">{gettext('Add page')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this.openFolderEditor}>
|
@ -9,41 +9,27 @@ class ViewStructureFooter extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isShowAddViewDropdownMenu: false,
|
||||
isAddToolHover: false,
|
||||
isShowDropdownMenu: false,
|
||||
};
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({ isAddToolHover: true });
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({ isAddToolHover: false });
|
||||
};
|
||||
|
||||
onToggleAddViewDropdown = (event) => {
|
||||
toggleDropdown = (event) => {
|
||||
event && event.stopPropagation();
|
||||
this.setState({ isShowAddViewDropdownMenu: !this.state.isShowAddViewDropdownMenu });
|
||||
this.setState({ isShowDropdownMenu: !this.state.isShowDropdownMenu });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className='view-structure-footer'
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
ref={ref => this.viewFooterRef = ref}
|
||||
>
|
||||
<div className='view-structure-footer'>
|
||||
<div className='add-view-wrapper'>
|
||||
<CommonAddTool
|
||||
className='add-view-btn'
|
||||
callBack={this.onToggleAddViewDropdown}
|
||||
callBack={this.toggleDropdown}
|
||||
footerName={gettext('Add page or folder')}
|
||||
/>
|
||||
{this.state.isShowAddViewDropdownMenu &&
|
||||
{this.state.isShowDropdownMenu &&
|
||||
<AddViewDropdownMenu
|
||||
onToggleAddViewDropdown={this.onToggleAddViewDropdown}
|
||||
toggleDropdown={this.toggleDropdown}
|
||||
onToggleAddView={this.props.onToggleAddView}
|
||||
onToggleAddFolder={this.props.onToggleAddFolder}
|
||||
/>
|
@ -67,10 +67,6 @@ class ViewStructure extends Component {
|
||||
this.idFoldedStatusMap = idFoldedStatusMap;
|
||||
};
|
||||
|
||||
onToggleAddView = (folderId) => {
|
||||
this.props.onToggleAddView(folderId);
|
||||
};
|
||||
|
||||
onMoveViewToFolder = (source_view_folder_id, moved_view_id, target_view_folder_id) => {
|
||||
this.props.onMoveView({
|
||||
moved_view_id,
|
||||
@ -221,7 +217,7 @@ class ViewStructure extends Component {
|
||||
<StructureBody />
|
||||
{(this.props.isEditMode && !isSpecialInstance) &&
|
||||
<ViewStructureFooter
|
||||
onToggleAddView={this.onToggleAddView.bind(this, null)}
|
||||
onToggleAddView={this.props.onToggleAddView}
|
||||
onToggleAddFolder={this.props.onToggleAddFolder}
|
||||
/>
|
||||
}
|
143
frontend/src/pages/wiki2/wiki.css
Normal file
143
frontend/src/pages/wiki2/wiki.css
Normal file
@ -0,0 +1,143 @@
|
||||
.wiki-side-panel .panel-top {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wiki-side-nav {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* for ff */
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.wiki-pages-heading {
|
||||
position: relative;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
padding: 0.5rem 0 0.5rem 2rem;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
line-height: 1.5;
|
||||
height: 40px;
|
||||
background-color: #f9f9f9;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.heading-icon {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 25%;
|
||||
color: #888;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.wiki-pages-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.wiki-pages-container:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.wiki-pages-container .tree-view {
|
||||
margin-left: -10px;
|
||||
margin-top: 14px;
|
||||
padding-left:0;
|
||||
}
|
||||
|
||||
img[src=""] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.wiki-side-panel {
|
||||
flex: 0 0 20%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.wiki-side-panel {
|
||||
z-index: 1051;
|
||||
}
|
||||
}
|
||||
|
||||
.wiki-main-panel {
|
||||
flex: 1 0 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wiki-main-panel .main-panel-north {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.cur-view-content .wiki-page-container {
|
||||
margin: 0 -1rem -1.25rem;
|
||||
padding: 30px 1rem 1.25rem;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding-left: 30px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.cur-view-content .wiki-page-content {
|
||||
width: calc(100% - 200px);
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.cur-view-content .wiki-page-container {
|
||||
padding: 0 14px;
|
||||
padding-top: 30px;
|
||||
}
|
||||
.cur-view-content .wiki-page-content {
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* reset article h1 */
|
||||
.wiki-main-panel .article h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.wiki-page-container .outline-h2,
|
||||
.wiki-page-container .outline-h3 {
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
color: #4d5156;
|
||||
}
|
||||
|
||||
.wiki-page-container .outline-h2.active,
|
||||
.wiki-page-container .outline-h3.active {
|
||||
color: #eb8205;
|
||||
}
|
||||
|
||||
.wiki-page-container .sf-slate-viewer-scroll-container {
|
||||
background-color: #fff !important;
|
||||
padding: 0px !important;
|
||||
overflow: inherit;
|
||||
}
|
||||
|
||||
.wiki-page-container .sf-slate-viewer-article-container {
|
||||
margin: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wiki-page-container .sf-slate-viewer-outline {
|
||||
top: 79px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.wiki-page-container .article {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
@ -2,16 +2,15 @@ import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'reactstrap';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { gettext, canPublishRepo } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../../components/toast';
|
||||
import ModalPortal from '../../components/modal-portal';
|
||||
import EmptyTip from '../../components/empty-tip';
|
||||
import CommonToolbar from '../../components/toolbar/common-toolbar';
|
||||
import NewWikiDialog from '../../components/dialog/new-wiki-dialog';
|
||||
import WikiSelectDialog from '../../components/dialog/wiki-select-dialog';
|
||||
import AddWikiDialog from '../../components/dialog/add-wiki-dialog';
|
||||
import WikiListView from '../../components/wiki-list-view/wiki-list-view';
|
||||
import wikiAPI from '../../utils/wiki-api';
|
||||
|
||||
const propTypes = {
|
||||
onShowSidePanel: PropTypes.func.isRequired,
|
||||
@ -26,8 +25,7 @@ class Wikis extends Component {
|
||||
errorMsg: '',
|
||||
wikis: [],
|
||||
isShowAddWikiMenu: false,
|
||||
isShowSelectDialog: false,
|
||||
isShowCreateDialog: false,
|
||||
isShowAddDialog: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -36,10 +34,30 @@ class Wikis extends Component {
|
||||
}
|
||||
|
||||
getWikis = () => {
|
||||
seafileAPI.listWikis().then(res => {
|
||||
let wikis = [];
|
||||
wikiAPI.listWikis().then(res => {
|
||||
wikis = wikis.concat(res.data.data);
|
||||
wikis.map(wiki => {
|
||||
return wiki['version'] = 'v1';
|
||||
});
|
||||
wikiAPI.listWikis2().then(res => {
|
||||
let wikis2 = res.data.data;
|
||||
wikis2.map(wiki => {
|
||||
return wiki['version'] = 'v2';
|
||||
});
|
||||
this.setState({
|
||||
loading: false,
|
||||
wikis: wikis.concat(wikis2)
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
this.setState({
|
||||
loading: false,
|
||||
wikis: res.data.data
|
||||
wikis: wikis
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
@ -58,55 +76,51 @@ class Wikis extends Component {
|
||||
this.setState({isShowAddWikiMenu: !this.state.isShowAddWikiMenu});
|
||||
};
|
||||
|
||||
onSelectToggle = () => {
|
||||
this.setState({isShowSelectDialog: !this.state.isShowSelectDialog});
|
||||
toggelAddWikiDialog = () => {
|
||||
this.setState({isShowAddDialog: !this.state.isShowAddDialog});
|
||||
};
|
||||
|
||||
onCreateToggle = () => {
|
||||
this.setState({isShowCreateDialog: !this.state.isShowCreateDialog});
|
||||
};
|
||||
|
||||
addWiki = (repoID) => {
|
||||
seafileAPI.addWiki(repoID).then((res) => {
|
||||
this.state.wikis.push(res.data);
|
||||
this.setState({wikis: this.state.wikis});
|
||||
addWiki = (wikiName) => {
|
||||
wikiAPI.addWiki2(wikiName).then((res) => {
|
||||
let wikis = this.state.wikis.slice(0);
|
||||
let new_wiki = res.data;
|
||||
new_wiki['version'] = 'v2';
|
||||
wikis.push(new_wiki);
|
||||
this.setState({ wikis });
|
||||
}).catch((error) => {
|
||||
if(error.response) {
|
||||
let errorMsg = error.response.data.error_msg;
|
||||
toaster.danger(errorMsg);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
renameWiki = (wiki, newName) => {
|
||||
seafileAPI.renameWiki(wiki.slug, newName).then((res) => {
|
||||
let wikis = this.state.wikis.map((item) => {
|
||||
if (item.name === wiki.name) {
|
||||
item = res.data;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setState({wikis: wikis});
|
||||
}).catch((error) => {
|
||||
if(error.response) {
|
||||
let errorMsg = error.response.data.error_msg;
|
||||
toaster.danger(errorMsg);
|
||||
if (error.response) {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
deleteWiki = (wiki) => {
|
||||
seafileAPI.deleteWiki(wiki.slug).then(() => {
|
||||
let wikis = this.state.wikis.filter(item => {
|
||||
return item.name !== wiki.name;
|
||||
if (wiki.version === 'v1') {
|
||||
wikiAPI.deleteWiki(wiki.id).then(() => {
|
||||
let wikis = this.state.wikis.filter(item => {
|
||||
return item.name !== wiki.name;
|
||||
});
|
||||
this.setState({wikis: wikis});
|
||||
}).catch((error) => {
|
||||
if(error.response) {
|
||||
let errorMsg = error.response.data.error_msg;
|
||||
toaster.danger(errorMsg);
|
||||
}
|
||||
});
|
||||
this.setState({wikis: wikis});
|
||||
}).catch((error) => {
|
||||
if(error.response) {
|
||||
let errorMsg = error.response.data.error_msg;
|
||||
toaster.danger(errorMsg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
wikiAPI.deleteWiki2(wiki.id).then(() => {
|
||||
let wikis = this.state.wikis.filter(item => {
|
||||
return item.name !== wiki.name;
|
||||
});
|
||||
this.setState({wikis: wikis});
|
||||
}).catch((error) => {
|
||||
if(error.response) {
|
||||
let errorMsg = error.response.data.error_msg;
|
||||
toaster.danger(errorMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -119,10 +133,10 @@ class Wikis extends Component {
|
||||
<div className="operation">
|
||||
<Fragment>
|
||||
<MediaQuery query="(min-width: 768px)">
|
||||
<Button className="btn btn-secondary operation-item" onClick={this.onSelectToggle}>{gettext('Add Wiki')}</Button>
|
||||
<Button className="btn btn-secondary operation-item" onClick={this.toggelAddWikiDialog}>{gettext('Add Wiki')}</Button>
|
||||
</MediaQuery>
|
||||
<MediaQuery query="(max-width: 767.8px)">
|
||||
<span className="sf2-icon-plus mobile-toolbar-icon" title={gettext('Add Wiki')} onClick={this.onSelectToggle}></span>
|
||||
<span className="sf2-icon-plus mobile-toolbar-icon" title={gettext('Add Wiki')} onClick={this.toggelAddWikiDialog}></span>
|
||||
</MediaQuery>
|
||||
</Fragment>
|
||||
</div>
|
||||
@ -130,6 +144,11 @@ class Wikis extends Component {
|
||||
</div>
|
||||
<CommonToolbar onSearchedClick={this.props.onSearchedClick} />
|
||||
</div>
|
||||
{this.state.isShowAddDialog &&
|
||||
<ModalPortal>
|
||||
<AddWikiDialog toggleCancel={this.toggelAddWikiDialog} addWiki={this.addWiki} />
|
||||
</ModalPortal>
|
||||
}
|
||||
<div className="main-panel-center">
|
||||
<div className="cur-view-container" id="wikis">
|
||||
<div className="cur-view-path">
|
||||
@ -141,35 +160,20 @@ class Wikis extends Component {
|
||||
{(this.state.loading || this.state.wikis.length !== 0) &&
|
||||
<WikiListView
|
||||
data={this.state}
|
||||
renameWiki={this.renameWiki}
|
||||
deleteWiki={this.deleteWiki}
|
||||
/>
|
||||
}
|
||||
{(!this.state.loading && this.state.wikis.length === 0) &&
|
||||
<EmptyTip>
|
||||
<h2>{gettext('No published libraries')}</h2>
|
||||
<p>{gettext('You have not published any libraries yet. A published library can be accessed by anyone, not only users, via its URL. You can publish a library by clicking the "Add Wiki" button in the menu bar.')}</p>
|
||||
<h2>{gettext('No Wikis')}</h2>
|
||||
<p>{gettext('You have not any wikis yet.')}</p>
|
||||
<p>{gettext('A wiki can be accessed by anyone, not only users, via its URL.')}</p>
|
||||
<p>{gettext('You can add a wiki by clicking the "Add Wiki" button in the menu bar.')}</p>
|
||||
</EmptyTip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.isShowCreateDialog && (
|
||||
<ModalPortal>
|
||||
<NewWikiDialog
|
||||
toggleCancel={this.onCreateToggle}
|
||||
addWiki={this.addWiki}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isShowSelectDialog && (
|
||||
<ModalPortal>
|
||||
<WikiSelectDialog
|
||||
toggleCancel={this.onSelectToggle}
|
||||
addWiki={this.addWiki}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -110,6 +110,7 @@ export const enableSeaTableIntegration = window.app.pageOptions.enableSeaTableIn
|
||||
|
||||
// wiki
|
||||
export const slug = window.wiki ? window.wiki.config.slug : '';
|
||||
export const wikiId = window.wiki ? window.wiki.config.wikiId : '';
|
||||
export const repoID = window.wiki ? window.wiki.config.repoId : '';
|
||||
export const initialPath = window.wiki ? window.wiki.config.initial_path : '';
|
||||
export const permission = window.wiki ? window.wiki.config.permission === 'True' : '';
|
||||
|
@ -1,25 +1,8 @@
|
||||
import { slug, repoID, historyRepoID } from './constants';
|
||||
import { repoID, historyRepoID } from './constants';
|
||||
import { seafileAPI } from './seafile-api';
|
||||
|
||||
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,
|
||||
permission: item.permission,
|
||||
size: item.size
|
||||
};
|
||||
});
|
||||
return files;
|
||||
});
|
||||
}
|
||||
|
||||
listRepoDir() {
|
||||
return seafileAPI.listDir(repoID, '/',{recursive: true}).then(items => {
|
||||
const files = items.data.dirent_list.map(item => {
|
||||
|
@ -45,20 +45,20 @@ class WikiAPI {
|
||||
}
|
||||
}
|
||||
|
||||
listWikiDir(slug, dirPath, withParents) {
|
||||
listWikiDir(wikiId, dirPath, withParents) {
|
||||
const path = encodeURIComponent(dirPath);
|
||||
let url = this.server + '/api/v2.1/wikis/' + encodeURIComponent(slug) + '/dir/?p=' + path;
|
||||
let url = this.server + '/api/v2.1/wikis/' + wikiId + '/dir/?p=' + path;
|
||||
if (withParents) {
|
||||
url = this.server + '/api/v2.1/wikis/' + encodeURIComponent(slug) + '/dir/?p=' + path + '&with_parents=' + withParents;
|
||||
url = this.server + '/api/v2.1/wikis/' + wikiId + '/dir/?p=' + path + '&with_parents=' + withParents;
|
||||
}
|
||||
return this.req.get(url);
|
||||
}
|
||||
|
||||
|
||||
getWikiFileContent(slug, filePath) {
|
||||
getWikiFileContent(wikiId, filePath) {
|
||||
const path = encodeURIComponent(filePath);
|
||||
const time = new Date().getTime();
|
||||
const url = this.server + '/api/v2.1/wikis/' + encodeURIComponent(slug) + '/content/' + '?p=' + path + '&_=' + time;
|
||||
const url = this.server + '/api/v2.1/wikis/' + wikiId + '/content/' + '?p=' + path + '&_=' + time;
|
||||
return this.req.get(url);
|
||||
}
|
||||
|
||||
@ -92,43 +92,89 @@ class WikiAPI {
|
||||
});
|
||||
}
|
||||
|
||||
addWiki(repoID) {
|
||||
addWiki(wikiName) {
|
||||
const url = this.server + '/api/v2.1/wikis/';
|
||||
let form = new FormData();
|
||||
form.append('repo_id', repoID);
|
||||
form.append('name', wikiName);
|
||||
return this._sendPostRequest(url, form);
|
||||
}
|
||||
|
||||
renameWiki(slug, name) {
|
||||
const url = this.server + '/api/v2.1/wikis/' + slug + '/';
|
||||
let form = new FormData();
|
||||
form.append('wiki_name', name);
|
||||
return this._sendPostRequest(url, form);
|
||||
}
|
||||
|
||||
updateWikiPermission(wikiSlug, permission) {
|
||||
const url = this.server + '/api/v2.1/wikis/' + wikiSlug + '/';
|
||||
let params = {
|
||||
permission: permission
|
||||
};
|
||||
return this.req.put(url, params);
|
||||
}
|
||||
|
||||
deleteWiki(slug) {
|
||||
const url = this.server + '/api/v2.1/wikis/' + slug + '/';
|
||||
deleteWiki(wikiId) {
|
||||
const url = this.server + '/api/v2.1/wikis/' + wikiId + '/';
|
||||
return this.req.delete(url);
|
||||
}
|
||||
|
||||
updateWikiConfig(wikiSlug, wikiConfig) {
|
||||
const url = this.server + '/api/v2.1/wiki-config/' + wikiSlug + '/';
|
||||
|
||||
// for wiki2
|
||||
listWiki2Dir(wikiId, dirPath, withParents) {
|
||||
const path = encodeURIComponent(dirPath);
|
||||
let url = this.server + '/api/v2.1/wikis2/' + wikiId + '/dir/?p=' + path;
|
||||
if (withParents) {
|
||||
url = this.server + '/api/v2.1/wikis2/' + wikiId + '/dir/?p=' + path + '&with_parents=' + withParents;
|
||||
}
|
||||
return this.req.get(url);
|
||||
}
|
||||
|
||||
|
||||
getWiki2FileContent(wikiId, filePath) {
|
||||
const path = encodeURIComponent(filePath);
|
||||
const time = new Date().getTime();
|
||||
const url = this.server + '/api/v2.1/wikis2/' + wikiId + '/content/' + '?p=' + path + '&_=' + time;
|
||||
return this.req.get(url);
|
||||
}
|
||||
|
||||
|
||||
listWikis2(options) {
|
||||
/*
|
||||
* options: `{type: 'shared'}`, `{type: ['mine', 'shared', ...]}`
|
||||
*/
|
||||
let url = this.server + '/api/v2.1/wikis2/';
|
||||
if (!options) {
|
||||
// fetch all types of wikis
|
||||
return this.req.get(url);
|
||||
}
|
||||
return this.req.get(url, {
|
||||
params: options,
|
||||
paramsSerializer: {
|
||||
serialize: function(params) {
|
||||
let list = [];
|
||||
for (let key in params) {
|
||||
if (Array.isArray(params[key])) {
|
||||
for (let i = 0, len = params[key].length; i < len; i++) {
|
||||
list.push(key + '=' + encodeURIComponent(params[key][i]));
|
||||
}
|
||||
} else {
|
||||
list.push(key + '=' + encodeURIComponent(params[key]));
|
||||
}
|
||||
}
|
||||
return list.join('&');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addWiki2(wikiName) {
|
||||
const url = this.server + '/api/v2.1/wikis2/';
|
||||
let form = new FormData();
|
||||
form.append('name', wikiName);
|
||||
return this._sendPostRequest(url, form);
|
||||
}
|
||||
|
||||
deleteWiki2(wikiId) {
|
||||
const url = this.server + '/api/v2.1/wikis2/' + wikiId + '/';
|
||||
return this.req.delete(url);
|
||||
}
|
||||
|
||||
updateWiki2Config(wikiId, wikiConfig) {
|
||||
const url = this.server + '/api/v2.1/wiki2-config/' + wikiId + '/';
|
||||
let params = {
|
||||
wiki_config: wikiConfig
|
||||
};
|
||||
return this.req.put(url, params);
|
||||
}
|
||||
|
||||
getWikiConfig(wikiSlug) {
|
||||
const url = this.server + '/api/v2.1/wiki-config/' + wikiSlug + '/';
|
||||
getWiki2Config(wikiId) {
|
||||
const url = this.server + '/api/v2.1/wiki2-config/' + wikiId + '/';
|
||||
return this.req.get(url);
|
||||
}
|
||||
|
||||
|
5
frontend/src/wiki2.js
Normal file
5
frontend/src/wiki2.js
Normal file
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
import Wiki from './pages/wiki2';
|
||||
|
||||
ReactDom.render(<Wiki />, document.getElementById('wrapper'));
|
398
seahub/api2/endpoints/wiki2.py
Normal file
398
seahub/api2/endpoints/wiki2.py
Normal file
@ -0,0 +1,398 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
import posixpath
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated, IsAuthenticated, IsAuthenticatedOrReadOnly
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from seaserv import seafile_api, edit_repo
|
||||
from pysearpc import SearpcError
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.utils import api_error, to_python_boolean
|
||||
from seahub.wiki2.models import Wiki2 as Wiki
|
||||
from seahub.wiki2.utils import is_valid_wiki_name, can_edit_wiki, get_wiki_dirs_by_path
|
||||
from seahub.utils import is_org_context, get_user_repos, gen_inner_file_get_url, gen_file_upload_url, normalize_dir_path
|
||||
from seahub.views import check_folder_permission
|
||||
from seahub.views.file import send_file_access_msg
|
||||
from seahub.base.templatetags.seahub_tags import email2nickname
|
||||
|
||||
|
||||
WIKI_CONFIG_PATH = '_Internal/Wiki'
|
||||
WIKI_CONFIG_FILE_NAME = 'index.json'
|
||||
HTTP_520_OPERATION_FAILED = 520
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Wikis2View(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def get(self, request, format=None):
|
||||
"""List all wikis.
|
||||
"""
|
||||
# parse request params
|
||||
filter_by = {
|
||||
'mine': False,
|
||||
'shared': False,
|
||||
'group': False,
|
||||
'org': False,
|
||||
}
|
||||
|
||||
rtype = request.GET.get('type', "")
|
||||
if not rtype:
|
||||
# set all to True, no filter applied
|
||||
filter_by = filter_by.fromkeys(iter(filter_by.keys()), True)
|
||||
|
||||
for f in rtype.split(','):
|
||||
f = f.strip()
|
||||
filter_by[f] = True
|
||||
|
||||
username = request.user.username
|
||||
org_id = request.user.org.org_id if is_org_context(request) else None
|
||||
(owned, shared, groups, public) = get_user_repos(username, org_id)
|
||||
|
||||
filter_repo_ids = []
|
||||
if filter_by['mine']:
|
||||
filter_repo_ids += ([r.id for r in owned])
|
||||
|
||||
if filter_by['shared']:
|
||||
filter_repo_ids += ([r.id for r in shared])
|
||||
|
||||
if filter_by['group']:
|
||||
filter_repo_ids += ([r.id for r in groups])
|
||||
|
||||
if filter_by['org']:
|
||||
filter_repo_ids += ([r.id for r in public])
|
||||
|
||||
filter_repo_ids = list(set(filter_repo_ids))
|
||||
|
||||
wikis = Wiki.objects.filter(repo_id__in=filter_repo_ids)
|
||||
|
||||
wiki_list = []
|
||||
for wiki in wikis:
|
||||
wiki_info = wiki.to_dict()
|
||||
wiki_info['can_edit'] = (username == wiki.username)
|
||||
wiki_list.append(wiki_info)
|
||||
|
||||
return Response({'data': wiki_list})
|
||||
|
||||
def post(self, request, format=None):
|
||||
"""Add a new wiki.
|
||||
"""
|
||||
username = request.user.username
|
||||
|
||||
if not request.user.permissions.can_add_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'You do not have permission to create library.')
|
||||
|
||||
wiki_name = request.data.get("name", None)
|
||||
if not wiki_name:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'wiki name is required.')
|
||||
|
||||
if not is_valid_wiki_name(wiki_name):
|
||||
msg = _('Name can only contain letters, numbers, blank, hyphen or underscore.')
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, msg)
|
||||
|
||||
org_id = -1
|
||||
if is_org_context(request):
|
||||
org_id = request.user.org.org_id
|
||||
|
||||
try:
|
||||
wiki = Wiki.objects.add(wiki_name=wiki_name, username=username, org_id=org_id)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, msg)
|
||||
|
||||
return Response(wiki.to_dict())
|
||||
|
||||
|
||||
class Wiki2View(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def delete(self, request, wiki_id):
|
||||
"""Delete a wiki.
|
||||
"""
|
||||
username = request.user.username
|
||||
try:
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = 'Wiki not found.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
owner = wiki.username
|
||||
if owner != username:
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
wiki.delete()
|
||||
|
||||
repo_id = wiki.repo_id
|
||||
file_name = WIKI_CONFIG_FILE_NAME
|
||||
try:
|
||||
seafile_api.del_file(repo_id, WIKI_CONFIG_PATH,
|
||||
json.dumps([file_name]),
|
||||
request.user.username)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
return Response()
|
||||
|
||||
|
||||
class Wiki2ConfigView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def put(self, request, wiki_id):
|
||||
"""Edit a wiki config
|
||||
"""
|
||||
username = request.user.username
|
||||
|
||||
try:
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = "Wiki not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if not can_edit_wiki(wiki, request.user.username):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
repo_id = wiki.repo_id
|
||||
obj_id = json.dumps({'parent_dir': WIKI_CONFIG_PATH})
|
||||
|
||||
dir_id = seafile_api.get_dir_id_by_path(repo_id, WIKI_CONFIG_PATH)
|
||||
if not dir_id:
|
||||
seafile_api.mkdir_with_parents(repo_id, '/', WIKI_CONFIG_PATH, username)
|
||||
|
||||
token = seafile_api.get_fileserver_access_token(
|
||||
repo_id, obj_id, 'upload-link', username, use_onetime=False)
|
||||
if not token:
|
||||
return None
|
||||
upload_link = gen_file_upload_url(token, 'upload-api')
|
||||
upload_link = upload_link + '?replace=1'
|
||||
|
||||
wiki_config = request.data.get('wiki_config', '{}')
|
||||
|
||||
files = {
|
||||
'file': (WIKI_CONFIG_FILE_NAME, wiki_config)
|
||||
}
|
||||
data = {'parent_dir': WIKI_CONFIG_PATH, 'relative_path': '', 'replace': 1}
|
||||
resp = requests.post(upload_link, files=files, data=data)
|
||||
if not resp.ok:
|
||||
logger.error(resp.text)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
wiki = wiki.to_dict()
|
||||
wiki['wiki_config'] = wiki_config
|
||||
return Response({'wiki': wiki})
|
||||
|
||||
def get(self, request, wiki_id):
|
||||
|
||||
try:
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = "Wiki not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if not can_edit_wiki(wiki, request.user.username):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
path = posixpath.join(WIKI_CONFIG_PATH, WIKI_CONFIG_FILE_NAME)
|
||||
try:
|
||||
repo = seafile_api.get_repo(wiki.repo_id)
|
||||
if not repo:
|
||||
error_msg = "Wiki library not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
except SearpcError:
|
||||
error_msg = _("Internal Server Error")
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
try:
|
||||
file_id = seafile_api.get_file_id_by_path(repo.repo_id, path)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
wiki = wiki.to_dict()
|
||||
if not file_id:
|
||||
wiki['wiki_config'] = '{}'
|
||||
return Response({'wiki': wiki})
|
||||
|
||||
token = seafile_api.get_fileserver_access_token(repo.repo_id, file_id, 'download', request.user.username, use_onetime=True)
|
||||
|
||||
if not token:
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
url = gen_inner_file_get_url(token, WIKI_CONFIG_FILE_NAME)
|
||||
resp = requests.get(url)
|
||||
content = resp.content
|
||||
|
||||
wiki['wiki_config'] = content
|
||||
|
||||
return Response({'wiki': wiki})
|
||||
|
||||
|
||||
class Wiki2PagesDirView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticatedOrReadOnly,)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def get(self, request, wiki_id):
|
||||
"""List all dir files in a wiki.
|
||||
"""
|
||||
try:
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = "Wiki not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
parent_dir = request.GET.get("p", '/')
|
||||
parent_dir = normalize_dir_path(parent_dir)
|
||||
permission = check_folder_permission(request, wiki.repo_id, parent_dir)
|
||||
if not permission:
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
if not permission:
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
try:
|
||||
repo = seafile_api.get_repo(wiki.repo_id)
|
||||
if not repo:
|
||||
error_msg = "Wiki library not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
except SearpcError:
|
||||
error_msg = "Internal Server Error"
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
with_parents = request.GET.get('with_parents', 'false')
|
||||
if with_parents not in ('true', 'false'):
|
||||
error_msg = 'with_parents invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
with_parents = to_python_boolean(with_parents)
|
||||
dir_id = seafile_api.get_dir_id_by_path(repo.repo_id, parent_dir)
|
||||
if not dir_id:
|
||||
error_msg = 'Folder %s not found.' % parent_dir
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
parent_dir_list = []
|
||||
if not with_parents:
|
||||
# only return dirent list in current parent folder
|
||||
parent_dir_list.append(parent_dir)
|
||||
else:
|
||||
# if value of 'p' parameter is '/a/b/c' add with_parents's is 'true'
|
||||
# then return dirent list in '/', '/a', '/a/b' and '/a/b/c'.
|
||||
if parent_dir == '/':
|
||||
parent_dir_list.append(parent_dir)
|
||||
else:
|
||||
tmp_parent_dir = '/'
|
||||
parent_dir_list.append(tmp_parent_dir)
|
||||
for folder_name in parent_dir.strip('/').split('/'):
|
||||
tmp_parent_dir = posixpath.join(tmp_parent_dir, folder_name)
|
||||
parent_dir_list.append(tmp_parent_dir)
|
||||
|
||||
all_dirs_info = []
|
||||
for parent_dir in parent_dir_list:
|
||||
all_dirs = get_wiki_dirs_by_path(repo.repo_id, parent_dir, [])
|
||||
all_dirs_info += all_dirs
|
||||
|
||||
return Response({
|
||||
"dirent_list": all_dirs_info
|
||||
})
|
||||
|
||||
|
||||
class Wiki2PageContentView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticatedOrReadOnly,)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def get(self, request, wiki_id):
|
||||
"""Get content of a wiki
|
||||
"""
|
||||
path = request.GET.get('p', '/')
|
||||
try:
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = "Wiki not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
parent_dir = os.path.dirname(path)
|
||||
permission = check_folder_permission(request, wiki.repo_id, parent_dir)
|
||||
|
||||
if not permission:
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
try:
|
||||
repo = seafile_api.get_repo(wiki.repo_id)
|
||||
if not repo:
|
||||
error_msg = "Wiki library not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
except SearpcError:
|
||||
error_msg = _("Internal Server Error")
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
file_id = None
|
||||
try:
|
||||
file_id = seafile_api.get_file_id_by_path(repo.repo_id, path)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
return api_error(HTTP_520_OPERATION_FAILED,
|
||||
"Failed to get file id by path.")
|
||||
if not file_id:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, "File not found")
|
||||
|
||||
# send stats message
|
||||
send_file_access_msg(request, repo, path, 'api')
|
||||
|
||||
file_name = os.path.basename(path)
|
||||
token = seafile_api.get_fileserver_access_token(repo.repo_id,
|
||||
file_id, 'download', request.user.username, 'False')
|
||||
|
||||
if not token:
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
url = gen_inner_file_get_url(token, file_name)
|
||||
file_response = urllib.request.urlopen(url)
|
||||
content = file_response.read()
|
||||
|
||||
try:
|
||||
dirent = seafile_api.get_dirent_by_path(repo.repo_id, path)
|
||||
if dirent:
|
||||
latest_contributor, last_modified = dirent.modifier, dirent.mtime
|
||||
else:
|
||||
latest_contributor, last_modified = None, 0
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
latest_contributor, last_modified = None, 0
|
||||
|
||||
return Response({
|
||||
"content": content,
|
||||
"latest_contributor": email2nickname(latest_contributor),
|
||||
"last_modified": last_modified,
|
||||
"permission": permission,
|
||||
})
|
@ -36,11 +36,11 @@ class WikiPagesDirView(APIView):
|
||||
permission_classes = (IsAuthenticatedOrReadOnly, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def get(self, request, slug):
|
||||
def get(self, request, wiki_id):
|
||||
"""List all dir files in a wiki.
|
||||
"""
|
||||
try:
|
||||
wiki = Wiki.objects.get(slug=slug)
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = "Wiki not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
@ -105,12 +105,12 @@ class WikiPageContentView(APIView):
|
||||
permission_classes = (IsAuthenticatedOrReadOnly, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def get(self, request, slug):
|
||||
def get(self, request, wiki_id):
|
||||
"""Get content of a wiki
|
||||
"""
|
||||
path = request.GET.get('p', '/')
|
||||
try:
|
||||
wiki = Wiki.objects.get(slug=slug)
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = "Wiki not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
@ -28,10 +28,6 @@ from seahub.share.utils import is_repo_admin
|
||||
from seahub.share.models import FileShare
|
||||
|
||||
|
||||
WIKI_CONFIG_PATH = '_Internal/Wiki'
|
||||
WIKI_CONFIG_FILE_NAME = 'index.json'
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -78,16 +74,10 @@ class WikisView(APIView):
|
||||
filter_repo_ids += ([r.id for r in public])
|
||||
|
||||
filter_repo_ids = list(set(filter_repo_ids))
|
||||
ret = [x.to_dict() for x in Wiki.objects.filter(
|
||||
repo_id__in=filter_repo_ids)]
|
||||
|
||||
wikis = Wiki.objects.filter(repo_id__in=filter_repo_ids)
|
||||
|
||||
wiki_list = []
|
||||
for wiki in wikis:
|
||||
wiki_info = wiki.to_dict()
|
||||
wiki_info['can_edit'] = (username == wiki.username)
|
||||
wiki_list.append(wiki_info)
|
||||
|
||||
return Response({'data': wiki_list})
|
||||
return Response({'data': ret})
|
||||
|
||||
def post(self, request, format=None):
|
||||
"""Add a new wiki.
|
||||
@ -163,12 +153,12 @@ class WikiView(APIView):
|
||||
permission_classes = (IsAuthenticated, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def delete(self, request, slug):
|
||||
def delete(self, request, wiki_id):
|
||||
"""Delete a wiki.
|
||||
"""
|
||||
username = request.user.username
|
||||
try:
|
||||
wiki = Wiki.objects.get(slug=slug)
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = 'Wiki not found.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
@ -177,29 +167,17 @@ class WikiView(APIView):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
Wiki.objects.filter(slug=slug).delete()
|
||||
|
||||
# file_name = os.path.basename(path)
|
||||
repo_id = wiki.repo_id
|
||||
file_name = WIKI_CONFIG_FILE_NAME
|
||||
try:
|
||||
seafile_api.del_file(repo_id, WIKI_CONFIG_PATH,
|
||||
json.dumps([file_name]),
|
||||
request.user.username)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
wiki.delete()
|
||||
|
||||
return Response()
|
||||
|
||||
def put(self, request, slug):
|
||||
def put(self, request, wiki_id):
|
||||
"""Edit a wiki permission
|
||||
"""
|
||||
username = request.user.username
|
||||
|
||||
try:
|
||||
wiki = Wiki.objects.get(slug=slug)
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = "Wiki not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
@ -217,13 +195,13 @@ class WikiView(APIView):
|
||||
wiki.save()
|
||||
return Response(wiki.to_dict())
|
||||
|
||||
def post(self, request, slug):
|
||||
def post(self, request, wiki_id):
|
||||
"""Rename a Wiki
|
||||
"""
|
||||
username = request.user.username
|
||||
|
||||
try:
|
||||
wiki = Wiki.objects.get(slug=slug)
|
||||
wiki = Wiki.objects.get(id=wiki_id)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = _("Wiki not found.")
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
@ -257,102 +235,3 @@ class WikiView(APIView):
|
||||
"Unable to rename wiki")
|
||||
|
||||
return Response(wiki.to_dict())
|
||||
|
||||
|
||||
class WikiConfigView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def put(self, request, slug):
|
||||
"""Edit a wiki config
|
||||
"""
|
||||
username = request.user.username
|
||||
|
||||
try:
|
||||
wiki = Wiki.objects.get(slug=slug)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = "Wiki not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if wiki.username != username:
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
repo_id = wiki.repo_id
|
||||
obj_id = json.dumps({'parent_dir': WIKI_CONFIG_PATH})
|
||||
|
||||
dir_id = seafile_api.get_dir_id_by_path(repo_id, WIKI_CONFIG_PATH)
|
||||
if not dir_id:
|
||||
seafile_api.mkdir_with_parents(repo_id, '/', WIKI_CONFIG_PATH, username)
|
||||
|
||||
token = seafile_api.get_fileserver_access_token(
|
||||
repo_id, obj_id, 'upload-link', username, use_onetime=False)
|
||||
if not token:
|
||||
return None
|
||||
upload_link = gen_file_upload_url(token, 'upload-api')
|
||||
upload_link = upload_link + '?replace=1'
|
||||
|
||||
wiki_config = request.data.get('wiki_config', '{}')
|
||||
|
||||
files = {
|
||||
'file': (WIKI_CONFIG_FILE_NAME, wiki_config)
|
||||
}
|
||||
data = {'parent_dir': WIKI_CONFIG_PATH, 'relative_path': '', 'replace': 1}
|
||||
resp = requests.post(upload_link, files=files, data=data)
|
||||
if not resp.ok:
|
||||
logger.error(resp.text)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
wiki = wiki.to_dict()
|
||||
wiki['wiki_config'] = wiki_config
|
||||
return Response({'wiki': wiki})
|
||||
|
||||
def get(self, request, slug):
|
||||
|
||||
try:
|
||||
wiki = Wiki.objects.get(slug=slug)
|
||||
except Wiki.DoesNotExist:
|
||||
error_msg = "Wiki not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if not wiki.has_read_perm(request):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
path = posixpath.join(WIKI_CONFIG_PATH, WIKI_CONFIG_FILE_NAME)
|
||||
try:
|
||||
repo = seafile_api.get_repo(wiki.repo_id)
|
||||
if not repo:
|
||||
error_msg = "Wiki library not found."
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
except SearpcError:
|
||||
error_msg = _("Internal Server Error")
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
try:
|
||||
file_id = seafile_api.get_file_id_by_path(repo.repo_id, path)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
wiki = wiki.to_dict()
|
||||
if not file_id:
|
||||
wiki['wiki_config'] = '{}'
|
||||
return Response({'wiki': wiki})
|
||||
|
||||
token = seafile_api.get_fileserver_access_token(repo.repo_id, file_id, 'download', request.user.username, use_onetime=True)
|
||||
|
||||
if not token:
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
url = gen_inner_file_get_url(token, WIKI_CONFIG_FILE_NAME)
|
||||
resp = requests.get(url)
|
||||
content = resp.content
|
||||
|
||||
wiki['wiki_config'] = content
|
||||
|
||||
return Response({'wiki': wiki})
|
||||
|
@ -245,6 +245,7 @@ INSTALLED_APPS = [
|
||||
'seahub.institutions',
|
||||
'seahub.invitations',
|
||||
'seahub.wiki',
|
||||
'seahub.wiki2',
|
||||
'seahub.group',
|
||||
'seahub.notifications',
|
||||
'seahub.options',
|
||||
|
@ -40,6 +40,7 @@
|
||||
window.wiki = {
|
||||
config: {
|
||||
slug: "{{ wiki.slug }}",
|
||||
wikiId: "{{ wiki.id }}",
|
||||
repoId: "{{ wiki.repo_id }}",
|
||||
sharedToken: "{{ shared_token }}",
|
||||
sharedType: "{{ shared_type }}",
|
||||
|
@ -39,16 +39,12 @@
|
||||
<script type="text/javascript">
|
||||
window.wiki = {
|
||||
config: {
|
||||
slug: "{{ wiki.slug }}",
|
||||
wikiId: "{{ wiki.id }}",
|
||||
repoId: "{{ wiki.repo_id }}",
|
||||
repoName: "{{ wiki.repo_name }}",
|
||||
sharedToken: "{{ shared_token }}",
|
||||
sharedType: "{{ shared_type }}",
|
||||
initial_path: "{{ file_path|escapejs }}",
|
||||
permission: "{{ user_can_write }}",
|
||||
isPublicWiki: "{{ is_public_wiki }}",
|
||||
isDir: "{{ is_dir }}",
|
||||
hasIndex: {% if has_index %} true {% else %} false {% endif %},
|
||||
isEditWiki: true,
|
||||
assetsUrl: "{{ assets_url }}"
|
||||
}
|
||||
@ -123,5 +119,5 @@
|
||||
updateOutline(0);
|
||||
</script>
|
||||
|
||||
{% render_bundle 'wiki' 'js' %}
|
||||
{% render_bundle 'wiki2' 'js' %}
|
||||
{% endblock %}
|
||||
|
@ -86,7 +86,7 @@ from seahub.api2.endpoints.repo_share_invitation import RepoShareInvitationView
|
||||
from seahub.api2.endpoints.notifications import NotificationsView, NotificationView
|
||||
from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView
|
||||
from seahub.api2.endpoints.user_avatar import UserAvatarView
|
||||
from seahub.api2.endpoints.wikis import WikisView, WikiView, WikiConfigView
|
||||
from seahub.api2.endpoints.wikis import WikisView, WikiView
|
||||
from seahub.api2.endpoints.drafts import DraftsView, DraftView
|
||||
from seahub.api2.endpoints.draft_reviewer import DraftReviewerView
|
||||
from seahub.api2.endpoints.repo_draft_info import RepoDraftInfo, RepoDraftCounts
|
||||
@ -203,7 +203,8 @@ from seahub.ocm.settings import OCM_ENDPOINT
|
||||
|
||||
from seahub.ai.apis import LibrarySdocIndexes, Search, LibrarySdocIndex, TaskStatus, \
|
||||
LibraryIndexState, QuestionAnsweringSearchInLibrary, FileDownloadToken
|
||||
from seahub.wiki.views import edit_slug
|
||||
from seahub.wiki2.views import edit_wiki
|
||||
from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2PagesDirView, Wiki2PageContentView
|
||||
|
||||
urlpatterns = [
|
||||
path('accounts/', include('seahub.base.registration_urls')),
|
||||
@ -512,12 +513,18 @@ urlpatterns = [
|
||||
|
||||
## user::wiki
|
||||
re_path(r'^api/v2.1/wikis/$', WikisView.as_view(), name='api-v2.1-wikis'),
|
||||
re_path(r'^api/v2.1/wikis/(?P<slug>[^/]+)/$', WikiView.as_view(), name='api-v2.1-wiki'),
|
||||
re_path(r'^api/v2.1/wikis/(?P<slug>[^/]+)/dir/$', WikiPagesDirView.as_view(), name='api-v2.1-wiki-pages-dir'),
|
||||
re_path(r'^api/v2.1/wiki-config/(?P<slug>[^/]+)/$', WikiConfigView.as_view(), name='api-v2.1-wiki-config'),
|
||||
re_path(r'^api/v2.1/wikis/(?P<slug>[^/]+)/content/$', WikiPageContentView.as_view(), name='api-v2.1-wiki-pages-content'),
|
||||
re_path(r'^api/v2.1/wikis/(?P<wiki_id>\d+)/$', WikiView.as_view(), name='api-v2.1-wiki'),
|
||||
re_path(r'^api/v2.1/wikis/(?P<wiki_id>\d+)/dir/$', WikiPagesDirView.as_view(), name='api-v2.1-wiki-pages-dir'),
|
||||
re_path(r'^api/v2.1/wikis/(?P<wiki_id>\d+)/content/$', WikiPageContentView.as_view(), name='api-v2.1-wiki-pages-content'),
|
||||
path('view-image-via-public-wiki/', view_media_file_via_public_wiki, name='view_media_file_via_public_wiki'),
|
||||
|
||||
## user::wiki2
|
||||
re_path(r'^api/v2.1/wikis2/$', Wikis2View.as_view(), name='api-v2.1-wikis2'),
|
||||
re_path(r'^api/v2.1/wikis2/(?P<wiki_id>\d+)/$', Wiki2View.as_view(), name='api-v2.1-wiki2'),
|
||||
re_path(r'^api/v2.1/wikis2/(?P<wiki_id>\d+)/dir/$', Wiki2PagesDirView.as_view(), name='api-v2.1-wiki2-pages-dir'),
|
||||
re_path(r'^api/v2.1/wiki2-config/(?P<wiki_id>\d+)/$', Wiki2ConfigView.as_view(), name='api-v2.1-wiki2-config'),
|
||||
re_path(r'^api/v2.1/wikis2/(?P<wiki_id>\d+)/content/$', Wiki2PageContentView.as_view(), name='api-v2.1-wiki2-pages-content'),
|
||||
|
||||
## user::drafts
|
||||
re_path(r'^api/v2.1/drafts/$', DraftsView.as_view(), name='api-v2.1-drafts'),
|
||||
re_path(r'^api/v2.1/drafts/(?P<pk>\d+)/$', DraftView.as_view(), name='api-v2.1-draft'),
|
||||
@ -699,7 +706,7 @@ urlpatterns = [
|
||||
re_path(r'^api/v2.1/admin/invitations/$', AdminInvitations.as_view(), name='api-v2.1-admin-invitations'),
|
||||
re_path(r'^api/v2.1/admin/invitations/(?P<token>[a-f0-9]{32})/$', AdminInvitation.as_view(), name='api-v2.1-admin-invitation'),
|
||||
|
||||
re_path(r'^edit-wiki/(?P<slug>[^/]+)/(?P<file_path>.*)$', edit_slug, name='edit_slug'),
|
||||
re_path(r'^edit-wiki/(?P<wiki_id>[^/]+)/(?P<file_path>.*)$', edit_wiki, name='edit_wiki'),
|
||||
|
||||
path('avatar/', include('seahub.avatar.urls')),
|
||||
path('notice/', include('seahub.notifications.urls')),
|
||||
|
@ -20,6 +20,7 @@ from seahub.utils import gen_file_get_url, get_file_type_and_ext, \
|
||||
from seahub.utils.file_types import IMAGE
|
||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||
from .models import WikiPageMissing, WikiDoesNotExist
|
||||
from seahub.constants import PERMISSION_READ_WRITE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -261,145 +261,6 @@ def slug(request, slug, file_path="home.md"):
|
||||
"assets_url": assets_url,
|
||||
})
|
||||
|
||||
def edit_slug(request, slug, file_path="home.md"):
|
||||
""" edit wiki page.
|
||||
"""
|
||||
# get wiki object or 404
|
||||
wiki = get_object_or_404(Wiki, slug=slug)
|
||||
file_path = "/" + file_path
|
||||
|
||||
# only wiki owner can edit wiki app
|
||||
if not (request.user.username == wiki.username):
|
||||
return render_permission_error(request, 'Permission denied.')
|
||||
|
||||
is_dir = None
|
||||
file_id = seafile_api.get_file_id_by_path(wiki.repo_id, file_path)
|
||||
if file_id:
|
||||
is_dir = False
|
||||
|
||||
dir_id = seafile_api.get_dir_id_by_path(wiki.repo_id, file_path)
|
||||
if dir_id:
|
||||
is_dir = True
|
||||
|
||||
# compatible with old wiki url
|
||||
if is_dir is None:
|
||||
if len(file_path.split('.')) == 1:
|
||||
new_path = file_path[1:] + '.md'
|
||||
url = reverse('edit_slug', args=[slug, new_path])
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# perm check
|
||||
req_user = request.user.username
|
||||
|
||||
if not req_user and not wiki.has_read_perm(request):
|
||||
return redirect('auth_login')
|
||||
else:
|
||||
if not wiki.has_read_perm(request):
|
||||
return render_permission_error(request, _('Unable to view Wiki'))
|
||||
|
||||
file_type, ext = get_file_type_and_ext(posixpath.basename(file_path))
|
||||
if file_type == IMAGE:
|
||||
file_url = reverse('view_lib_file', args=[wiki.repo_id, file_path])
|
||||
return HttpResponseRedirect(file_url + "?raw=1")
|
||||
|
||||
if not req_user:
|
||||
user_can_write = False
|
||||
elif req_user == wiki.username or check_folder_permission(
|
||||
request, wiki.repo_id, '/') == 'rw':
|
||||
user_can_write = True
|
||||
else:
|
||||
user_can_write = False
|
||||
|
||||
is_public_wiki = False
|
||||
if wiki.permission == 'public':
|
||||
is_public_wiki = True
|
||||
|
||||
has_index = False
|
||||
dirs = seafile_api.list_dir_by_path(wiki.repo_id, '/')
|
||||
for dir_obj in dirs:
|
||||
if dir_obj.obj_name == 'index.md':
|
||||
has_index = True
|
||||
break
|
||||
|
||||
try:
|
||||
fs = FileShare.objects.filter(repo_id=wiki.repo_id, path='/').first()
|
||||
except FileShare.DoesNotExist:
|
||||
fs = FileShare.objects.create_dir_link(wiki.username, wiki.repo_id, '/',
|
||||
permission='view_download')
|
||||
wiki.permission = 'public'
|
||||
wiki.save()
|
||||
is_public_wiki = True
|
||||
|
||||
repo = seafile_api.get_repo(wiki.repo_id)
|
||||
|
||||
file_content = ''
|
||||
h1_head_content = ''
|
||||
outlines = []
|
||||
latest_contributor = ''
|
||||
last_modified = 0
|
||||
assets_url = ''
|
||||
|
||||
if is_dir is False and file_type == MARKDOWN:
|
||||
send_file_access_msg(request, repo, file_path, 'web')
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
token = seafile_api.get_fileserver_access_token(
|
||||
repo.repo_id, file_id, 'download', request.user.username, 'False')
|
||||
if not token:
|
||||
return render_error(request, _('Internal Server Error'))
|
||||
|
||||
url = gen_inner_file_get_url(token, file_name)
|
||||
try:
|
||||
file_response = urllib.request.urlopen(url).read().decode()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return render_error(request, _('Internal Server Error'))
|
||||
|
||||
err_msg = None
|
||||
if file_response:
|
||||
file_content, h1_head_content, outlines, err_msg = format_markdown_file_content(
|
||||
slug, wiki.repo_id, file_path, fs.token, file_response)
|
||||
|
||||
if err_msg:
|
||||
logger.error(err_msg)
|
||||
return render_error(request, _('Internal Server Error'))
|
||||
|
||||
try:
|
||||
dirent = seafile_api.get_dirent_by_path(wiki.repo_id, file_path)
|
||||
if dirent:
|
||||
latest_contributor, last_modified = dirent.modifier, dirent.mtime
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
if is_dir is False and file_type == SEADOC:
|
||||
file_uuid = get_seadoc_file_uuid(repo, file_path)
|
||||
assets_url = '/api/v2.1/seadoc/download-image/' + file_uuid
|
||||
|
||||
last_modified = datetime.fromtimestamp(last_modified)
|
||||
|
||||
return render(request, "wiki/wiki_edit.html", {
|
||||
"wiki": wiki,
|
||||
"repo_name": repo.name if repo else '',
|
||||
"page_name": file_path,
|
||||
"shared_token": fs.token,
|
||||
"shared_type": fs.s_type,
|
||||
"user_can_write": user_can_write,
|
||||
"file_path": file_path,
|
||||
"filename": os.path.splitext(os.path.basename(file_path))[0],
|
||||
"h1_head_content": h1_head_content,
|
||||
"file_content": file_content,
|
||||
"outlines": outlines,
|
||||
"modifier": latest_contributor,
|
||||
"modify_time": last_modified,
|
||||
"repo_id": wiki.repo_id,
|
||||
"search_repo_id": wiki.repo_id,
|
||||
"search_wiki": True,
|
||||
"is_public_wiki": is_public_wiki,
|
||||
"is_dir": is_dir,
|
||||
"has_index": has_index,
|
||||
"assets_url": assets_url,
|
||||
})
|
||||
|
||||
|
||||
'''
|
||||
@login_required
|
||||
|
0
seahub/wiki2/__init__.py
Normal file
0
seahub/wiki2/__init__.py
Normal file
77
seahub/wiki2/models.py
Normal file
77
seahub/wiki2/models.py
Normal file
@ -0,0 +1,77 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from seaserv import seafile_api
|
||||
|
||||
from seahub.base.fields import LowerCaseCharField
|
||||
from seahub.base.templatetags.seahub_tags import email2nickname
|
||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
|
||||
|
||||
|
||||
class WikiDoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class WikiManager(models.Manager):
|
||||
def add(self, wiki_name, username, org_id=-1):
|
||||
now = timezone.now()
|
||||
if org_id and org_id > 0:
|
||||
repo_id = seafile_api.create_org_repo(wiki_name, '', username, org_id)
|
||||
else:
|
||||
repo_id = seafile_api.create_repo(wiki_name, '', username)
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
assert repo is not None
|
||||
|
||||
wiki = self.model(username=username, name=wiki_name, repo_id=repo.id, created_at=now)
|
||||
wiki.save(using=self._db)
|
||||
return wiki
|
||||
|
||||
|
||||
class Wiki2(models.Model):
|
||||
"""New wiki model to enable a user has multiple wikis and replace
|
||||
personal wiki.
|
||||
"""
|
||||
|
||||
username = LowerCaseCharField(max_length=255)
|
||||
name = models.CharField(max_length=255)
|
||||
repo_id = models.CharField(max_length=36, db_index=True)
|
||||
created_at = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
objects = WikiManager()
|
||||
|
||||
class Meta:
|
||||
db_table = 'wiki_wiki2'
|
||||
unique_together = (('username', 'repo_id'),)
|
||||
ordering = ["name"]
|
||||
|
||||
@property
|
||||
def updated_at(self):
|
||||
assert len(self.repo_id) == 36
|
||||
|
||||
repo = seafile_api.get_repo(self.repo_id)
|
||||
if not repo:
|
||||
return ''
|
||||
|
||||
return repo.last_modify
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.pk,
|
||||
'owner': self.username,
|
||||
'owner_nickname': email2nickname(self.username),
|
||||
'name': self.name,
|
||||
'created_at': datetime_to_isoformat_timestr(self.created_at),
|
||||
'updated_at': timestamp_to_isoformat_timestr(self.updated_at),
|
||||
'repo_id': self.repo_id,
|
||||
}
|
||||
|
||||
|
||||
###### signal handlers
|
||||
from django.dispatch import receiver
|
||||
from seahub.signals import repo_deleted
|
||||
|
||||
@receiver(repo_deleted)
|
||||
def remove_wiki(sender, **kwargs):
|
||||
repo_id = kwargs['repo_id']
|
||||
|
||||
Wiki2.objects.filter(repo_id=repo_id).delete()
|
43
seahub/wiki2/utils.py
Normal file
43
seahub/wiki2/utils.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import stat
|
||||
import logging
|
||||
|
||||
from seaserv import seafile_api
|
||||
from seahub.constants import PERMISSION_READ_WRITE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_valid_wiki_name(name):
|
||||
name = name.strip()
|
||||
if len(name) > 255 or len(name) < 1:
|
||||
return False
|
||||
return True if re.match('^[\w\s-]+$', name, re.U) else False
|
||||
|
||||
|
||||
def get_wiki_dirs_by_path(repo_id, path, all_dirs):
|
||||
dirs = seafile_api.list_dir_by_path(repo_id, path)
|
||||
|
||||
for dirent in dirs:
|
||||
entry = {}
|
||||
if stat.S_ISDIR(dirent.mode):
|
||||
entry["type"] = 'dir'
|
||||
else:
|
||||
entry["type"] = 'file'
|
||||
|
||||
entry["parent_dir"] = path
|
||||
entry["id"] = dirent.obj_id
|
||||
entry["name"] = dirent.obj_name
|
||||
entry["size"] = dirent.size
|
||||
entry["mtime"] = dirent.mtime
|
||||
|
||||
all_dirs.append(entry)
|
||||
|
||||
return all_dirs
|
||||
|
||||
|
||||
def can_edit_wiki(wiki, username):
|
||||
permission = seafile_api.check_permission_by_path(wiki.repo_id, '/', username)
|
||||
return permission == PERMISSION_READ_WRITE
|
80
seahub/wiki2/views.py
Normal file
80
seahub/wiki2/views.py
Normal file
@ -0,0 +1,80 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
import os
|
||||
import logging
|
||||
import posixpath
|
||||
from datetime import datetime
|
||||
|
||||
from seaserv import seafile_api
|
||||
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
|
||||
from seahub.share.models import FileShare
|
||||
from seahub.wiki2.models import Wiki2 as Wiki
|
||||
from seahub.views import check_folder_permission
|
||||
from seahub.utils import get_file_type_and_ext, render_permission_error
|
||||
from seahub.utils.file_types import IMAGE, SEADOC
|
||||
from seahub.seadoc.utils import get_seadoc_file_uuid
|
||||
from seahub.auth.decorators import login_required
|
||||
from seahub.wiki2.utils import can_edit_wiki
|
||||
|
||||
# Get an instance of a logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_wiki(request, wiki_id, file_path):
|
||||
""" edit wiki page. for wiki2
|
||||
"""
|
||||
# get wiki object or 404
|
||||
wiki = get_object_or_404(Wiki, id=wiki_id)
|
||||
file_path = "/" + file_path
|
||||
|
||||
# perm check
|
||||
req_user = request.user.username
|
||||
if not can_edit_wiki(wiki, req_user):
|
||||
return render_permission_error(request, 'Permission denied.')
|
||||
|
||||
is_dir = None
|
||||
file_id = seafile_api.get_file_id_by_path(wiki.repo_id, file_path)
|
||||
if file_id:
|
||||
is_dir = False
|
||||
|
||||
dir_id = seafile_api.get_dir_id_by_path(wiki.repo_id, file_path)
|
||||
if dir_id:
|
||||
is_dir = True
|
||||
|
||||
file_content = ''
|
||||
outlines = []
|
||||
latest_contributor = ''
|
||||
last_modified = 0
|
||||
assets_url = ''
|
||||
file_type, ext = get_file_type_and_ext(posixpath.basename(file_path))
|
||||
repo = seafile_api.get_repo(wiki.repo_id)
|
||||
if is_dir is False and file_type == SEADOC:
|
||||
file_uuid = get_seadoc_file_uuid(repo, file_path)
|
||||
assets_url = '/api/v2.1/seadoc/download-image/' + file_uuid
|
||||
try:
|
||||
dirent = seafile_api.get_dirent_by_path(wiki.repo_id, file_path)
|
||||
if dirent:
|
||||
latest_contributor, last_modified = dirent.modifier, dirent.mtime
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
last_modified = datetime.fromtimestamp(last_modified)
|
||||
|
||||
return render(request, "wiki/wiki_edit.html", {
|
||||
"wiki": wiki,
|
||||
"repo_name": repo.name if repo else '',
|
||||
"user_can_write": True,
|
||||
"file_path": file_path,
|
||||
"filename": os.path.splitext(os.path.basename(file_path))[0],
|
||||
"file_content": file_content,
|
||||
"outlines": outlines,
|
||||
"modifier": latest_contributor,
|
||||
"modify_time": last_modified,
|
||||
"repo_id": wiki.repo_id,
|
||||
"is_dir": is_dir,
|
||||
"assets_url": assets_url,
|
||||
})
|
@ -105,7 +105,7 @@ class WikiViewTest(BaseTestCase):
|
||||
wiki = Wiki.objects.add('test wiki', self.user.username,
|
||||
repo_id=self.repo.id)
|
||||
|
||||
self.url = reverse('api-v2.1-wiki', args=[wiki.slug])
|
||||
self.url = reverse('api-v2.1-wiki', args=[wiki.id])
|
||||
self.login_as(self.user)
|
||||
|
||||
def test_can_delete(self):
|
||||
|
Loading…
Reference in New Issue
Block a user