forked from Gitlink/forgeplus-react
349 lines
13 KiB
JavaScript
349 lines
13 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
||
import { Button, Dropdown, Icon, Input, Menu, Select, message, Spin, Modal } from 'antd';
|
||
import { getImageUrl, timeAgo } from 'educoder';
|
||
import CopyTool from '../Component/CopyTool';
|
||
import Welcome from './Welcome';
|
||
import RenderHtml from "../../components/render-html";
|
||
|
||
import { wikiPages, getWiki, parseSidebar, updateWiki, treeToMd } from './api';
|
||
import { httpUrl } from './fetch';
|
||
import './Index.scss';
|
||
import './components/ModalFun/index.scss';
|
||
import { isArray } from 'lodash';
|
||
import { Base64 } from 'js-base64';
|
||
import Sidebar from './components/sidebar';
|
||
import UploadWiki from './components/uploadWiki';
|
||
import axios from 'axios';
|
||
const InputGroup = Input.Group;
|
||
const { Option } = Select;
|
||
|
||
export default (props) => {
|
||
const { match, history, showNotification, project, projectDetail } = props;
|
||
const {location:{pathname}} = history;
|
||
const permission = projectDetail && projectDetail.permission && projectDetail.permission !== "Reporter";
|
||
let projectsId = match.params.projectsId;
|
||
let owner = match.params.owner;
|
||
|
||
const [fileArrInit, setFileArrInit] = useState(null);
|
||
const [checkItem, setCheckItem] = useState({});
|
||
const [itemDetail, setItemDetail] = useState({});
|
||
|
||
const [fileArr, setFileArr] = useState([]);
|
||
const [fileList, setFileList] = useState([]);
|
||
const [reload, setReload] = useState();
|
||
|
||
const [urlType, setUrlType] = useState('HTTPS');
|
||
const [visible, setVisible] = useState(false);
|
||
const [menuName, setMenuName] = useState(undefined);
|
||
const [addMenuError, setAddMenuError] = useState(undefined);
|
||
const [sidebar, setSidebar] = useState(undefined);
|
||
const [defaultSelectedKeys, setDefaultSelectedKeys] = useState(undefined);
|
||
const [masterProtected, setMasterProtected] = useState(false);
|
||
|
||
useEffect(()=>{
|
||
window.scrollTo(0,0);
|
||
// 获取仓库分支信息,判断master是否设置了分支保护
|
||
if(projectsId && owner){
|
||
axios.get(`/v1/${owner}/${projectsId}/branches.json`, {keyword: 'master'}).then(res=>{
|
||
if(res && res.data && res.data.total_count === 1){
|
||
setMasterProtected(res.data.branches[0].protected);
|
||
}
|
||
})
|
||
}
|
||
}, [])
|
||
|
||
useEffect(()=>{
|
||
if(projectDetail){
|
||
const { author, name} = projectDetail;
|
||
if(Object.keys(checkItem).length === 0){
|
||
document.title = `维基-${author.name}/${name}`;
|
||
}else{
|
||
document.title = `${checkItem.name}-维基-${author.name}/${name}`;
|
||
}
|
||
}
|
||
}, [projectDetail, checkItem])
|
||
|
||
// 加载wiki列表
|
||
useEffect(() => {
|
||
project && wikiPages({
|
||
owner: owner,
|
||
repo: projectsId,
|
||
projectId: project.id
|
||
}).then(async res => {
|
||
if (res && res.message === "200" && isArray(res.data)) {
|
||
setFileArr(res.data);
|
||
setFileArrInit(res.data);
|
||
// 解析sidebar获取目录list
|
||
const info = await parseSidebar(res.data, {
|
||
owner: owner,
|
||
repo: projectsId,
|
||
projectId: project.id
|
||
})
|
||
if(!info){return}
|
||
const {menuList, sidebar} = info;
|
||
setSidebar(sidebar)
|
||
setFileList(menuList);
|
||
let firstWikiNode = undefined;
|
||
const arr = pathname.split('/');
|
||
if(!pathname.endsWith('/wiki')){
|
||
firstWikiNode = findNodeByName(menuList, arr[arr.length-2]);
|
||
} else{
|
||
// 找到目录树中第一个[[页面]]
|
||
firstWikiNode = findFirstWiki(menuList);
|
||
}
|
||
// 当前选中wiki被删除
|
||
!firstWikiNode && (firstWikiNode = findFirstWiki(menuList));
|
||
// 设置目录 选中节点初始化
|
||
setDefaultSelectedKeys([firstWikiNode.key+""]);
|
||
const title = firstWikiNode.title.trim().substring(2,firstWikiNode.title.trim().length-2);
|
||
const firstWiki = res.data.filter(item => { return title === item.name })[0];
|
||
setCheckItem({
|
||
...firstWiki,
|
||
key: firstWikiNode.key
|
||
})
|
||
history.push(`/${owner}/${projectsId}/wiki/${title}/${firstWikiNode.key}`);
|
||
} else {
|
||
setFileArr([]);
|
||
setFileArrInit([]);
|
||
}
|
||
});
|
||
}, [project, reload])
|
||
|
||
// 加载单个wiki详情,参数尽量用驼峰,此处由于后端太麻烦所以不标准
|
||
useEffect(() => {
|
||
project && checkItem.name && getWiki({
|
||
owner: owner,
|
||
repo: projectsId,
|
||
pageName: checkItem.name,
|
||
projectId: project.id
|
||
}).then(res => {
|
||
if (res && res.message === "200") {
|
||
setItemDetail(res.data);
|
||
} else {
|
||
showNotification("加载失败")
|
||
}
|
||
});
|
||
}, [project, checkItem]);
|
||
|
||
function findNodeByName(list, name){
|
||
for (let index = 0; index < list.length; index++) {
|
||
const element = list[index];
|
||
if (element.title.indexOf(`[[${name}]]`) !== -1) {
|
||
return element;
|
||
}else if (element.children.length) {
|
||
let result = findNodeByName(element.children, name);
|
||
if (result) {
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 初始化页面:递归menulist找到第一个页面
|
||
function findFirstWiki(list){
|
||
for (let index = 0; index < list.length; index++) {
|
||
const element = list[index];
|
||
if (element.title.trim().startsWith('[[')) {
|
||
return element;
|
||
}else if (element.children.length) {
|
||
let result = findFirstWiki(element.children);
|
||
if (result) {
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
function filterTree(list, value, arr = []){
|
||
if(!list.length) return []
|
||
for(let item of list){
|
||
const bool = item.title.match(new RegExp(value, 'i'));
|
||
let node = JSON.parse(JSON.stringify(item));
|
||
node.children = [];
|
||
bool && arr.push(node);
|
||
if(item.children && item.children.length){
|
||
filterTree(item.children, value, bool ? node.children : arr);
|
||
}
|
||
}
|
||
return arr
|
||
}
|
||
|
||
|
||
function goUser(login) {
|
||
window.location.href = `/${login}`;
|
||
}
|
||
|
||
function addFile() {
|
||
// 新增一级页面
|
||
history.push(`/${owner}/${projectsId}/wiki/add`);
|
||
}
|
||
|
||
// 新增目录
|
||
function addMenu(){
|
||
if(!menuName){
|
||
setAddMenuError('请输入目录名称');
|
||
return;
|
||
}
|
||
if(new RegExp("^[ ]+$").test(menuName)){
|
||
setAddMenuError("不能仅输入空格");
|
||
return;
|
||
}
|
||
const regEn = /[\\/:*?"<>|[\]-]/g;
|
||
if (regEn.test(menuName)) {
|
||
setAddMenuError("不能有特殊字符: \\ / : * ? \" < > | [ \] -");
|
||
return;
|
||
}
|
||
// 校验一级目录中是否有相同名称
|
||
const oneMenuList = fileList.filter(item=>!item.title.trim().startsWith('[[')).map(i=>i.title.trim())
|
||
if(oneMenuList.includes(`- ${menuName}`)){
|
||
setAddMenuError('不能与已有文件标题相同');
|
||
return;
|
||
}
|
||
|
||
// 重写fileList,再转换成md内容,最后保存sidebar
|
||
const newSidebarList = fileList.concat({title: `- ${menuName}`, children: []})
|
||
updateWikiFun(newSidebarList, 1);
|
||
}
|
||
|
||
// 更新wiki sidebar文件
|
||
function updateWikiFun(sidebarList, type){
|
||
updateWiki({
|
||
owner,
|
||
repo: projectsId,
|
||
projectId: project.id,
|
||
pageName: '_Sidebar',
|
||
title: '_Sidebar',
|
||
commit_message: '',
|
||
content_base64: Base64.encode(treeToMd(sidebarList))
|
||
}).then(res => {
|
||
if (res && res.message === "200") {
|
||
if(type === 1){
|
||
setVisible(false);
|
||
setMenuName(undefined);
|
||
message.success('操作成功');
|
||
}
|
||
// setFileList(sidebarList);
|
||
setReload(Math.random());
|
||
}else {
|
||
message.error(res.data || "操作失败");
|
||
}
|
||
});
|
||
}
|
||
|
||
function goEdit(params) {
|
||
history.push(`/${owner}/${projectsId}/wiki/${encodeURI(checkItem.name)}/${checkItem.key}/edit${params}`);
|
||
}
|
||
|
||
function preview() {
|
||
window.open(`/${owner}/${projectsId}/wiki/preview/${encodeURI(project.name)}/${project.id}`);
|
||
}
|
||
|
||
// 支持 Markdown,Html,Pdf格式文件
|
||
const menu = (
|
||
<Menu >
|
||
<Menu.Item key="1" onClick={() => { downloadWiki('markdown') }}>
|
||
Markdown
|
||
</Menu.Item>
|
||
<Menu.Item key="2" onClick={() => { downloadWiki('html') }}>
|
||
HTML
|
||
</Menu.Item>
|
||
<Menu.Item key="3" onClick={() => { downloadWiki('pdf') }}>
|
||
PDF
|
||
</Menu.Item>
|
||
</Menu>
|
||
);
|
||
|
||
function downloadWiki(type) {
|
||
window.open(`${httpUrl}/api/wikiExport/wikiExport-wrapper?repoName=${projectsId}&owner=${owner}&type=${type}&projectName=${project.name}&projectId=${project.id}`);
|
||
}
|
||
|
||
function changeitem(item){
|
||
const {name, key} = item;
|
||
const wiki = fileArrInit.filter(item => { return item.name == name })[0];
|
||
setCheckItem({
|
||
...wiki,
|
||
key
|
||
})
|
||
history.push(`/${owner}/${projectsId}/wiki/${name}/${key}`);
|
||
}
|
||
|
||
return (
|
||
<Spin spinning={!fileArrInit} className="opacitySpin">
|
||
{fileArrInit && fileArrInit.length ?
|
||
<div className="wiki-main">
|
||
<div className="wiki-head">
|
||
|
||
<span className="head-title">
|
||
{
|
||
permission ?
|
||
<div>
|
||
<Button type="default" onClick={()=>{setVisible(true)}} className='mr15'><Icon type="plus"/>新增目录</Button>
|
||
<Button type="default" onClick={addFile}><Icon type="plus" />新增页面</Button>
|
||
</div> : "Wiki文档"
|
||
}
|
||
</span>
|
||
<div>
|
||
{
|
||
permission && <UploadWiki {...props} fileList={fileList} setFileList={setFileList} fileArrInit={fileArrInit} setReload={setReload}/>
|
||
}
|
||
<Dropdown overlay={menu}>
|
||
<Button type="default" className="ml10">导出<Icon type="caret-down" /></Button>
|
||
</Dropdown>
|
||
<Button type="default" className="ml10" onClick={preview}>预览</Button>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div className="wiki-body">
|
||
<div className="wiki-nav-parent">
|
||
<div className='wiki-nav-scroll'>
|
||
<div className="wiki-nav">
|
||
|
||
{/* 读sidebar转换成目录树 */}
|
||
<Sidebar fileList = {fileList} changeitem={changeitem} projectsId={projectsId} owner={owner} project={project} setReload={setReload} sidebar={sidebar} history={history} permission={permission} defaultSelectedKeys={defaultSelectedKeys} fileArrInit={fileArrInit} masterProtected={masterProtected}/>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
{checkItem.wiki_clone_link && <InputGroup className="copy-url" compact>
|
||
<Select dropdownClassName="wiki-url-type" defaultValue="HTTPS" onChange={(v) => { setUrlType(v) }}>
|
||
<Option value="HTTPS">HTTPS</Option>
|
||
<Option value="SSH">SSH</Option>
|
||
</Select>
|
||
<Input id="wikiUrl" value={urlType === 'HTTPS' ? checkItem.wiki_clone_link.https : checkItem.wiki_clone_link.ssh} />
|
||
<CopyTool className="copy-wiki" inputId="wikiUrl" />
|
||
</InputGroup>}
|
||
|
||
</div>
|
||
|
||
<div className="wiki-content">
|
||
<div className="wiki-content-head">
|
||
<div className="wiki-content-head-left">
|
||
<h3 className="wiki-detail-title">{checkItem.name}</h3>
|
||
<span className="user-box mr10" onClick={() => { checkItem.commit && goUser(checkItem.commit.author.name) }}>
|
||
{itemDetail.image_url && <img alt="头像" className="head-log-small" src={getImageUrl(`/${itemDetail.image_url}`)} />}
|
||
<span className='font-14'>{itemDetail.userName}</span>
|
||
</span>
|
||
<span className="time-ago font-14">上次修改于{checkItem.commit ? timeAgo(checkItem.commit.author.date) : '刚刚'}</span>
|
||
</div>
|
||
{permission && <Button type="primary" ghost onClick={()=>{goEdit(`?copy`)}} className='mr10'>复制</Button>}
|
||
{permission && <Button type="primary" onClick={()=>{goEdit("")}}>编辑</Button>}
|
||
</div>
|
||
|
||
{itemDetail&&itemDetail.md_content&&<RenderHtml className="wiki-content-detail editor-content-panel imageLayerParent" value={ itemDetail && Base64.decode(itemDetail.md_content) } url={history.location}/>}
|
||
</div>
|
||
</div>
|
||
</div >
|
||
:
|
||
<Welcome {...props} reloadList={setReload} fileList={fileList} setFileList={setFileList} fileArrInit={fileArrInit} setReload={setReload}/>
|
||
}
|
||
<Modal title="新增目录" visible={visible} onCancel={()=>{setVisible(false);setAddMenuError(undefined);setMenuName(undefined);}} onOk={addMenu} className="wikiAddMenu myself-modal" width={450} centered>
|
||
<div className='mb10'>目录名称: </div>
|
||
<Input placeholder='请输入目录名称' value={menuName} onChange={(e)=>{setMenuName(e.target.value);setAddMenuError(undefined)}} maxLength={50} autoFocus></Input>
|
||
<div className='wikiAddMenuError mt5'>{addMenuError}</div>
|
||
</Modal>
|
||
</Spin>
|
||
)
|
||
}
|