forgeplus-react/src/forge/Wiki/components/sidebar/index.jsx

625 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { Tree, Dropdown, Menu, message, Modal, Input } from 'antd';
import DelModal from '../../components/ModalFun';
import './index.scss';
import '../../components/ModalFun/index.scss';
import { deleteWiki, findBrotherNodesByKey, findNodeByKey, getWiki, treeToMd, updateWiki } from '../../api';
import { useEffect } from 'react';
import { Base64 } from 'js-base64';
import { splicingTabs } from '../../utils';
const Search = Input.Search;
const {DirectoryTree, TreeNode} = Tree;
// wiki目录树
export default function Sidebar(props) {
const {fileList, changeitem, owner, projectsId, project, setReload, sidebar, history, permission, defaultSelectedKeys, fileArrInit} = props;
const {location:{pathname}} = history;
const [initMenuList, setInitMenuList] = useState(undefined);
const [menuList, setMenuList] = useState(undefined);
const [actionVisible, setActionVisible] = useState(undefined);
const [visible, setVisible] = useState(false);
const [menuName, setMenuName] = useState(undefined);
const [addMenuError, setAddMenuError] = useState(undefined);
const [selectedKey, setSelectedKey] = useState(undefined);
const [expandedKeys, setExpandedKeys] = useState(undefined);
// 是否全部展开
const [isExpendedAll, setIsExpendedAll] = useState(false);
// 重命名目录名称
const [isEditName, setIsEditName] = useState(undefined);
// wiki重命名-wiki内容
const [wikiContent, setWikiContent] = useState(undefined);
useEffect(()=>{
if(defaultSelectedKeys){
// 设置选中wiki
setSelectedKey(defaultSelectedKeys);
// 第一次渲染,默认打开选中的页面所处目录
const keys = expandedKeys || [];
const list = defaultSelectedKeys[0].split('-');
list.map((item, index)=>{
keys.push(index > 0 ? `${keys[index-1]}-${item}` : item)
})
setExpandedKeys(Array.from(new Set(keys)));
}
}, [defaultSelectedKeys])
useEffect(()=>{
setMenuList(fileList);
setInitMenuList(fileList);
}, [fileList])
const menu = (title, isFile, item, level) =>{
return <Menu>
<Menu.Item>
<a onClick={(e) => { addFile(e, item) }}>
添加子页面
</a>
</Menu.Item>
<Menu.Item>
<a onClick={(e) => {e.stopPropagation();setVisible(item);}}>
添加子目录
</a>
</Menu.Item>
<Menu.Item>
<a onClick={(e) => { updateName(e, item, 1) }}>
重命名
</a>
</Menu.Item>
<Menu.Item>
<a onClick={(e) => { deleteFileModal(e, title, isFile, item) }}>删除</a>
</Menu.Item>
</Menu>
};
const fileMenu = (title, isFile, item) =>{
return <Menu>
<Menu.Item>
<a onClick={(e) => { updateName(e, item, 2) }}>
重命名
</a>
</Menu.Item>
<Menu.Item>
<a onClick={(e) => { deleteFileModal(e, title, isFile, item) }}>删除</a>
</Menu.Item>
</Menu>
};
const treeTitle = (title, isFile, key, item, level)=>{
return <React.Fragment>
<span className='wikiName font-15'>{title}</span>
{permission && <span className= {actionVisible === key ? 'visibleIcon wikiActionBox' : 'wikiActionBox'}>
<Dropdown overlay={isFile ? fileMenu(title, isFile, item) : menu(title, isFile, item, level)} placement="bottomRight" onVisibleChange={(value)=>{setActionVisible(value ? key : undefined)}}>
<i className="iconfont icon-kongbai font-14"></i>
</Dropdown>
</span>}
</React.Fragment>
}
function renderTreeNodes(data) {
return data && data.length > 0 && data.map((item) => {
const level = item.title.search(/\S/);
let title = item.title.trim();
const isFile = title.startsWith('[[') && title.endsWith(']]');
title = isFile ? title.substring(2,title.length-2) : title.substring(2, title.length);
return (
<TreeNode title={treeTitle(title, isFile, item.key, item, level)} key={item.key+""} isLeaf={isFile} dataRef={item}>
{renderTreeNodes(item.children)}
</TreeNode>
);
});
}
// 遍历list找到相同的key callback
const loop = (data, key, callback) => {
data.forEach((item, index, arr) => {
if (item.key === key) {
return callback(item, index, arr);
}
if (item.children) {
return loop(item.children, key, callback);
}
});
};
// 选中节点
function selectTree(keys,event){
let {title, key} = event.node.props.dataRef;
setSelectedKey([key+""]);
title = title.trim();
const isFile = title.startsWith('[[') && title.endsWith(']]');
if(isFile){
title = title.substring(2,title.length-2);
changeitem({name: title, key});
}
}
// 收起展开
function onExpand(keys, obj){
const oldKeys = new Set(keys);
const {expanded} = obj;
let {key} = obj.node.props.dataRef;
if(expanded){
oldKeys.add(key);
}else{
oldKeys.delete(key);
}
setExpandedKeys(Array.from(oldKeys))
}
// 删除wiki模态框
function deleteFileModal(e, title, isFile, item) {
e.stopPropagation();
DelModal({
title: `删除${isFile ? '页面' : '目录'}`,
contentTitle: `您确定要删除“${title}”此${isFile ? '页面' : '目录'}吗?`,
content: `此操作将删除此${isFile ? '页面' : '目录'},请进行确认以防文件的丢失`,
onOk: () => {
const {node, children, key} = item;
if(isFile){
// 删除页面
deleteWiki({owner: owner, repo: projectsId, projectId: project.id, pageName: item.title_sub}).then(res => {
if(res && res.message === "204"){
// 递归过滤相同key值的节点执行删除操作
const menuListByDel = findSameKeyByDel(menuList, key);
updateSidebar(treeToMd(menuListByDel), 1);
}
});
}else{
// 删除目录(递归找到所有的[[页面]],遍历删除)
// needDelNode: 被删除的子节点
const needDelNode = findNodeByKey(menuList, key);
// needDelWikis所有需要删除的子目录列表
const needDelWikis = [];
findDelNode(needDelNode.children, needDelWikis);
if(needDelWikis.length){
// 有需要删除的子目录
const promiseList = []
needDelWikis.map(item=>{
promiseList.push(deleteWikiFun({owner: owner, repo: projectsId, projectId: project.id},item.title.trim()))
})
Promise.all(promiseList).then((values) => {
if(values.filter(item=>item.message === '204' || item.message === '404').length === needDelWikis.length){
// 执行修改sidebar操作递归过滤相同key值的节点执行删除操作
const menuListByDel = findSameKeyByDel(menuList, key);
updateSidebar(treeToMd(menuListByDel), 1);
}else{
message.error('发生未知错误,请联系系统管理员')
}
});
}else{
// 没有需要删除的子目录直接修改sidebar文件即可
const menuListByDel = findSameKeyByDel(menuList, key);
updateSidebar(treeToMd(menuListByDel), 1);
}
}
}
});
}
// 删除页面递归过滤相同key值的节点
function findSameKeyByDel(list, key){
return list.filter(item => {
if (item.key === key) {
return false
}
if(item.children && item.children.length > 0){
item.children = findSameKeyByDel(item.children , key)
}
return true
})
}
// 删除目录:递归找到需要删除的子目录
function findDelNode(list, nodes){
//利用foreach循环遍历
list.forEach(item => {
//判断递归结束条件
const title = item.title.trim();
if (title.startsWith('[[') && title.endsWith(']]')) {
// 存储数据到空数组
nodes.push(item);
}
if (item.children.length){
//递归调用
const result = findDelNode(item.children, nodes);
if(result) return result
}else{
return true
}
})
return nodes;
}
// 删除wiki
function deleteWikiFun(info, title){
return new Promise((resolve, reject)=>{
deleteWiki({
...info,
pageName: title
}).then(res => {
resolve(res)
})
})
}
// 修改此仓库的sidebar文件 type 1删除 2拖拽 3添加子目录 4目录重命名
function updateSidebar(content, type, addWikiName){
if(type === 1 && !content){
// 删除最后一个wiki的时候同时删除sidebar
deleteWiki({owner: owner, repo: projectsId, projectId: project.id, pageName: "_Sidebar"}).then(res => {
if(res && res.message === "204"){
history.push(`/${owner}/${projectsId}/wiki`)
setReload(Math.random());
message.success('删除成功');
}
});
return
}
updateWiki({
owner,
repo: projectsId,
projectId: project.id,
pageName: '_Sidebar',
title: '_Sidebar',
message: '',
content_base64: Base64.encode(content)
}).then(res => {
if (res && res.message === "200") {
setReload(Math.random());
if(type === 1){
message.success('删除成功');
}else if(type === 3){
setMenuName(undefined);
setVisible(false);
message.success('新增成功');
}else if(type === 4){
setIsEditName(false);
setMenuName(undefined);
setVisible(false);
message.success('重命名成功');
}else{
// message.success('修改文件目录顺序成功');
}
}else{
message.error(res.message);
}
});
}
// 添加子页面
function addFile(e, item){
e.stopPropagation();
history.push(`/${owner}/${projectsId}/wiki/add?key=${item.key}`)
}
// 重命名目录文件 type:1 目录 2页面
function updateName(e, item, type){
e.stopPropagation();
setVisible(item);
setIsEditName(type);
const {titleStr, title_sub} = item;
type === 2 && getWiki({
owner: owner,
repo: projectsId,
pageName: title_sub,
projectId: project.id
}).then(res => {
if (res && res.message === "200") {
setWikiContent(res.data.md_content);
}
})
setMenuName(titleStr);
}
// 新增子目录 或者 重命名目录
function submit(){
if(!menuName){
setAddMenuError('请输入目录名称');
return;
}
if(new RegExp("^[ ]+$").test(menuName)){
setAddMenuError("不能仅输入空格");
return;
}
if (isEditName === 2 && /^-/.test(menuName)) {
setAddMenuError('不能以-开头');
return;
}
if(!isEditName){
// 新增目录:校验同级目录中是否有相同名称
const {children, key} = visible;
const mList = children.filter(item=>!item.title.trim().startsWith('[[')).map(i=>i.title.trim())
if(mList.includes(`- ${menuName}`)){
setAddMenuError('不能与已有目录名称相同');
return;
}
const newList = menuList;
// 设置Tree 打开的目录
const keys = expandedKeys || [];
const list = key.split('-');
list.map((item, index)=>{
keys.push(index > 0 ? `${keys[index-1]}-${item}` : item)
})
keys.push(`${key}-${children.length}`);
setExpandedKeys(Array.from(new Set(keys)));
addChildrenByKey(newList, key, menuName);
updateSidebar(treeToMd(newList), 3);
}else{
// isEditName 1 编辑目录名称 2编辑页面名称
const titleToJudge = isEditName === 2 ? `[[${menuName}]]` : `- ${menuName}`;
const {key, titleStr, title_sub} = visible;
// 判断是否有修改名称
if(titleStr === menuName){
setAddMenuError("请修改名称");
return
}
// 找兄弟节点
const list = findBrotherNodesByKey(menuList, key) || [];
if(list.map(item=>item.titleStr).includes(menuName)){
setAddMenuError("不能与同级目录名称相同");
return
}
// wiki名称不可重复
const sameNameWiki = fileArrInit.filter(item=>item.name === menuName)
if(isEditName === 2 && sameNameWiki.length){
setAddMenuError("wiki名称不可重复");
return
}
// 编辑sidebar
const newMenuList = JSON.parse(JSON.stringify(menuList))
loop(newMenuList, key, (item)=>{
const {key} = item;
item.title = `${'\t'.repeat(key.split('-').length-1)}${titleToJudge}`
})
if(isEditName === 2){
// 编辑页面名称-> 修改wiki
updateWiki({
owner,
repo: projectsId,
projectId: project.id,
// 原名称
pageName: title_sub,
// 现名称
title: menuName,
content_base64: wikiContent,
message: '',
}).then(res=>{
if(res && res.message === '200'){
// 修改wiki名称接口请求成功更新sidebar
updateSidebar(treeToMd(newMenuList), 4);
history.push(`/${owner}/${projectsId}/wiki/${encodeURI(menuName)}/${key}`);
}
})
return;
}
updateSidebar(treeToMd(newMenuList), 4)
}
}
// 递归menuList 创建子目录
function addChildrenByKey(list, key, name){
for (let index = 0; index < list.length; index++) {
const element = list[index];
if (element.key == key) {
const {title} = element;
element.children.push({
title: `\t${title.substring(0, title.indexOf('- '))}- ${name}`,
children: []
});
return true;
}else if (element.children.length) {
let result = addChildrenByKey(element.children, key, name);
if (result) {
return result;
}
}
}
}
function onDrop(info){
// dragNode: 拖拽的节点 node: 放入的节点
const dropKey = info.node.props.dataRef.key;
const dragKey = info.dragNode.props.dataRef.key;
const dropPos = info.node.props.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
// dropPosition 0元素之上 1元素之下 -1元素之上
function upTitle(list, title){
const kongGe = title.substring(0, title.indexOf('- ') === -1 ? title.indexOf('[[') : title.indexOf('- '));
list.map(item=>{
item.title = `\t${kongGe}${item.title.trim()}`;
if(item.children){
return upTitle(item.children, item.title);
}
})
}
const data = JSON.parse(JSON.stringify(menuList));
// Find dragObject
let dragObj;
loop(data, dragKey, (item, index, arr) => {
arr.splice(index, 1);
dragObj = item;
});
// 如果拖拽的是目录,那么需要遍历所有的子节点,改变\t个数
if(!dragObj.isFile){
// 放入的节点标题
const title = info.node.props.dataRef.title;
const addBool = !info.dropToGap
upTitle(dragObj.children, addBool ? `\t${title}` : title);
}
// 是否要保存sidebar文件
let bool = false;
// dropToGap: true拖拽在节点中间 false拖拽到节点之上
if (!info.dropToGap) {
// 拖拽到节点之上->追加到尾部
loop(data, dropKey, (item, index, arr) => {
const {title, key} = item;
if(title.indexOf('- ') === -1){
message.error('不能拖拽到页面上');
return;
}
const childrenName = item.children.map(item=>item.titleStr);
if(childrenName.includes(dragObj.titleStr)){
message.error('不能与同级目录名称相同');
return;
}
dragObj.title = `${splicingTabs(key.split('-').length)}${dragObj.title.trim()}`;
item.children = item.children || [];
item.children.push(dragObj);
bool = true;
});
}
// else if (
// (info.node.props.children || []).length > 0 && // Has children
// dropPosition === 1 // On the bottom gap
// ) {
// // 拖拽到目录节点下
// loop(data, dropKey, item => {
// const childrenName = item.children.map(item=>item.title.trim())
// if(childrenName.includes(dragObj.title.trim())){
// message.error('不能与同级目录名称相同');
// return;
// }
// console.log('item', item, 'dragObj', dragObj);
// const {title} = item;
// dragObj.title = `\t${title.substring(0, title.indexOf('- '))}${dragObj.title.trim()}`;
// item.children = item.children || [];
// item.children.unshift(dragObj);
// });
// bool = true;
// }
else {
let ar;
let i;
loop(data, dropKey, (item, index, arr) => {
ar = arr;
i = index;
});
const childrenName = ar.map(item=>item.titleStr)
if(childrenName.includes(dragObj.titleStr)){
message.error('不能与同级目录名称相同');
return;
}
dragObj.title = `${splicingTabs(ar[i].key.split('-').length-1)}${dragObj.title.trim()}`;
if (dropPosition === -1) {
ar.splice(i, 0, dragObj);
} else {
ar.splice(i + 1, 0, dragObj);
}
bool = true;
}
bool && updateSidebar(treeToMd(data), 2);
setMenuList(bool ? data : initMenuList);
}
// modal cancel
function onCancel(){
setVisible(false);
setAddMenuError(undefined);
setIsEditName(false);
setMenuName(undefined);
}
// 展开收起全部节点
function expandAll(){
setIsExpendedAll(!isExpendedAll);
if(isExpendedAll){
setExpandedKeys([]);
return;
}
const allKeys = [];
findMenuKey(allKeys, menuList);
setExpandedKeys(allKeys);
}
function findMenuKey(keys, list){
list.map(item=>{
if(item && !item.title.startsWith("[[")){
keys.push(item.key);
}
if(item && item.children.length){
findMenuKey(keys, item.children);
}
})
}
function changeSearchValue(value) {
if(value){
// 找到符合条件的子节点, 重新渲染页面
let keysMap = findNodesToKeys(initMenuList, value);
const newMenuList = [];
let openedKeysMap = [];
const oneNodeKeys = new Set();
keysMap.map((item, index)=>{
const {path, isFile, title} = item;
// 把所有的一级目录key筛选出来
oneNodeKeys.add(path[0]);
openedKeysMap = openedKeysMap.concat(path);
if(!index && isFile){
setSelectedKey([path[path.length-1]]);
const titleSplit = title.substring(2,title.length-2);
changeitem({key: path[path.length-1], name: titleSplit})
}
})
Array.from(oneNodeKeys).map(item=>loop(initMenuList, item,i=>{
newMenuList.push(i);
}))
const openedKeys = Array.from(new Set(openedKeysMap));
setExpandedKeys(openedKeys);
setMenuList(newMenuList);
}else{
setMenuList(JSON.parse(JSON.stringify(initMenuList)));
}
}
function findNodesToKeys(arr, value, path, nodes) {
path = path || [];
nodes = nodes || [];
if (Array.isArray(arr)) {
arr.forEach(function (obj, index) {
if (obj && typeof obj === 'object') {
const {children, key} = obj;
const title = obj.title.trim();
const isFile = title.startsWith('[[') && title.endsWith(']]');
if (title.match(new RegExp(value, 'i'))) {
nodes.push({path: path.concat([key]), isFile, title});
} else {
findNodesToKeys(children, value, path.concat([key]), nodes);
}
}
});
}
return nodes;
}
return(
<div className='wikiSidebar'>
<Search
allowClear
placeholder="输入关键字搜索文件"
className="wiki-search"
onSearch={changeSearchValue}
/>
{/* 目录是否全部展开 */}
<div className='expendedAllAction mt5' onClick={expandAll}>{isExpendedAll ? '收起' : '展开'}所有节点</div>
{/* wiki目录 */}
{menuList && menuList.length ? <DirectoryTree onSelect={selectTree} draggable={permission} onDrop={onDrop} selectedKeys={selectedKey} expandedKeys={expandedKeys} onExpand={onExpand}>
{renderTreeNodes(menuList)}
</DirectoryTree> : ''}
{/* 新增子目录弹窗 */}
<Modal title={isEditName ? `编辑${isEditName === 1 ? '目录' : '页面'}` : "新增目录"} visible={Boolean(visible)} onCancel={onCancel} onOk={submit} className="wikiAddMenu myself-modal" width={450} centered destroyOnClose>
<div className='mb10'>{isEditName === 2 ? '页面' : '目录'}名称: </div>
<Input placeholder={`请输入${isEditName === 2 ? '页面' : '目录'}名称`} value={menuName} onChange={(e)=>{setMenuName(e.target.value);setAddMenuError(undefined)}} maxLength={50} autoFocus></Input>
<div className='wikiAddMenuError mt5'>{addMenuError}</div>
</Modal>
</div>
);
}