From d7ac5688ef566c0e30344ed71bbbc4c46d6a04d1 Mon Sep 17 00:00:00 2001 From: Michael An <2331806369@qq.com> Date: Wed, 15 May 2024 11:57:30 +0800 Subject: [PATCH] 12.0 change add existing file in wiki edit (#6057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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’ --- frontend/config/webpack.entry.js | 1 + ...{new-wiki-dialog.js => add-wiki-dialog.js} | 26 +- .../components/dialog/wiki-select-dialog.js | 110 ---- .../wiki-list-view/wiki-list-item.js | 91 +-- .../wiki-list-view/wiki-list-view.js | 2 - .../src/pages/wiki/css/add-page-dialog.css | 90 --- frontend/src/pages/wiki/index.js | 114 +--- frontend/src/pages/wiki/main-panel.js | 4 +- frontend/src/pages/wiki/side-panel.js | 356 +---------- .../wiki/view-structure/add-page-dialog.js | 233 ------- frontend/src/pages/wiki/wiki.css | 2 - .../{wiki => wiki2}/css/view-edit-popover.css | 0 .../{wiki => wiki2}/css/view-structure.css | 11 +- .../src/pages/wiki2/index-md-viewer/index.js | 114 ++++ .../pages/wiki2/index-md-viewer/nav-item.js | 91 +++ .../src/pages/wiki2/index-md-viewer/style.css | 37 ++ frontend/src/pages/wiki2/index.js | 599 ++++++++++++++++++ frontend/src/pages/wiki2/main-panel.js | 164 +++++ .../pages/{wiki => wiki2}/models/folder.js | 0 .../src/pages/{wiki => wiki2}/models/page.js | 0 .../{wiki => wiki2}/models/wiki-config.js | 0 frontend/src/pages/wiki2/side-panel.js | 417 ++++++++++++ .../src/pages/wiki2/utils/generate-navs.js | 91 +++ .../src/pages/{wiki => wiki2}/utils/index.js | 0 .../view-structure/add-new-page-dialog.js | 138 ++++ .../view-structure/add-view-dropdownmenu.js | 14 +- .../view-structure/constant.js | 0 .../folders/dragged-folder-item.js | 0 .../view-structure/folders/folder-item.js | 0 .../folders/folder-operation-dropdownmenu.js | 2 +- .../view-structure/html5DragDropContext.js | 0 .../{wiki => wiki2}/view-structure/index.js | 0 .../view-structure/new-folder-dialog.js | 0 .../view-structure/page-utils.js | 0 .../view-structure/view-structure-footer.js | 28 +- .../view-structure/view-structure.js | 6 +- .../view-structure/views/delete-dialog.js | 0 .../views/drop-target-top-view.js | 0 .../view-structure/views/page-dropdownmenu.js | 0 .../view-structure/views/view-edit-popover.js | 0 .../view-structure/views/view-item.js | 0 .../app-left-bar-dialog.css | 0 .../app-settings-dialog-custom-icon.js | 0 .../app-settings-dialog-icon-color.css | 0 .../app-settings-dialog-icon-color.js | 0 .../app-settings-dialog-icons.js | 0 .../app-settings-dialog-name.js | 0 .../icon-settings-popover.css | 0 .../icon-settings-popover.js | 0 .../app-settings-dialog/index.js | 0 .../wiki-left-bar/wiki-left-bar-icon.jsx | 0 .../wiki-left-bar/wiki-left-bar.css | 0 .../wiki-left-bar/wiki-left-bar.js | 0 frontend/src/pages/wiki2/wiki.css | 143 +++++ frontend/src/pages/wikis/wikis.js | 138 ++-- frontend/src/utils/constants.js | 1 + frontend/src/utils/editor-utilities.js | 19 +- frontend/src/utils/wiki-api.js | 94 ++- frontend/src/wiki2.js | 5 + seahub/api2/endpoints/wiki2.py | 398 ++++++++++++ seahub/api2/endpoints/wiki_pages.py | 8 +- seahub/api2/endpoints/wikis.py | 141 +---- seahub/settings.py | 1 + seahub/templates/wiki/wiki.html | 1 + seahub/templates/wiki/wiki_edit.html | 8 +- seahub/urls.py | 21 +- seahub/wiki/utils.py | 1 + seahub/wiki/views.py | 139 ---- seahub/wiki2/__init__.py | 0 seahub/wiki2/models.py | 77 +++ seahub/wiki2/utils.py | 43 ++ seahub/wiki2/views.py | 80 +++ tests/api/endpoints/test_wikis.py | 2 +- 73 files changed, 2644 insertions(+), 1417 deletions(-) rename frontend/src/components/dialog/{new-wiki-dialog.js => add-wiki-dialog.js} (69%) delete mode 100644 frontend/src/components/dialog/wiki-select-dialog.js delete mode 100644 frontend/src/pages/wiki/css/add-page-dialog.css delete mode 100644 frontend/src/pages/wiki/view-structure/add-page-dialog.js rename frontend/src/pages/{wiki => wiki2}/css/view-edit-popover.css (100%) rename frontend/src/pages/{wiki => wiki2}/css/view-structure.css (98%) create mode 100644 frontend/src/pages/wiki2/index-md-viewer/index.js create mode 100644 frontend/src/pages/wiki2/index-md-viewer/nav-item.js create mode 100644 frontend/src/pages/wiki2/index-md-viewer/style.css create mode 100644 frontend/src/pages/wiki2/index.js create mode 100644 frontend/src/pages/wiki2/main-panel.js rename frontend/src/pages/{wiki => wiki2}/models/folder.js (100%) rename frontend/src/pages/{wiki => wiki2}/models/page.js (100%) rename frontend/src/pages/{wiki => wiki2}/models/wiki-config.js (100%) create mode 100644 frontend/src/pages/wiki2/side-panel.js create mode 100644 frontend/src/pages/wiki2/utils/generate-navs.js rename frontend/src/pages/{wiki => wiki2}/utils/index.js (100%) create mode 100644 frontend/src/pages/wiki2/view-structure/add-new-page-dialog.js rename frontend/src/pages/{wiki => wiki2}/view-structure/add-view-dropdownmenu.js (80%) rename frontend/src/pages/{wiki => wiki2}/view-structure/constant.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/folders/dragged-folder-item.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/folders/folder-item.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/folders/folder-operation-dropdownmenu.js (98%) rename frontend/src/pages/{wiki => wiki2}/view-structure/html5DragDropContext.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/index.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/new-folder-dialog.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/page-utils.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/view-structure-footer.js (58%) rename frontend/src/pages/{wiki => wiki2}/view-structure/view-structure.js (98%) rename frontend/src/pages/{wiki => wiki2}/view-structure/views/delete-dialog.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/views/drop-target-top-view.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/views/page-dropdownmenu.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/views/view-edit-popover.js (100%) rename frontend/src/pages/{wiki => wiki2}/view-structure/views/view-item.js (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/app-settings-dialog/app-left-bar-dialog.css (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/app-settings-dialog/app-settings-dialog-custom-icon.js (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.css (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.js (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/app-settings-dialog/app-settings-dialog-icons.js (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/app-settings-dialog/app-settings-dialog-name.js (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/app-settings-dialog/icon-settings-popover.css (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/app-settings-dialog/icon-settings-popover.js (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/app-settings-dialog/index.js (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/wiki-left-bar-icon.jsx (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/wiki-left-bar.css (100%) rename frontend/src/pages/{wiki => wiki2}/wiki-left-bar/wiki-left-bar.js (100%) create mode 100644 frontend/src/pages/wiki2/wiki.css create mode 100644 frontend/src/wiki2.js create mode 100644 seahub/api2/endpoints/wiki2.py create mode 100644 seahub/wiki2/__init__.py create mode 100644 seahub/wiki2/models.py create mode 100644 seahub/wiki2/utils.py create mode 100644 seahub/wiki2/views.py diff --git a/frontend/config/webpack.entry.js b/frontend/config/webpack.entry.js index 4f4b1eb78a6..1f1d8645d19 100644 --- a/frontend/config/webpack.entry.js +++ b/frontend/config/webpack.entry.js @@ -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', diff --git a/frontend/src/components/dialog/new-wiki-dialog.js b/frontend/src/components/dialog/add-wiki-dialog.js similarity index 69% rename from frontend/src/components/dialog/new-wiki-dialog.js rename to frontend/src/components/dialog/add-wiki-dialog.js index 2daf50f442a..e76597ac3bf 100644 --- a/frontend/src/components/dialog/new-wiki-dialog.js +++ b/frontend/src/components/dialog/add-wiki-dialog.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 ( - {gettext('New Wiki')} + {gettext('Add Wiki')} - + @@ -65,6 +59,6 @@ class NewWikiDialog extends React.Component { } } -NewWikiDialog.propTypes = propTypes; +AddWikiDialog.propTypes = propTypes; -export default NewWikiDialog; +export default AddWikiDialog; diff --git a/frontend/src/components/dialog/wiki-select-dialog.js b/frontend/src/components/dialog/wiki-select-dialog.js deleted file mode 100644 index 060156cbca6..00000000000 --- a/frontend/src/components/dialog/wiki-select-dialog.js +++ /dev/null @@ -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 ( - - {gettext('Create Wiki from existing library')} - - - - - - - - - - - - {this.state.repos.map((repo, index) => { - return ( - - - - - - - ); - })} - -
{/* select */}{/* icon */}{gettext('Name')}{gettext('Last Update')}
{Utils.getLibIconTitle(repo)}{repo.repo_name}{moment(repo.last_modified).fromNow()}
-
- - - {this.state.repoID ? - : - - } - -
- ); - } -} - -WikiSelectDialog.propTypes = propTypes; - -export default WikiSelectDialog; diff --git a/frontend/src/components/wiki-list-view/wiki-list-item.js b/frontend/src/components/wiki-list-view/wiki-list-item.js index b5f0a468436..86cbd466aba 100644 --- a/frontend/src/components/wiki-list-view/wiki-list-item.js +++ b/frontend/src/components/wiki-list-view/wiki-list-item.js @@ -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 = ( - {wiki.name} - {/*this.state.isRenameing ? - : - {wiki.name} - */} + {isOldVersion && {wiki.name} (old version)} + {!isOldVersion && {wiki.name}} {wiki.owner_nickname} {moment(wiki.updated_at).fromNow()} - {isWikiOwner && - window.open(wiki.link.replace('/published/', '/edit-wiki/'))} - title={gettext('Edit')} - aria-label={gettext('Edit')} - style={{color: '#999', fontSize: '20px'}} - > - } - {wiki.name}
+ {isOldVersion && {wiki.name} (old version)} + {!isOldVersion && {wiki.name}}
{wiki.owner_nickname} {moment(wiki.updated_at).fromNow()} diff --git a/frontend/src/components/wiki-list-view/wiki-list-view.js b/frontend/src/components/wiki-list-view/wiki-list-view.js index f875e575a07..f3c99a980df 100644 --- a/frontend/src/components/wiki-list-view/wiki-list-view.js +++ b/frontend/src/components/wiki-list-view/wiki-list-view.js @@ -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 { 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 (
- {isEditWiki && - this.saveWikiConfig(Object.assign({}, this.state.config, data))} - /> - } {gettext('Folder does not exist.')}
); const isViewingFile = this.props.pathExist && !this.props.isDataLoading && this.props.isViewFile; return ( -
+
{this.props.content}
{!username && diff --git a/frontend/src/pages/wiki/side-panel.js b/frontend/src/pages/wiki/side-panel.js index 98ae713987f..3388a0c90c9 100644 --- a/frontend/src/pages/wiki/side-panel.js +++ b/frontend/src/pages/wiki/side-panel.js @@ -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 (
- {/* {this.props.treeData && ( + {this.props.treeData && ( - )} */} - {isEditWiki && - - } - {this.state.isShowNewFolderDialog && - - } - {this.state.isShowAddPageDialog && - - } + )}
); }; - 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 ( -
- - {this.state.isShowNewFolderDialog && - - } - {this.state.isShowAddPageDialog && - - } -
- ); - }; - - renderContent = () => { - const { isLoading, indexNode, config } = this.props; - if (isLoading) { - return ; - } - 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 (
- {src && } -

{wiki_name || slug}

+
); diff --git a/frontend/src/pages/wiki/view-structure/add-page-dialog.js b/frontend/src/pages/wiki/view-structure/add-page-dialog.js deleted file mode 100644 index df9bcd4f149..00000000000 --- a/frontend/src/pages/wiki/view-structure/add-page-dialog.js +++ /dev/null @@ -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 ( - - {gettext('Add page')} - -
- - - - - - - - - {this.state.selectedOption.value !== 'existing' && - <> - - - - - - - - - - } - {this.state.selectedOption.value === 'existing' && - - - - - } -
-
- - - {this.state.isLoading ? - : - - } - -
- ); - } -} - -AddPageDialog.propTypes = propTypes; - -export default AddPageDialog; diff --git a/frontend/src/pages/wiki/wiki.css b/frontend/src/pages/wiki/wiki.css index f7809d7abda..09498a065f1 100644 --- a/frontend/src/pages/wiki/wiki.css +++ b/frontend/src/pages/wiki/wiki.css @@ -1,7 +1,5 @@ .wiki-side-panel .panel-top { background: #fff; - display: flex; - align-items: center; } .wiki-side-nav { diff --git a/frontend/src/pages/wiki/css/view-edit-popover.css b/frontend/src/pages/wiki2/css/view-edit-popover.css similarity index 100% rename from frontend/src/pages/wiki/css/view-edit-popover.css rename to frontend/src/pages/wiki2/css/view-edit-popover.css diff --git a/frontend/src/pages/wiki/css/view-structure.css b/frontend/src/pages/wiki2/css/view-structure.css similarity index 98% rename from frontend/src/pages/wiki/css/view-structure.css rename to frontend/src/pages/wiki2/css/view-structure.css index f9b15dde9d6..606e3088206 100644 --- a/frontend/src/pages/wiki/css/view-structure.css +++ b/frontend/src/pages/wiki2/css/view-structure.css @@ -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 { diff --git a/frontend/src/pages/wiki2/index-md-viewer/index.js b/frontend/src/pages/wiki2/index-md-viewer/index.js new file mode 100644 index 00000000000..e7dfdfa934e --- /dev/null +++ b/frontend/src/pages/wiki2/index-md-viewer/index.js @@ -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 ( +
+ {treeRoot.children.map(node => { + return ( + + ); + })} +
+ ); + } +} + +IndexMdViewer.propTypes = viewerPropTypes; + +export default IndexMdViewer; diff --git a/frontend/src/pages/wiki2/index-md-viewer/nav-item.js b/frontend/src/pages/wiki2/index-md-viewer/nav-item.js new file mode 100644 index 00000000000..3bfdd0064eb --- /dev/null +++ b/frontend/src/pages/wiki2/index-md-viewer/nav-item.js @@ -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 ( +
+ {name} +
+ ); + } + + if (name) { + return
{name}
; + } + + return null; + }; + + render() { + const { node } = this.props; + const { expanded } = this.state; + if (node.children.length > 0) { + return ( +
+ + {} + + {this.renderLink(node)} + {expanded && node.children.map((child, index) => { + return ( + + ); + })} +
+ ); + } + + return this.renderLink(node); + } +} + +NavItem.propTypes = propTypes; + +export default NavItem; diff --git a/frontend/src/pages/wiki2/index-md-viewer/style.css b/frontend/src/pages/wiki2/index-md-viewer/style.css new file mode 100644 index 00000000000..cc49daacc41 --- /dev/null +++ b/frontend/src/pages/wiki2/index-md-viewer/style.css @@ -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; +} diff --git a/frontend/src/pages/wiki2/index.js b/frontend/src/pages/wiki2/index.js new file mode 100644 index 00000000000..ac730ee8f67 --- /dev/null +++ b/frontend/src/pages/wiki2/index.js @@ -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 ( +
+ {isEditWiki && + this.saveWikiConfig(Object.assign({}, this.state.config, data))} + /> + } + + + + + +
+ ); + } +} + +export default Wiki; diff --git a/frontend/src/pages/wiki2/main-panel.js b/frontend/src/pages/wiki2/main-panel.js new file mode 100644 index 00000000000..2cf7c807b02 --- /dev/null +++ b/frontend/src/pages/wiki2/main-panel.js @@ -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 ( + + / + {item} + + ); + } else { + nodePath += '/' + item; + return ( + + / + + {item} + + + ); + } + }); + return pathElem; + }; + + + render() { + let { onSearchedClick } = this.props; + const errMessage = (
{gettext('Folder does not exist.')}
); + const isViewingFile = this.props.pathExist && !this.props.isDataLoading && this.props.isViewFile; + return ( +
+
{this.props.content}
+
+ {!username && + +
+ +
+
+ {isPro && ( + + )} +
+
+ } + {username && ( + +
+ + {this.props.permission == 'rw' && ( + Utils.isDesktop() ? + : + + )} +
+
+ {isPro && ( + + )} + + +
+
+ )} +
+
+
+ {!this.props.pathExist && errMessage} + {this.props.pathExist && this.props.isDataLoading && } + {isViewingFile && Utils.isMarkdownFile(this.props.path) && ( + + )} + {isViewingFile && Utils.isSdocFile(this.props.path) && ( + + )} + {(!this.props.isDataLoading && !this.props.isViewFile) && ( + + )} +
+
+
+ ); + } +} + +MainPanel.propTypes = propTypes; + +export default MainPanel; diff --git a/frontend/src/pages/wiki/models/folder.js b/frontend/src/pages/wiki2/models/folder.js similarity index 100% rename from frontend/src/pages/wiki/models/folder.js rename to frontend/src/pages/wiki2/models/folder.js diff --git a/frontend/src/pages/wiki/models/page.js b/frontend/src/pages/wiki2/models/page.js similarity index 100% rename from frontend/src/pages/wiki/models/page.js rename to frontend/src/pages/wiki2/models/page.js diff --git a/frontend/src/pages/wiki/models/wiki-config.js b/frontend/src/pages/wiki2/models/wiki-config.js similarity index 100% rename from frontend/src/pages/wiki/models/wiki-config.js rename to frontend/src/pages/wiki2/models/wiki-config.js diff --git a/frontend/src/pages/wiki2/side-panel.js b/frontend/src/pages/wiki2/side-panel.js new file mode 100644 index 00000000000..ff09f54908a --- /dev/null +++ b/frontend/src/pages/wiki2/side-panel.js @@ -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 ( +
+
+ +
+ ); + }; + + renderTreeView = () => { + return ( +
+ {/* {this.props.treeData && ( + + )} */} + {isEditWiki && + + } + {this.state.isShowNewFolderDialog && + + } + {this.state.isShowAddNewPageDialog && + + } +
+ ); + }; + + 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 ( +
+ + {this.state.isShowNewFolderDialog && + + } + {this.state.isShowAddNewPageDialog && + + } +
+ ); + }; + + renderContent = () => { + const { isLoading, indexNode, config } = this.props; + if (isLoading) { + return ; + } + 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 ( +
+
+ {src && } +

{wiki_name}

+
+ +
+ ); + } +} + +SidePanel.propTypes = propTypes; + +export default SidePanel; diff --git a/frontend/src/pages/wiki2/utils/generate-navs.js b/frontend/src/pages/wiki2/utils/generate-navs.js new file mode 100644 index 00000000000..b58dd0470fa --- /dev/null +++ b/frontend/src/pages/wiki2/utils/generate-navs.js @@ -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; +}; + + + diff --git a/frontend/src/pages/wiki/utils/index.js b/frontend/src/pages/wiki2/utils/index.js similarity index 100% rename from frontend/src/pages/wiki/utils/index.js rename to frontend/src/pages/wiki2/utils/index.js diff --git a/frontend/src/pages/wiki2/view-structure/add-new-page-dialog.js b/frontend/src/pages/wiki2/view-structure/add-new-page-dialog.js new file mode 100644 index 00000000000..a1f887f3032 --- /dev/null +++ b/frontend/src/pages/wiki2/view-structure/add-new-page-dialog.js @@ -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 ( + + {gettext('Add page')} + +
+ + + + +
+
+ + + {this.state.isLoading ? + : + + } + +
+ ); + } +} + +AddNewPageDialog.propTypes = propTypes; + +export default AddNewPageDialog; diff --git a/frontend/src/pages/wiki/view-structure/add-view-dropdownmenu.js b/frontend/src/pages/wiki2/view-structure/add-view-dropdownmenu.js similarity index 80% rename from frontend/src/pages/wiki/view-structure/add-view-dropdownmenu.js rename to frontend/src/pages/wiki2/view-structure/add-view-dropdownmenu.js index e648606c500..5881b497dd9 100644 --- a/frontend/src/pages/wiki/view-structure/add-view-dropdownmenu.js +++ b/frontend/src/pages/wiki2/view-structure/add-view-dropdownmenu.js @@ -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 ( - - - + + + {gettext('Add page')} @@ -45,7 +45,7 @@ class AddViewDropdownMenu extends Component { } AddViewDropdownMenu.propTypes = { - onToggleAddViewDropdown: PropTypes.func, + toggleDropdown: PropTypes.func, onToggleAddView: PropTypes.func, onToggleAddFolder: PropTypes.func, }; diff --git a/frontend/src/pages/wiki/view-structure/constant.js b/frontend/src/pages/wiki2/view-structure/constant.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/constant.js rename to frontend/src/pages/wiki2/view-structure/constant.js diff --git a/frontend/src/pages/wiki/view-structure/folders/dragged-folder-item.js b/frontend/src/pages/wiki2/view-structure/folders/dragged-folder-item.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/folders/dragged-folder-item.js rename to frontend/src/pages/wiki2/view-structure/folders/dragged-folder-item.js diff --git a/frontend/src/pages/wiki/view-structure/folders/folder-item.js b/frontend/src/pages/wiki2/view-structure/folders/folder-item.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/folders/folder-item.js rename to frontend/src/pages/wiki2/view-structure/folders/folder-item.js diff --git a/frontend/src/pages/wiki/view-structure/folders/folder-operation-dropdownmenu.js b/frontend/src/pages/wiki2/view-structure/folders/folder-operation-dropdownmenu.js similarity index 98% rename from frontend/src/pages/wiki/view-structure/folders/folder-operation-dropdownmenu.js rename to frontend/src/pages/wiki2/view-structure/folders/folder-operation-dropdownmenu.js index 5e268342650..27fa07bb92e 100644 --- a/frontend/src/pages/wiki/view-structure/folders/folder-operation-dropdownmenu.js +++ b/frontend/src/pages/wiki2/view-structure/folders/folder-operation-dropdownmenu.js @@ -82,7 +82,7 @@ export default class FolderOperationDropdownMenu extends Component { style={{ zIndex: 1051 }} > - + {gettext('Add page')} diff --git a/frontend/src/pages/wiki/view-structure/html5DragDropContext.js b/frontend/src/pages/wiki2/view-structure/html5DragDropContext.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/html5DragDropContext.js rename to frontend/src/pages/wiki2/view-structure/html5DragDropContext.js diff --git a/frontend/src/pages/wiki/view-structure/index.js b/frontend/src/pages/wiki2/view-structure/index.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/index.js rename to frontend/src/pages/wiki2/view-structure/index.js diff --git a/frontend/src/pages/wiki/view-structure/new-folder-dialog.js b/frontend/src/pages/wiki2/view-structure/new-folder-dialog.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/new-folder-dialog.js rename to frontend/src/pages/wiki2/view-structure/new-folder-dialog.js diff --git a/frontend/src/pages/wiki/view-structure/page-utils.js b/frontend/src/pages/wiki2/view-structure/page-utils.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/page-utils.js rename to frontend/src/pages/wiki2/view-structure/page-utils.js diff --git a/frontend/src/pages/wiki/view-structure/view-structure-footer.js b/frontend/src/pages/wiki2/view-structure/view-structure-footer.js similarity index 58% rename from frontend/src/pages/wiki/view-structure/view-structure-footer.js rename to frontend/src/pages/wiki2/view-structure/view-structure-footer.js index cc82a75e3fa..a580a9e0c2f 100644 --- a/frontend/src/pages/wiki/view-structure/view-structure-footer.js +++ b/frontend/src/pages/wiki2/view-structure/view-structure-footer.js @@ -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 ( -
this.viewFooterRef = ref} - > +
- {this.state.isShowAddViewDropdownMenu && + {this.state.isShowDropdownMenu && diff --git a/frontend/src/pages/wiki/view-structure/view-structure.js b/frontend/src/pages/wiki2/view-structure/view-structure.js similarity index 98% rename from frontend/src/pages/wiki/view-structure/view-structure.js rename to frontend/src/pages/wiki2/view-structure/view-structure.js index ec63de47d2b..729eab9411b 100644 --- a/frontend/src/pages/wiki/view-structure/view-structure.js +++ b/frontend/src/pages/wiki2/view-structure/view-structure.js @@ -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 { {(this.props.isEditMode && !isSpecialInstance) && } diff --git a/frontend/src/pages/wiki/view-structure/views/delete-dialog.js b/frontend/src/pages/wiki2/view-structure/views/delete-dialog.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/views/delete-dialog.js rename to frontend/src/pages/wiki2/view-structure/views/delete-dialog.js diff --git a/frontend/src/pages/wiki/view-structure/views/drop-target-top-view.js b/frontend/src/pages/wiki2/view-structure/views/drop-target-top-view.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/views/drop-target-top-view.js rename to frontend/src/pages/wiki2/view-structure/views/drop-target-top-view.js diff --git a/frontend/src/pages/wiki/view-structure/views/page-dropdownmenu.js b/frontend/src/pages/wiki2/view-structure/views/page-dropdownmenu.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/views/page-dropdownmenu.js rename to frontend/src/pages/wiki2/view-structure/views/page-dropdownmenu.js diff --git a/frontend/src/pages/wiki/view-structure/views/view-edit-popover.js b/frontend/src/pages/wiki2/view-structure/views/view-edit-popover.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/views/view-edit-popover.js rename to frontend/src/pages/wiki2/view-structure/views/view-edit-popover.js diff --git a/frontend/src/pages/wiki/view-structure/views/view-item.js b/frontend/src/pages/wiki2/view-structure/views/view-item.js similarity index 100% rename from frontend/src/pages/wiki/view-structure/views/view-item.js rename to frontend/src/pages/wiki2/view-structure/views/view-item.js diff --git a/frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-left-bar-dialog.css b/frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-left-bar-dialog.css similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-left-bar-dialog.css rename to frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-left-bar-dialog.css diff --git a/frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-custom-icon.js b/frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-custom-icon.js similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-custom-icon.js rename to frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-custom-icon.js diff --git a/frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.css b/frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.css similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.css rename to frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.css diff --git a/frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.js b/frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.js similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.js rename to frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-icon-color.js diff --git a/frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-icons.js b/frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-icons.js similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-icons.js rename to frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-icons.js diff --git a/frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-name.js b/frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-name.js similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/app-settings-dialog-name.js rename to frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/app-settings-dialog-name.js diff --git a/frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/icon-settings-popover.css b/frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/icon-settings-popover.css similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/icon-settings-popover.css rename to frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/icon-settings-popover.css diff --git a/frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/icon-settings-popover.js b/frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/icon-settings-popover.js similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/icon-settings-popover.js rename to frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/icon-settings-popover.js diff --git a/frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/index.js b/frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/index.js similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/app-settings-dialog/index.js rename to frontend/src/pages/wiki2/wiki-left-bar/app-settings-dialog/index.js diff --git a/frontend/src/pages/wiki/wiki-left-bar/wiki-left-bar-icon.jsx b/frontend/src/pages/wiki2/wiki-left-bar/wiki-left-bar-icon.jsx similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/wiki-left-bar-icon.jsx rename to frontend/src/pages/wiki2/wiki-left-bar/wiki-left-bar-icon.jsx diff --git a/frontend/src/pages/wiki/wiki-left-bar/wiki-left-bar.css b/frontend/src/pages/wiki2/wiki-left-bar/wiki-left-bar.css similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/wiki-left-bar.css rename to frontend/src/pages/wiki2/wiki-left-bar/wiki-left-bar.css diff --git a/frontend/src/pages/wiki/wiki-left-bar/wiki-left-bar.js b/frontend/src/pages/wiki2/wiki-left-bar/wiki-left-bar.js similarity index 100% rename from frontend/src/pages/wiki/wiki-left-bar/wiki-left-bar.js rename to frontend/src/pages/wiki2/wiki-left-bar/wiki-left-bar.js diff --git a/frontend/src/pages/wiki2/wiki.css b/frontend/src/pages/wiki2/wiki.css new file mode 100644 index 00000000000..f7809d7abda --- /dev/null +++ b/frontend/src/pages/wiki2/wiki.css @@ -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; + } +} diff --git a/frontend/src/pages/wikis/wikis.js b/frontend/src/pages/wikis/wikis.js index b6696c4e018..d043aedf0d2 100644 --- a/frontend/src/pages/wikis/wikis.js +++ b/frontend/src/pages/wikis/wikis.js @@ -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}); - }; - - onCreateToggle = () => { - this.setState({isShowCreateDialog: !this.state.isShowCreateDialog}); + toggelAddWikiDialog = () => { + this.setState({isShowAddDialog: !this.state.isShowAddDialog}); }; - 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); + if (error.response) { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); } }); }; - 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; + deleteWiki = (wiki) => { + 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); } - return item; }); - this.setState({wikis: wikis}); - }).catch((error) => { - if(error.response) { - let errorMsg = error.response.data.error_msg; - toaster.danger(errorMsg); - } - }); - }; - - deleteWiki = (wiki) => { - seafileAPI.deleteWiki(wiki.slug).then(() => { - let wikis = this.state.wikis.filter(item => { - return item.name !== wiki.name; + } 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); + } }); - 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 {
- + - +
@@ -130,6 +144,11 @@ class Wikis extends Component {
+ {this.state.isShowAddDialog && + + + + }
@@ -141,35 +160,20 @@ class Wikis extends Component { {(this.state.loading || this.state.wikis.length !== 0) && } {(!this.state.loading && this.state.wikis.length === 0) && -

{gettext('No published libraries')}

-

{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.')}

+

{gettext('No Wikis')}

+

{gettext('You have not any wikis yet.')}

+

{gettext('A wiki can be accessed by anyone, not only users, via its URL.')}

+

{gettext('You can add a wiki by clicking the "Add Wiki" button in the menu bar.')}

}
- {this.state.isShowCreateDialog && ( - - - - )} - {this.state.isShowSelectDialog && ( - - - - )} ); } diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index bf537b7313c..98d720cc61d 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -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' : ''; diff --git a/frontend/src/utils/editor-utilities.js b/frontend/src/utils/editor-utilities.js index e4db2acd139..d176c8add64 100644 --- a/frontend/src/utils/editor-utilities.js +++ b/frontend/src/utils/editor-utilities.js @@ -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 => { diff --git a/frontend/src/utils/wiki-api.js b/frontend/src/utils/wiki-api.js index 501dbd9bb07..97d21052bc9 100644 --- a/frontend/src/utils/wiki-api.js +++ b/frontend/src/utils/wiki-api.js @@ -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); + deleteWiki(wikiId) { + const url = this.server + '/api/v2.1/wikis/' + wikiId + '/'; + return this.req.delete(url); } - updateWikiPermission(wikiSlug, permission) { - const url = this.server + '/api/v2.1/wikis/' + wikiSlug + '/'; - let params = { - permission: permission - }; - return this.req.put(url, params); + + // 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); } - deleteWiki(slug) { - const url = this.server + '/api/v2.1/wikis/' + slug + '/'; + deleteWiki2(wikiId) { + const url = this.server + '/api/v2.1/wikis2/' + wikiId + '/'; return this.req.delete(url); } - updateWikiConfig(wikiSlug, wikiConfig) { - const url = this.server + '/api/v2.1/wiki-config/' + wikiSlug + '/'; + 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); } diff --git a/frontend/src/wiki2.js b/frontend/src/wiki2.js new file mode 100644 index 00000000000..560641a78f6 --- /dev/null +++ b/frontend/src/wiki2.js @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import Wiki from './pages/wiki2'; + +ReactDom.render(, document.getElementById('wrapper')); diff --git a/seahub/api2/endpoints/wiki2.py b/seahub/api2/endpoints/wiki2.py new file mode 100644 index 00000000000..abb69ff07db --- /dev/null +++ b/seahub/api2/endpoints/wiki2.py @@ -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, + }) diff --git a/seahub/api2/endpoints/wiki_pages.py b/seahub/api2/endpoints/wiki_pages.py index 565e3978f72..1c305a3e03a 100644 --- a/seahub/api2/endpoints/wiki_pages.py +++ b/seahub/api2/endpoints/wiki_pages.py @@ -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) diff --git a/seahub/api2/endpoints/wikis.py b/seahub/api2/endpoints/wikis.py index 031008b5fc0..fe749836975 100644 --- a/seahub/api2/endpoints/wikis.py +++ b/seahub/api2/endpoints/wikis.py @@ -28,10 +28,6 @@ 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 @@ def get(self, request, format=None): 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 @@ def delete(self, request, slug): 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 @@ def put(self, request, slug): 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 @@ def post(self, request, slug): "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}) diff --git a/seahub/settings.py b/seahub/settings.py index 7d8f05535df..0732519da56 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -245,6 +245,7 @@ 'seahub.institutions', 'seahub.invitations', 'seahub.wiki', + 'seahub.wiki2', 'seahub.group', 'seahub.notifications', 'seahub.options', diff --git a/seahub/templates/wiki/wiki.html b/seahub/templates/wiki/wiki.html index fedf9e3e4a0..e1b4a5425c6 100644 --- a/seahub/templates/wiki/wiki.html +++ b/seahub/templates/wiki/wiki.html @@ -40,6 +40,7 @@ window.wiki = { config: { slug: "{{ wiki.slug }}", + wikiId: "{{ wiki.id }}", repoId: "{{ wiki.repo_id }}", sharedToken: "{{ shared_token }}", sharedType: "{{ shared_type }}", diff --git a/seahub/templates/wiki/wiki_edit.html b/seahub/templates/wiki/wiki_edit.html index 8c3280d5236..4baa5992b43 100644 --- a/seahub/templates/wiki/wiki_edit.html +++ b/seahub/templates/wiki/wiki_edit.html @@ -39,16 +39,12 @@ -{% render_bundle 'wiki' 'js' %} +{% render_bundle 'wiki2' 'js' %} {% endblock %} diff --git a/seahub/urls.py b/seahub/urls.py index 444a9ed3be0..7dd2e5f3ce6 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -86,7 +86,7 @@ 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.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 @@ ## 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[^/]+)/$', WikiView.as_view(), name='api-v2.1-wiki'), - re_path(r'^api/v2.1/wikis/(?P[^/]+)/dir/$', WikiPagesDirView.as_view(), name='api-v2.1-wiki-pages-dir'), - re_path(r'^api/v2.1/wiki-config/(?P[^/]+)/$', WikiConfigView.as_view(), name='api-v2.1-wiki-config'), - re_path(r'^api/v2.1/wikis/(?P[^/]+)/content/$', WikiPageContentView.as_view(), name='api-v2.1-wiki-pages-content'), + re_path(r'^api/v2.1/wikis/(?P\d+)/$', WikiView.as_view(), name='api-v2.1-wiki'), + re_path(r'^api/v2.1/wikis/(?P\d+)/dir/$', WikiPagesDirView.as_view(), name='api-v2.1-wiki-pages-dir'), + re_path(r'^api/v2.1/wikis/(?P\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\d+)/$', Wiki2View.as_view(), name='api-v2.1-wiki2'), + re_path(r'^api/v2.1/wikis2/(?P\d+)/dir/$', Wiki2PagesDirView.as_view(), name='api-v2.1-wiki2-pages-dir'), + re_path(r'^api/v2.1/wiki2-config/(?P\d+)/$', Wiki2ConfigView.as_view(), name='api-v2.1-wiki2-config'), + re_path(r'^api/v2.1/wikis2/(?P\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\d+)/$', DraftView.as_view(), name='api-v2.1-draft'), @@ -699,7 +706,7 @@ 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[a-f0-9]{32})/$', AdminInvitation.as_view(), name='api-v2.1-admin-invitation'), - re_path(r'^edit-wiki/(?P[^/]+)/(?P.*)$', edit_slug, name='edit_slug'), + re_path(r'^edit-wiki/(?P[^/]+)/(?P.*)$', edit_wiki, name='edit_wiki'), path('avatar/', include('seahub.avatar.urls')), path('notice/', include('seahub.notifications.urls')), diff --git a/seahub/wiki/utils.py b/seahub/wiki/utils.py index 842c6af5515..250256e18df 100644 --- a/seahub/wiki/utils.py +++ b/seahub/wiki/utils.py @@ -20,6 +20,7 @@ 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__) diff --git a/seahub/wiki/views.py b/seahub/wiki/views.py index 544e2673e41..1480f603715 100644 --- a/seahub/wiki/views.py +++ b/seahub/wiki/views.py @@ -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 diff --git a/seahub/wiki2/__init__.py b/seahub/wiki2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seahub/wiki2/models.py b/seahub/wiki2/models.py new file mode 100644 index 00000000000..b94afbe1392 --- /dev/null +++ b/seahub/wiki2/models.py @@ -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() diff --git a/seahub/wiki2/utils.py b/seahub/wiki2/utils.py new file mode 100644 index 00000000000..e607c41da4a --- /dev/null +++ b/seahub/wiki2/utils.py @@ -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 diff --git a/seahub/wiki2/views.py b/seahub/wiki2/views.py new file mode 100644 index 00000000000..077924c9e23 --- /dev/null +++ b/seahub/wiki2/views.py @@ -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, + }) diff --git a/tests/api/endpoints/test_wikis.py b/tests/api/endpoints/test_wikis.py index 8ef7978ad3a..cc6b2a788cb 100644 --- a/tests/api/endpoints/test_wikis.py +++ b/tests/api/endpoints/test_wikis.py @@ -105,7 +105,7 @@ def setUp(self): 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):