import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
import { gettext, repoID, siteRoot, initialPath, isDir, serviceUrl } from './utils/constants';
import { seafileAPI } from './utils/seafile-api';
import { Utils } from './utils/utils';
import SidePanel from './pages/repo-wiki-mode/side-panel';
import MainPanel from './pages/repo-wiki-mode/main-panel';
import Node from './components/tree-view/node';
import Tree from './components/tree-view/tree';
import toaster from './components/toast';
import LibDecryptDialog from './components/dialog/lib-decrypt-dialog';
import ModalPortal from './components/modal-portal';
import Dirent from './models/dirent';
import FileTag from './models/file-tag';
import './assets/css/fa-solid.css';
import './assets/css/fa-regular.css';
import './assets/css/fontawesome.css';
import './css/layout.css';
import './css/side-panel.css';
import './css/wiki.css';
import './css/toolbar.css';
import './css/search.css';
class Wiki extends Component {
constructor(props) {
super(props);
this.state = {
path: '',
pathExist: true,
treeData: new Tree(),
closeSideBar: false,
currentNode: null,
isDirentListLoading: true,
isViewFile: false,
direntList: [],
isFileLoading: true,
content: '',
lastModified: '',
latestContributor: '',
permission: '',
isDirentSelected: false,
isAllDirentSelected: false,
selectedDirentList: [],
libNeedDecrypt: false,
isDraft: false,
hasDraft: false,
reviewStatus: '',
reviewID: '',
draftFilePath: '',
};
window.onpopstate = this.onpopstate;
this.hash = '';
}
componentWillMount() {
const hash = window.location.hash;
if (hash.slice(0, 1) === '#') {
this.hash = hash;
}
}
componentDidMount() {
seafileAPI.getRepoInfo(repoID).then(res => {
this.setState({
libNeedDecrypt: res.data.lib_need_decrypt,
});
if (!res.data.lib_need_decrypt) {
if (isDir === 'None') {
this.setState({pathExist: false});
} else if (isDir === 'True') {
this.showDir(initialPath);
} else if (isDir === 'False') {
this.showFile(initialPath);
}
this.loadSidePanel(initialPath);
}
});
}
deleteItemAjaxCallback(path, isDir) {
let node = this.state.treeData.getNodeByPath(path);
this.deleteTreeNode(node);
this.deleteDirent(path);
}
deleteItem(path, isDir) {
if (isDir) {
seafileAPI.deleteDir(repoID, path).then(() => {
this.deleteItemAjaxCallback(path, isDir);
}).catch(() => {
//todos;
});
} else {
seafileAPI.deleteFile(repoID, path).then(() => {
this.deleteItemAjaxCallback(path, isDir);
}).catch(() => {
//todos;
});
}
}
renameItemAjaxCallback(path, isDir, newName) {
let node = this.state.treeData.getNodeByPath(path);
this.renameTreeNode(node, newName);
this.renameDirent(path, newName);
}
renameItem = (path, isDir, newName) => {
//validate task
if (isDir) {
seafileAPI.renameDir(repoID, path, newName).then(() => {
this.renameItemAjaxCallback(path, isDir, newName);
}).catch(() => {
//todos;
});
} else {
seafileAPI.renameFile(repoID, path, newName).then(() => {
this.renameItemAjaxCallback(path, isDir, newName);
}).catch(() => {
//todos;
});
}
}
onAddFile = (filePath, isDraft) => {
//todo validate;
seafileAPI.createFile(repoID, filePath, isDraft).then(res => {
let name = Utils.getFileName(filePath);
let parentPath = Utils.getDirName(filePath);
this.addNodeToTree(name, parentPath, 'file');
if (parentPath === this.state.path && !this.state.isViewFile) {
this.addDirent(name, 'file');
}
}).catch(() => {
//todo;
});
}
onAddFolder = (dirPath) => {
//validate task
seafileAPI.createDir(repoID, dirPath).then(() => {
let name = Utils.getFileName(dirPath);
let parentPath = Utils.getDirName(dirPath);
this.addNodeToTree(name, parentPath, 'dir');
if (parentPath === this.state.path && !this.state.isViewFile) {
this.addDirent(name, 'dir');
}
}).catch(() => {
//return error message
});
}
onMoveItem = (destRepo, dirent, moveToDirentPath) => {
//just for view list state
let dirName = dirent.name
let direntPath = Utils.joinPath(this.state.path, dirName);
seafileAPI.moveDir(repoID, destRepo.repo_id,moveToDirentPath, this.state.path, dirName).then(() => {
this.moveTreeNode(direntPath, moveToDirentPath, destRepo);
this.moveDirent(direntPath);
let message = gettext('Successfully moved %(name)s.');
message = message.replace('%(name)s', dirName);
toaster.success(message);
}).catch(() => {
let message = gettext('Failed to move %(name)s');
message = message.replace('%(name)s', dirName);
toaster.danger(message);
});
}
onCopyItem = (destRepo, dirent, copyToDirentPath) => {
//just for view list state
let dirName = dirent.name;
let direntPath = Utils.joinPath(this.state.path, dirName);
seafileAPI.copyDir(repoID, destRepo.repo_id, copyToDirentPath, this.state.path, dirName).then(() => {
this.copyTreeNode(direntPath, copyToDirentPath, destRepo);
let message = gettext('Successfully copied %(name)s.');
message = message.replace('%(name)s', dirName);
toaster.success(message);
}).catch(() => {
let message = gettext('Failed to copy %(name)s');
message = message.replace('%(name)s', dirName);
toaster.danger(message);
});
}
loadSidePanel = (filePath) => {
seafileAPI.listDir(repoID, '/',{recursive: true}).then(items => {
const files = items.data.map(item => {
return {
name: item.name,
type: item.type === 'dir' ? 'dir' : 'file',
isExpanded: item.type === 'dir' ? true : false,
parent_path: item.parent_dir,
last_update_time: item.mtime,
permission: item.permission,
size: item.size
};
});
var treeData = new Tree();
treeData.parseListToTree(files);
let node = treeData.getNodeByPath(filePath);
if (node) {
treeData.expandNode(node);
this.setState({treeData: treeData, currentNode: node});
} else {
this.setState({treeData: treeData});
}
}).catch(() => {
/* eslint-disable */
console.log('failed to load files');
/* eslint-enable */
this.setState({isLoadFailed: true});
});
}
showFile = (filePath) => {
this.setState({
path: filePath,
isViewFile: true
});
this.setState({isFileLoading: true});
seafileAPI.getFileInfo(repoID, filePath).then((res) => {
let { mtime, permission, last_modifier_name, is_draft, has_draft,
review_status, review_id, draft_file_path } = res.data;
seafileAPI.getFileDownloadLink(repoID, filePath).then((res) => {
seafileAPI.getFileContent(res.data).then((res) => {
this.setState({
content: res.data,
permission: permission,
latestContributor: last_modifier_name,
lastModified: moment.unix(mtime).fromNow(),
isFileLoading: false,
isDraft: is_draft,
hasDraft: has_draft,
reviewStatus: review_status,
reviewID: review_id,
draftFilePath: draft_file_path
});
});
});
});
let fileUrl = siteRoot + 'wiki/lib/' + repoID + filePath;
window.history.pushState({url: fileUrl, path: filePath}, filePath, fileUrl);
}
showDir = (path) => {
this.loadDirentList(path);
this.setState({
path: path,
isViewFile: false
});
// update location url
let url = siteRoot + 'wiki/lib/' + repoID + path;
window.history.pushState({ url: url, path: path}, path, url);
}
loadDirentList = (filePath) => {
this.setState({isDirentListLoading: true});
seafileAPI.listDir(repoID, filePath).then(res => {
let direntList = [];
res.data.forEach(item => {
let dirent = new Dirent(item);
direntList.push(dirent);
});
this.setState({
direntList: direntList,
isDirentListLoading: false,
});
});
}
onLinkClick = (event) => {
const url = event.path[2].href;
if (this.isInternalMarkdownLink(url)) {
let path = this.getPathFromInternalMarkdownLink(url);
this.showFile(path);
} else if (this.isInternalDirLink(url)) {
let path = this.getPathFromInternalDirLink(url);
this.showDir(path);
}
}
updateDirent = (dirent, paramKey, paramValue) => {
let newDirentList = this.state.direntList.map(item => {
if (item.name === dirent.name) {
item[paramKey] = paramValue;
}
return item;
});
this.setState({direntList: newDirentList});
}
onpopstate = (event) => {
if (event.state && event.state.path) {
let path = event.state.path;
if (this.isMarkdownFile(path)) {
this.showFile(path);
} else {
let currentNode = this.state.treeData.getNodeByPath(path);
this.showDir(currentNode.path);
}
}
}
onSearchedClick = (item) => {
if (item.is_dir) {
let path = item.path.slice(0, item.path.length - 1);
if (this.state.currentFilePath !== path) {
let tree = this.state.treeData.clone();
let node = tree.getNodeByPath(path);
tree.expandNode(node);
this.showDir(node.path);
}
} else if (Utils.isMarkdownFile(item.path)) {
let path = item.path;
if (this.state.currentFilePath !== path) {
let tree = this.state.treeData.clone();
let node = tree.getNodeByPath(path);
tree.expandNode(node);
this.showFile(node.path);
}
} else {
let url = siteRoot + 'lib/' + item.repo_id + '/file' + Utils.encodePath(item.path);
let newWindow = window.open('about:blank');
newWindow.location.href = url;
}
}
onMainNavBarClick = (nodePath) => {
//just for dir
this.resetSelected();
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) => {
this.resetSelected();
let direntPath = Utils.joinPath(this.state.path, dirent.name);
let tree = this.state.treeData.clone();
let node = tree.getNodeByPath(direntPath);
let parentNode = tree.findNodeParentFromTree(node);
tree.expandNode(parentNode);
if (node.isMarkdown()) {
this.setState({treeData: tree}); // tree
this.showFile(direntPath);
} else if (node.isDir()) {
this.setState({treeData: tree, currentNode: node}); //tree
this.showDir(node.path);
} else {
const w=window.open('about:blank');
const url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
w.location.href = url;
}
}
onDirentSelected = (dirent) => {
let direntList = this.state.direntList.map(item => {
if (item.name === dirent.name) {
item.isSelected = !item.isSelected;
}
return item;
});
let selectedDirentList = direntList.filter(item => {
return item.isSelected;
});
if (selectedDirentList.length) {
this.setState({isDirentSelected: true});
if (selectedDirentList.length === direntList.length) {
this.setState({
isAllDirentSelected: true,
direntList: direntList,
selectedDirentList: selectedDirentList,
});
} else {
this.setState({
isAllDirentSelected: false,
direntList: direntList,
selectedDirentList: selectedDirentList
});
}
} else {
this.setState({
isDirentSelected: false,
isAllDirentSelected: false,
direntList: direntList,
selectedDirentList: []
});
}
}
onAllDirentSelected = () => {
if (this.state.isAllDirentSelected) {
let direntList = this.state.direntList.map(item => {
item.isSelected = false;
return item;
});
this.setState({
isDirentSelected: false,
isAllDirentSelected: false,
direntList: direntList,
selectedDirentList: [],
});
} else {
let direntList = this.state.direntList.map(item => {
item.isSelected = true;
return item;
});
this.setState({
isDirentSelected: true,
isAllDirentSelected: true,
direntList: direntList,
selectedDirentList: direntList,
});
}
}
onMainPanelItemRename = (dirent, newName) => {
let path = Utils.joinPath(this.state.path, dirent.name);
this.renameItem(path, dirent.isDir(), newName);
}
onMainPanelItemDelete = (dirent) => {
let path = Utils.joinPath(this.state.path, dirent.name);
this.deleteItem(path, dirent.isDir());
}
renameDirent = (direntPath, newName) => {
let parentPath = Utils.getDirName(direntPath);
let newDirentPath = Utils.joinPath(parentPath, newName);
if (direntPath === this.state.path) {
// the renamed item is current viewed item
// example: direntPath = /A/B/C, state.path = /A/B/C
this.setState({ path: newDirentPath });
} else if (Utils.isChildPath(direntPath, this.state.path)) {
// example: direntPath = /A/B/C/D, state.path = /A/B/C
let oldName = Utils.getFileName(direntPath);
let direntList = this.state.direntList.map(item => {
if (item.name === oldName) {
item.name = newName;
}
return item;
});
this.setState({ direntList: direntList });
} else if (Utils.isAncestorPath(direntPath, this.state.path)) {
// example: direntPath = /A/B, state.path = /A/B/C
let newPath = Utils.renameAncestorPath(this.state.path, direntPath, newDirentPath);
this.setState({ path: newPath });
}
}
deleteDirent(direntPath) {
if (direntPath === this.state.path) {
// The deleted item is current item
let parentPath = Utils.getDirName(direntPath);
this.showDir(parentPath);
} else if (Utils.isChildPath(direntPath, this.state.path)) {
// The deleted item is inside current path
let name = Utils.getFileName(direntPath);
let direntList = this.state.direntList.filter(item => {
return item.name !== name;
});
this.setState({ direntList: direntList });
} else if (Utils.isAncestorPath(direntPath, this.state.path)) {
// the deleted item is ancester of the current item
let parentPath = Utils.getDirName(direntPath);
this.showDir(parentPath);
}
// else do nothing
}
addDirent = (name, type) => {
let item = this.createDirent(name, type);
let direntList = this.state.direntList;
if (type === 'dir') {
direntList.unshift(item);
} else {
direntList.push(item);
}
this.setState({direntList: direntList});
}
moveDirent = (filePath) => {
let name = filePath.slice(filePath.lastIndexOf('/') + 1);
let direntList = this.state.direntList.filter(item => {
return item.name !== name;
});
this.setState({direntList: direntList});
}
onMoveItems = (destRepo, destDirentPath) => {
let direntPaths = this.getSelectedDirentPaths();
let dirNames = this.getSelectedDirentNames();
seafileAPI.moveDir(repoID, destRepo.repo_id, destDirentPath, this.state.path, dirNames).then(() => {
direntPaths.forEach(direntPath => {
this.moveTreeNode(direntPath, destDirentPath, destRepo);
this.moveDirent(direntPath);
});
let message = gettext('Successfully moved %(name)s.');
message = message.replace('%(name)s', dirNames);
toaster.success(message);
}).catch(() => {
let message = gettext('Failed to move %(name)s');
message = message.replace('%(name)s', dirNames);
toaster.danger(message);
});
}
onCopyItems = (destRepo, destDirentPath) => {
let direntPaths = this.getSelectedDirentPaths();
let dirNames = this.getSelectedDirentNames();
seafileAPI.copyDir(repoID, destRepo.repo_id, destDirentPath, this.state.path, dirNames).then(() => {
direntPaths.forEach(direntPath => {
this.copyTreeNode(direntPath, destDirentPath, destRepo);
});
let message = gettext('Successfully copied %(name)s.');
message = message.replace('%(name)s', dirNames);
toaster.success(message);
}).catch(() => {
let message = gettext('Failed to copy %(name)s');
message = message.replace('%(name)s', dirNames);
toaster.danger(message);
});
}
onDeleteItems = () => {
let direntPaths = this.getSelectedDirentPaths();
let dirNames = this.getSelectedDirentNames();
seafileAPI.deleteMutipleDirents(repoID, this.state.path, dirNames).then(res => {
direntPaths.forEach(direntPath => {
let node = this.state.treeData.getNodeByPath(direntPath);
this.deleteTreeNode(node);
this.deleteDirent(direntPath);
});
});
}
onFileTagChanged = (dirent, direntPath) => {
seafileAPI.listFileTags(repoID, direntPath).then(res => {
let fileTags = res.data.file_tags.map(item => {
return new FileTag(item);
});
this.updateDirent(dirent, 'file_tags', fileTags);
});
}
onTreeNodeClick = (node) => {
this.resetSelected();
if (!this.state.pathExist) {
this.setState({pathExist: true});
}
if (node instanceof Node && node.isMarkdown()) {
let tree = this.state.treeData.clone();
this.setState({treeData: tree});
if (node.path !== this.state.path) {
this.showFile(node.path);
}
} else if (node instanceof Node && node.isDir()) {
let tree = this.state.treeData.clone();
if (this.state.path === node.path) {
if (node.isExpanded) {
tree.collapseNode(node);
} else {
tree.expandNode(node);
}
}
this.setState({treeData: tree});
if (node.path !== this.state.path) {
this.showDir(node.path);
}
} else {
const w = window.open('about:blank');
const url = siteRoot + 'lib/' + repoID + '/file' + node.path;
w.location.href = url;
}
}
onTreeDirCollapse = (node) => {
let tree = this.state.treeData.clone();
let findNode = tree.getNodeByPath(node.path);
findNode.isExpanded = !findNode.isExpanded;
this.setState({treeData: tree});
}
onSideNavMenuClick = () => {
this.setState({
closeSideBar: !this.state.closeSideBar,
});
}
onCloseSide = () => {
this.setState({
closeSideBar: !this.state.closeSideBar,
});
}
onDeleteTreeNode = (node) => {
this.deleteItem(node.path, node.isDir());
}
onRenameTreeNode = (node, newName) => {
this.renameItem(node.path, node.isDir(), newName);
}
addNodeToTree = (name, parentPath, type) => {
let tree = this.state.treeData.clone();
let node = this.createTreeNode(name, type);
let parentNode = tree.getNodeByPath(parentPath);
tree.addNodeToParent(node, parentNode);
this.setState({treeData: tree});
}
renameTreeNode = (node, newName) => {
let tree = this.state.treeData.clone();
tree.updateNodeParam(node, 'name', newName);
this.setState({treeData: tree});
}
deleteTreeNode = (node) => {
let tree = this.state.treeData.clone();
tree.deleteNode(node);
this.setState({treeData: tree});
}
moveTreeNode = (nodePath, moveToPath, moveToRepo) => {
let tree = this.state.treeData.clone();
if (repoID === moveToRepo.repo_id) {
tree.moveNodeByPath(nodePath, moveToPath, true);
} else {
tree.deleteNodeByPath(nodePath);
}
this.setState({treeData: tree});
}
copyTreeNode = (nodePath, copyToPath, destRepo) => {
if (repoID !== destRepo.repo_id) {
return;
}
let tree = this.state.treeData.clone();
tree.moveNodeByPath(nodePath, copyToPath, false);
this.setState({treeData: tree});
}
createTreeNode(name, type) {
let date = new Date().getTime()/1000;
let node = new Node({
name : name,
type: type,
size: '0',
last_update_time: moment.unix(date).fromNow(),
isExpanded: false,
children: []
});
return node;
}
createDirent(name, type) {
let data = new Date().getTime()/1000;
let dirent = null;
if (type === 'dir') {
dirent = new Dirent({
id: '000000000000000000',
name: name,
type: type,
mtime: data,
permission: 'rw',
});
} else {
dirent = new Dirent({
id: '000000000000000000',
name: name,
type: type,
mtime: data,
permission: 'rw',
size: 0,
starred: false,
is_locked: false,
lock_time: '',
lock_owner: null,
locked_by_me: false,
modifier_name: '',
modifier_email: '',
modifier_contact_email: '',
file_tags: []
});
}
return dirent;
}
isMarkdownFile(filePath) {
let index = filePath.lastIndexOf('.');
if (index === -1) {
return false;
} else {
let type = filePath.substring(index).toLowerCase();
if (type === '.md' || type === '.markdown') {
return true;
} else {
return false;
}
}
}
isInternalMarkdownLink(url) {
var re = new RegExp(serviceUrl + '/wiki/lib/' + repoID + '/file' + '.*\.md$');
return re.test(url);
}
isInternalDirLink(url) {
var re = new RegExp(serviceUrl + '/wiki/lib/' + repoID + '/.*');
return re.test(url);
}
getPathFromInternalMarkdownLink(url) {
var re = new RegExp(serviceUrl + '/wiki/lib/' + repoID + '/file' + '(.*\.md)');
var array = re.exec(url);
var path = decodeURIComponent(array[1]);
return path;
}
getPathFromInternalDirLink(url) {
var re = new RegExp(serviceUrl + '/wiki/lib/' + repoID + '(/.*)');
var array = re.exec(url);
var path = decodeURIComponent(array[1]);
var dirPath = path.substring(1);
re = new RegExp('(^/.*)');
if (re.test(dirPath)) {
path = dirPath;
} else {
path = '/' + dirPath;
}
return path;
}
getSelectedDirentPaths = () => {
let paths = [];
this.state.selectedDirentList.forEach(selectedDirent => {
paths.push(Utils.joinPath(this.state.path, selectedDirent.name));
});
return paths;
}
getSelectedDirentNames = () => {
let names = [];
this.state.selectedDirentList.forEach(selectedDirent => {
names.push(selectedDirent.name);
});
return names;
}
resetSelected = () => {
this.setState({
isDirentSelected: false,
isAllDirentSelected: false,
});
}
onLibDecryptDialog = () => {
this.setState({
libNeedDecrypt: false,
})
if (isDir === 'None') {
this.setState({pathExist: false});
} else if (isDir === 'True') {
this.showDir(initialPath);
} else if (isDir === 'False') {
this.showFile(initialPath);
}
this.loadSidePanel(initialPath);
}
goReviewPage = () => {
window.location.href = siteRoot + 'drafts/review/' + this.state.reviewID;
}
goDraftPage = () => {
window.location.href = siteRoot + 'lib/' + repoID + '/file' + this.state.draftFilePath + '?mode=edit';
}
render() {
let { libNeedDecrypt } = this.state;
if (libNeedDecrypt) {
return (