12.0 feature edit wiki page (#6033)
* 01 add static media files * 02 edit wiki page * 03 remove useless icons and support edit name and icon * 04 support wiki nav * delete useless codes * optimize feature edit wiki * 05 add wiki-api and save config * 06 change some UI * delete useless codes * delete useless codes * fix edit wiki url * change js path * change icon size * fix * fix fn name and do not show tree view * fix edit url * save config to index.json * fix new file name check * hide icon and library name * remove useless svgs * remove useless svgs --------- Co-authored-by: ‘JoinTyang’ <yangtong1009@163.com> Co-authored-by: JoinTyang <41655440+JoinTyang@users.noreply.github.com>
17
frontend/src/assets/icons/copy.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>copy</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="copy">
|
||||
<g id="_x31_复制" transform="translate(2.000000, 2.000000)">
|
||||
<path id="路径" class="st0" d="M21,23v3c0,1-1,2-2,2H2.6C1.3,28,0,26.6,0,25.4V9.2C0,8,1.1,7,2.2,7H5v14c0,1.1,0.6,2,1.9,2
|
||||
C7.5,23,20.9,23,21,23z"/>
|
||||
<path id="矩形" class="st0" d="M9,0h17c1.1,0,2,0.9,2,2v17c0,1.1-0.9,2-2,2H9c-1.1,0-2-0.9-2-2V2C7,0.9,7.9,0,9,0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 822 B |
18
frontend/src/assets/icons/delete.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>delete</title>
|
||||
<g id="delete">
|
||||
<g id="形状" transform="translate(1.000000, 1.000000)">
|
||||
<path class="st0" d="M19.4,0c0.8,0,1.6,0.7,1.7,1.5l0.4,2.7h7.2c0.7,0,1.3,0.6,1.3,1.2s-0.6,1.2-1.3,1.2h-2.5v20
|
||||
c0,0.8-0.4,1.7-1,2.4c-0.7,0.7-1.5,0.9-2.4,0.9H7.3c-0.9,0-1.8-0.4-2.4-0.9c-0.7-0.7-1-1.5-1-2.4v-20H1.3C0.6,6.7,0,6.1,0,5.5
|
||||
s0.6-1.2,1.3-1.2h7.1V4.1l0.4-2.7C8.9,0.7,9.6,0,10.5,0H19.4z M18.3,11.2c-0.8,0-1.4,0.6-1.4,1.4l0,0V23c0,0.8,0.6,1.4,1.4,1.4
|
||||
s1.4-0.6,1.4-1.4l0,0V12.7C19.7,11.9,19.1,11.2,18.3,11.2z M11.7,11.2c-0.8,0-1.4,0.6-1.4,1.4l0,0V23c0,0.8,0.6,1.4,1.4,1.4
|
||||
s1.4-0.6,1.4-1.4l0,0V12.7C13.1,11.9,12.5,11.2,11.7,11.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
15
frontend/src/assets/icons/drag.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>drag</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="drag">
|
||||
<g id="形状结合" transform="translate(8.000000, 3.000000)">
|
||||
<path class="st0" d="M0,0h6v6H0V0z M10,0h6v6h-6V0z M10,10h6v6h-6V10z M0,10h6v6H0V10z M0,20h6v6H0V20z M10,20h6v6h-6V20z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 675 B |
15
frontend/src/assets/icons/drop-down.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>drop-down</title>
|
||||
<g id="drop-down">
|
||||
<g id="下拉" transform="translate(2.000000, 6.000000)">
|
||||
<path id="路径" class="st0" d="M15.7,19.2L27.4,4.5C28.8,2.8,27.7,0,25.7,0H2.3c-2,0-3.1,2.7-1.7,4.5l11.7,14.7
|
||||
C13.2,20.3,14.8,20.3,15.7,19.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 672 B |
13
frontend/src/assets/icons/edit.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<path class="st0" d="M21.6,8l4.4,4.4c0.2,0.2,0.2,0.5,0,0.7L15.3,23.7l-4.5,0.5c-0.6,0.1-1.1-0.4-1-1l0.5-4.5L20.9,8
|
||||
C21.1,7.9,21.4,7.9,21.6,8z M29.4,6.9L27,4.5c-0.7-0.7-1.9-0.7-2.7,0l-1.7,1.7c-0.2,0.2-0.2,0.5,0,0.7l4.4,4.4
|
||||
c0.2,0.2,0.5,0.2,0.7,0l1.7-1.7C30.2,8.9,30.2,7.7,29.4,6.9z M20.7,20.8v4.9H5.1V10.2h11.2c0.2,0,0.3-0.1,0.4-0.2l1.9-1.9
|
||||
c0.4-0.4,0.1-1-0.4-1H4.3C3,7.1,2,8.2,2,9.4v17.1c0,1.3,1,2.3,2.3,2.3h17.1c1.3,0,2.3-1,2.3-2.3v-7.7c0-0.5-0.6-0.8-1-0.4l-1.9,1.9
|
||||
C20.7,20.5,20.7,20.7,20.7,20.8z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 912 B |
19
frontend/src/assets/icons/file.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>file1</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="file1">
|
||||
<g id="编组-5">
|
||||
<g id="文件" transform="translate(3.000000, 0.000000)">
|
||||
<path id="形状" class="st0" d="M25.6,6.9l-6.7-6.6C18.6,0,18.3,0,18,0H4C2.1,0,1,2.3,1,3.9C1,4.9,1,13.3,1,29c0,1,1.3,3,3,3
|
||||
s17.4,0,19.1,0s2.9-1.4,2.9-3c0-1,0-8.2,0-21.3C26,7.4,25.7,7,25.6,6.9z M17.9,3.7L22.3,8H18V3.7H17.9z M22.8,29H4V3h11v5.3
|
||||
c0,1.8,0.8,2.7,2.9,2.7H23v18H22.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 866 B |
16
frontend/src/assets/icons/folders.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>folders</title>
|
||||
<g id="folders">
|
||||
<g id="形状结合" transform="translate(1.000000, 3.000000)">
|
||||
<path class="st0" d="M28.6,8.4C29.4,8.4,30,9,30,9.7l0,0l-2.7,15.2v0c0,0.4-0.2,0.7-0.4,1c-0.3,0.3-0.6,0.4-1,0.4l0,0H3.2
|
||||
c-0.7,0-1.4-0.6-1.4-1.3l0,0L4.6,9.8v0c0-0.4,0.2-0.7,0.4-1c0.3-0.3,0.6-0.4,1-0.4l0,0H28.6z M9.9,0l2.5,2.6h12.4
|
||||
c0.9,0,1.7,0.8,1.7,1.8l0,0v2.2h-22c-1,0-1.8,0.8-1.8,1.8l0,0L0,23V1.8C0,0.8,0.7,0,1.7,0l0,0H9.9z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 859 B |
16
frontend/src/assets/icons/left-slide.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>left-slide</title>
|
||||
<g id="left-slide">
|
||||
|
||||
<g id="下拉" transform="translate(16.000000, 16.000000) rotate(90.000000) translate(-16.000000, -16.000000) translate(2.000000, 6.000000)">
|
||||
<path id="路径" class="st0" d="M15.7,19.2L27.4,4.5C28.8,2.8,27.7,0,25.7,0H2.3c-2,0-3.1,2.7-1.7,4.5l11.7,14.7
|
||||
C13.2,20.3,14.8,20.3,15.7,19.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 761 B |
23
frontend/src/assets/icons/main-view.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>main-view</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Symbols">
|
||||
<g id="main-view">
|
||||
<g id="形状结合" transform="translate(1.071429, 1.071429)">
|
||||
<path class="st0" d="M2,0h26c1.1,0,2,0.9,2,2v26c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2V2C0,0.9,0.9,0,2,0z M4.7,3.2
|
||||
c-0.8,0-1.5,0.7-1.5,1.5v1.7c0,0.8,0.7,1.5,1.5,1.5h4.9c0.8,0,1.5-0.7,1.5-1.5V4.7c0-0.8-0.7-1.5-1.5-1.5H4.7z M4.7,11.1
|
||||
c-0.8,0-1.5,0.7-1.5,1.5v3.3c0,0.8,0.7,1.5,1.5,1.5h4.9c0.8,0,1.5-0.7,1.5-1.5v-3.3c0-0.8-0.7-1.5-1.5-1.5H4.7z M4.7,20.5
|
||||
c-0.8,0-1.5,0.7-1.5,1.5v3.3c0,0.8,0.7,1.5,1.5,1.5h4.9c0.8,0,1.5-0.7,1.5-1.5V22c0-0.8-0.7-1.5-1.5-1.5H4.7z M14.1,11.1
|
||||
c-0.8,0-1.5,0.7-1.5,1.5v3.3c0,0.8,0.7,1.5,1.5,1.5h11.2c0.8,0,1.5-0.7,1.5-1.5v-3.3c0-0.8-0.7-1.5-1.5-1.5H14.1z M14.1,20.5
|
||||
c-0.8,0-1.5,0.7-1.5,1.5v3.3c0,0.8,0.7,1.5,1.5,1.5h11.2c0.8,0,1.5-0.7,1.5-1.5V22c0-0.8-0.7-1.5-1.5-1.5H14.1z M14.1,3.2
|
||||
c-0.8,0-1.5,0.7-1.5,1.5v1.7c0,0.8,0.7,1.5,1.5,1.5h11.2c0.8,0,1.5-0.7,1.5-1.5V4.7c0-0.8-0.7-1.5-1.5-1.5H14.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
15
frontend/src/assets/icons/more-level.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#929292;}
|
||||
</style>
|
||||
<title>more</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="more">
|
||||
<path id="形状结合" class="st0" d="M24.3,16c0-2,1.7-3.7,3.7-3.7s3.7,1.7,3.7,3.7S30,19.7,28,19.7S24.3,18,24.3,16z M12,16
|
||||
c0-2,1.7-3.7,3.7-3.7s3.7,1.7,3.7,3.7s-1.7,3.7-3.7,3.7S12,18,12,16z M0,16c0-2,1.3-3.7,3.4-3.7s4,1.7,4,3.7s-1.9,3.7-4,3.7
|
||||
S0,18,0,16z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 744 B |
15
frontend/src/assets/icons/more-vertical.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#929292;}
|
||||
</style>
|
||||
<title>more</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="more">
|
||||
<path id="形状结合" class="st0" d="M15.7,7.4c-2,0-3.7-1.7-3.7-3.7S13.7,0,15.7,0s3.7,1.7,3.7,3.7S17.7,7.4,15.7,7.4z
|
||||
M15.7,19.7c-2,0-3.7-1.7-3.7-3.7s1.7-3.7,3.7-3.7s3.7,1.7,3.7,3.7S17.7,19.7,15.7,19.7z M15.7,32c-2,0-3.7-1.7-3.7-3.7
|
||||
s1.7-3.7,3.7-3.7s3.7,1.7,3.7,3.7S17.7,32,15.7,32z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 773 B |
20
frontend/src/assets/icons/move-to.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>move-to</title>
|
||||
<g id="move-to-">
|
||||
<g id="export" transform="translate(2.000000, 3.000000)">
|
||||
<path id="形状结合" fill="currentColor" class="st0" d="M10.7,0c0.4,0,0.8,0.1,1.4,0.4c0.7,0.3,1.1,0.7,1.5,1s0.6,0.7,0.8,1
|
||||
c0.2,0.3,0.3,0.5,0.6,0.6c0.2,0.1,0.5,0.2,0.9,0.2h8.5c1,0,1.9,0.3,2.5,0.9C27.5,4.8,28,5.5,28,6.4v16.3c0,1-0.4,1.7-1.1,2.3
|
||||
S25.4,26,24.4,26H3.5c-1.1,0-1.8-0.2-2.5-1c-0.7-0.8-1-1.4-1-2.3l0-10.3c0-0.1,0-0.2,0-0.4l0-8.8l0,0c0-1,0.5-1.6,1.1-2.2
|
||||
C1.8,0.3,2.4,0,3.1,0H10.7z M17.4,9.6c-0.8-0.8-1.6-0.8-2.4,0l0,0l-0.1,0.1c-0.7,0.8-0.6,1.5,0.1,2.3l0,0l1.1,1.1l-8.4,0
|
||||
c-1.1,0-1.7,0.6-1.7,1.7l0,0L6,15c0.1,1,0.6,1.5,1.7,1.5l0,0l8.3,0l-1.1,1.1c-0.8,0.8-0.8,1.6,0,2.4l0,0l0.1,0.1
|
||||
c0.8,0.7,1.5,0.6,2.3-0.1l0,0l3.6-3.6c0.1-0.1,0.3-0.2,0.4-0.3l0,0l0.1-0.1c0.6-0.7,0.6-1.4,0.1-2c-0.1-0.1-0.2-0.2-0.3-0.3l0,0
|
||||
l-0.1-0.1l0,0l0,0L17.4,9.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
17
frontend/src/assets/icons/remove-from-folder.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>remove-from-folder</title>
|
||||
<g id="remove-from-folder">
|
||||
<g id="export" transform="translate(2.000000, 3.000000)">
|
||||
<path id="形状结合" fill="currentColor" class="st0" d="M10.7,0c0.4,0,0.8,0.1,1.4,0.4c0.7,0.3,1.1,0.7,1.5,1s0.6,0.7,0.8,1
|
||||
c0.2,0.3,0.3,0.5,0.6,0.6c0.2,0.1,0.5,0.2,0.9,0.2h8.5c1,0,1.9,0.3,2.5,0.9C27.5,4.8,28,5.5,28,6.4v16.3c0,1-0.4,1.7-1.1,2.3
|
||||
S25.4,26,24.4,26H3.5c-1.1,0-1.8-0.2-2.5-1c-0.7-0.8-1-1.4-1-2.3l0-10.3c0-0.1,0-0.2,0-0.4l0-8.8l0,0c0-1,0.5-1.6,1.1-2.2
|
||||
C1.8,0.3,2.4,0,3.1,0H10.7z M20,13H8c-1.1,0-2,0.9-2,2s0.9,2,2,2l0,0h12c1.1,0,2-0.9,2-2S21.1,13,20,13L20,13z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
16
frontend/src/assets/icons/right-slide.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>right-slide</title>
|
||||
<g id="right-slide">
|
||||
|
||||
<g id="下拉" transform="translate(16.000000, 16.000000) rotate(-90.000000) translate(-16.000000, -16.000000) translate(2.000000, 6.000000)">
|
||||
<path id="路径" class="st0" d="M15.7,19.2L27.4,4.5C28.8,2.8,27.7,0,25.7,0H2.3c-2,0-3.1,2.7-1.7,4.5l11.7,14.7
|
||||
C13.2,20.3,14.8,20.3,15.7,19.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 764 B |
16
frontend/src/assets/icons/table.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>table</title>
|
||||
<g id="table">
|
||||
<g id="form">
|
||||
<path id="形状结合" class="st0" d="M27,2c1.7,0,3,1.3,3,3v22c0,1.7-1.3,3-3,3H5c-1.7,0-3-1.3-3-3V5c0-1.7,1.3-3,3-3H27z
|
||||
M10,22H5v4c0,0.5,0.4,0.9,0.9,1L6,27h4V22z M19,22h-6v5h6V22z M27,22h-5v5h4c0.5,0,0.9-0.4,1-0.9l0-0.1V22z M10,13H5v6h5V13z
|
||||
M19,13h-6v6h6V13z M27,13h-5v6h5V13z M26,5H6C5.5,5,5.1,5.4,5,5.9L5,6v4h22V6C27,5.4,26.6,5,26,5z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 819 B |
16
frontend/src/assets/icons/upward.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<title>upward</title>
|
||||
<g id="upward">
|
||||
|
||||
<g id="下拉" transform="translate(16.000000, 16.000000) scale(1, -1) translate(-16.000000, -16.000000) translate(2.000000, 6.000000)">
|
||||
<path id="路径" class="st0" d="M15.7,19.2L27.4,4.5C28.8,2.8,27.7,0,25.7,0H2.3c-2,0-3.1,2.7-1.7,4.5l11.7,14.7
|
||||
C13.2,20.3,14.8,20.3,15.7,19.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 748 B |
13
frontend/src/assets/icons/wiki-preview.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#999999;}
|
||||
</style>
|
||||
<path class="st0" d="M16,4c3.1,0,6.1,1,8.9,2.9s5.1,4.5,6.7,7.6c0.3,0.4,0.4,1,0.4,1.4s-0.1,1-0.4,1.4c-1.7,3.2-3.9,5.7-6.7,7.6
|
||||
c-2.8,2-5.8,3.1-8.9,3.1s-6.1-1-8.9-2.9S2,20.6,0.4,17.4C0.1,17.1,0,16.4,0,16s0.1-1,0.4-1.4c1.6-3.2,4-5.7,6.7-7.7S12.9,4,16,4z
|
||||
M16,7c-2.5,0-4.9,0.7-7.2,2.2s-4.2,3.4-5.5,5.7C3.1,15.3,3,15.7,3,16c0,0.3,0.1,0.8,0.3,1.1c1.3,2.4,3.2,4.3,5.5,5.7S13.5,25,16,25
|
||||
s4.9-0.8,7.2-2.2c2.3-1.5,4.1-3.4,5.5-5.7C29,16.8,29,16.4,29,16s-0.1-0.7-0.3-1.1c-1.3-2.4-3.2-4.3-5.5-5.7S18.5,7,16,7z M16,10
|
||||
c3.3,0,6,2.7,6,6s-2.7,6-6,6s-6-2.7-6-6S12.7,10,16,10z M16,13c-1.7,0-3,1.3-3,3s1.3,3,3,3s3-1.3,3-3S17.7,13,16,13z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
22
frontend/src/assets/icons/wiki-settings.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#949494;}
|
||||
</style>
|
||||
<path class="st0" d="M31.7,13c-0.3-1.5-1.3-2.4-2.7-2.4s-2.5-1.1-2.5-2.5c0-0.3,0.2-0.7,0.2-0.9c0.6-1.3,0.1-2.9-1.1-3.7l-3.8-2.1
|
||||
l-0.2-0.1c-0.3-0.1-0.7-0.2-1.1-0.2c-0.9,0-1.8,0.3-2.4,0.9s-1.6,1.2-2,1.2c-0.5,0-1.5-0.7-2-1.3c-0.9-0.9-2.3-1.2-3.6-0.7l-4,2.1
|
||||
L6.3,3.6C5.1,4.4,4.7,6,5.3,7.3c0.1,0.2,0.2,0.6,0.2,0.9c0,1.4-1.1,2.5-2.6,2.5c-1.3,0-2.3,1-2.6,2.4c-0.1,0.4-0.3,1.8-0.3,3
|
||||
s0.2,2.6,0.3,3.1c0.3,1.4,1.3,2.4,2.6,2.4H3c1.4,0,2.5,1.1,2.5,2.4c0,0.3-0.1,0.7-0.2,0.9c-0.6,1.3-0.1,2.9,1,3.7l3.7,2.1l0.2,0.1
|
||||
C10.6,31,11,31,11.4,31c0.9,0,1.8-0.4,2.4-1c0.6-0.6,1.6-1.3,2.1-1.3s1.5,0.7,2.1,1.3s1.5,1,2.4,1c0.4,0,0.8-0.1,1.2-0.2l3.9-2.1
|
||||
l0.2-0.1c1.2-0.8,1.6-2.4,1-3.7c-0.1-0.2-0.2-0.6-0.2-0.9c0-1.4,1.1-2.4,2.6-2.4c1.3,0,2.3-0.9,2.6-2.4c0.1-0.4,0.3-1.9,0.3-3.1
|
||||
S31.8,13.5,31.7,13z M28.8,18.6c-0.1,0.3-0.2,0.6-0.4,0.6c-2.6,0-4.6,1.9-4.6,4.4c0,0.7,0.3,1.5,0.4,1.7c0.1,0.3,0,0.7-0.2,0.9
|
||||
l-3.5,1.9h-0.2c-0.2,0-0.5-0.1-0.6-0.3c-0.2-0.2-1.9-2-3.6-2s-3.6,1.9-3.6,1.9c-0.2,0.2-0.6,0.3-0.9,0.2L8,26.1
|
||||
c-0.2-0.2-0.3-0.5-0.2-0.8c0.1-0.2,0.4-0.9,0.4-1.7c0-2.4-2-4.4-4.5-4.4H3.6c-0.1,0-0.2-0.2-0.3-0.6C3.2,18.3,3,17.1,3,16.1
|
||||
c0-0.8,0.1-1.8,0.3-2.5c0.1-0.4,0.2-0.5,0.3-0.6h0.1c2.6,0,4.6-1.9,4.6-4.4c0-0.8-0.3-1.5-0.4-1.7C7.7,6.6,7.8,6.3,8.1,6.1l3.6-2
|
||||
c0.3-0.1,0.7,0,0.9,0.2c0.1,0.1,1.8,1.8,3.5,1.8s3.4-1.7,3.5-1.8c0.1-0.1,0.4-0.2,0.6-0.2h0.3l3.4,2c0.2,0.2,0.3,0.6,0.2,0.9
|
||||
c-0.1,0.2-0.4,0.9-0.4,1.7c0,2.4,2,4.4,4.5,4.4c0.3,0,0.4,0.4,0.4,0.6c0.1,0.7,0.3,1.7,0.3,2.5C29.1,16.9,29,17.9,28.8,18.6z"/>
|
||||
<path class="st0" d="M16.1,10c-2.2-0.1-4.2,1.1-5.3,3s-1.1,4.2,0,6s3.1,3,5.3,3c3.3-0.1,5.9-2.7,5.9-5.9C22,12.7,19.4,10.1,16.1,10z
|
||||
M16,19c-1.7,0-3-1.3-3-3s1.3-3,3-3s3,1.3,3,3S17.7,19,16,19z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
@@ -14,6 +14,7 @@ import '../../css/file-chooser.css';
|
||||
|
||||
const propTypes = {
|
||||
isShowFile: PropTypes.bool,
|
||||
hideLibraryName: PropTypes.bool,
|
||||
repoID: PropTypes.string,
|
||||
onDirentItemClick: PropTypes.func,
|
||||
onRepoItemClick: PropTypes.func,
|
||||
@@ -371,7 +372,6 @@ class FileChooser extends React.Component {
|
||||
};
|
||||
|
||||
renderRepoListView = () => {
|
||||
|
||||
return (
|
||||
<div className="file-chooser-container user-select-none" onScroll={this.onScroll}>
|
||||
{this.props.mode === 'current_repo_and_other_repos' && (
|
||||
@@ -421,10 +421,12 @@ class FileChooser extends React.Component {
|
||||
)}
|
||||
{this.props.mode === 'only_current_library' && (
|
||||
<div className="list-view">
|
||||
{!this.props.hideLibraryName &&
|
||||
<div className="list-view-header">
|
||||
<span className={`item-toggle fa ${this.state.isCurrentRepoShow ? 'fa-caret-down' : 'fa-caret-right'}`} onClick={this.onCurrentRepoToggle}></span>
|
||||
<span className="library">{gettext('Current Library')}</span>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
this.state.isCurrentRepoShow && this.state.currentRepoInfo &&
|
||||
<RepoListView
|
||||
@@ -438,6 +440,7 @@ class FileChooser extends React.Component {
|
||||
isShowFile={this.props.isShowFile}
|
||||
fileSuffixes={this.props.fileSuffixes}
|
||||
selectedItemInfo={this.state.selectedItemInfo}
|
||||
hideLibraryName={this.props.hideLibraryName}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
@@ -21,6 +21,7 @@ const propTypes = {
|
||||
onRepoItemClick: PropTypes.func.isRequired,
|
||||
fileSuffixes: PropTypes.array,
|
||||
selectedItemInfo: PropTypes.object,
|
||||
hideLibraryName: PropTypes.bool,
|
||||
};
|
||||
|
||||
class RepoListItem extends React.Component {
|
||||
@@ -199,17 +200,19 @@ class RepoListItem extends React.Component {
|
||||
|
||||
return (
|
||||
<li>
|
||||
<div className={`${repoActive ? 'item-active' : ''} item-info`} onClick={this.onRepoItemClick}>
|
||||
<div className="item-text">
|
||||
<span className="name user-select-none ellipsis" title={this.props.repo.repo_name}>{this.props.repo.repo_name}</span>
|
||||
{!this.props.hideLibraryName &&
|
||||
<div className={`${repoActive ? 'item-active' : ''} item-info`} onClick={this.onRepoItemClick}>
|
||||
<div className="item-text">
|
||||
<span className="name user-select-none ellipsis" title={this.props.repo.repo_name}>{this.props.repo.repo_name}</span>
|
||||
</div>
|
||||
<div className="item-left-icon">
|
||||
<span className={`item-toggle icon fa ${this.state.isShowChildren ? 'fa-caret-down' : 'fa-caret-right'}`} onClick={this.onToggleClick}></span>
|
||||
<i className="tree-node-icon">
|
||||
<span className="icon far fa-folder tree-node-icon"></span>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item-left-icon">
|
||||
<span className={`item-toggle icon fa ${this.state.isShowChildren ? 'fa-caret-down' : 'fa-caret-right'}`} onClick={this.onToggleClick}></span>
|
||||
<i className="tree-node-icon">
|
||||
<span className="icon far fa-folder tree-node-icon"></span>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{this.state.isShowChildren && (
|
||||
<TreeListView
|
||||
repo={this.props.repo}
|
||||
|
@@ -15,6 +15,7 @@ const propTypes = {
|
||||
fileSuffixes: PropTypes.array,
|
||||
selectedItemInfo: PropTypes.object,
|
||||
currentPath: PropTypes.string,
|
||||
hideLibraryName: PropTypes.bool,
|
||||
};
|
||||
|
||||
class RepoListView extends React.Component {
|
||||
@@ -25,8 +26,12 @@ class RepoListView extends React.Component {
|
||||
repoList = [];
|
||||
repoList.push(currentRepoInfo);
|
||||
}
|
||||
let style = {};
|
||||
if (this.props.hideLibraryName) {
|
||||
style = { marginLeft: '-44px' };
|
||||
}
|
||||
return (
|
||||
<ul className="list-view-content file-chooser-item">
|
||||
<ul className="list-view-content file-chooser-item" style={style}>
|
||||
{repoList.length > 0 && repoList.map((repoItem, index) => {
|
||||
return (
|
||||
<RepoListItem
|
||||
@@ -42,6 +47,7 @@ class RepoListView extends React.Component {
|
||||
isShowFile={this.props.isShowFile}
|
||||
fileSuffixes={this.props.fileSuffixes}
|
||||
selectedItemInfo={this.props.selectedItemInfo}
|
||||
hideLibraryName={this.props.hideLibraryName}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
@@ -136,10 +136,14 @@ class WikiListItem extends Component {
|
||||
let wiki = this.props.wiki;
|
||||
let userProfileURL = `${siteRoot}profile/${encodeURIComponent(wiki.owner)}/`;
|
||||
let fileIconUrl = Utils.getDefaultLibIconUrl(false);
|
||||
let deleteIcon = `action-icon sf2-icon-x3 ${this.state.highlight ? '' : 'invisible'}`;
|
||||
|
||||
const desktopItem = (
|
||||
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
||||
<tr
|
||||
className={this.state.highlight ? 'tr-highlight' : ''}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onFocus={this.onMouseEnter}
|
||||
>
|
||||
<td><img src={fileIconUrl} width="24" alt="" /></td>
|
||||
<td className="name">
|
||||
<a href={wiki.link}>{wiki.name}</a>
|
||||
@@ -151,13 +155,27 @@ class WikiListItem extends Component {
|
||||
<td><a href={userProfileURL} target='_blank' rel="noreferrer">{wiki.owner_nickname}</a></td>
|
||||
<td>{moment(wiki.updated_at).fromNow()}</td>
|
||||
<td className="text-center cursor-pointer">
|
||||
<a href="#" role="button" aria-label={gettext('Unpublish')} title={gettext('Unpublish')} className={deleteIcon} onClick={this.onDeleteToggle}></a>
|
||||
<span
|
||||
className={`iconfont icon-edit mr-4 action-icon ${this.state.highlight ? '' : 'invisible'}`}
|
||||
onClick={() => window.open(wiki.link.replace('/published/', '/edit-wiki/'))}
|
||||
title={gettext('Edit')}
|
||||
aria-label={gettext('Edit')}
|
||||
style={{color: '#999'}}
|
||||
></span>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
aria-label={gettext('Unpublish')}
|
||||
title={gettext('Unpublish')}
|
||||
className={`action-icon sf2-icon-x3 ${this.state.highlight ? '' : 'invisible'}`}
|
||||
onClick={this.onDeleteToggle}
|
||||
></a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
const mobileItem = (
|
||||
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
<tr>
|
||||
<td><img src={fileIconUrl} width="24" alt="" /></td>
|
||||
<td>
|
||||
<a href={wiki.link}>{wiki.name}</a><br />
|
||||
@@ -186,7 +204,7 @@ class WikiListItem extends Component {
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<>
|
||||
{Utils.isDesktop() ? desktopItem : mobileItem}
|
||||
{this.state.isShowDeleteDialog &&
|
||||
<ModalPortal>
|
||||
@@ -196,7 +214,7 @@ class WikiListItem extends Component {
|
||||
/>
|
||||
</ModalPortal>
|
||||
}
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
90
frontend/src/pages/wiki/css/add-page-dialog.css
Normal file
@@ -0,0 +1,90 @@
|
||||
.add-page-dialog {
|
||||
width: 506px;
|
||||
max-width: 506px;
|
||||
}
|
||||
|
||||
.add-page-dialog .modal-content {
|
||||
height: 100%;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.add-page-dialog .modal-content .modal-body {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-pages {
|
||||
margin: -8px 0;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-pages .app-select-page-item {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
width: 88px;
|
||||
flex-shrink: 0;
|
||||
margin: 8px 8px 8px 0px;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-pages .app-select-page-item:nth-child(5n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-pages .app-select-page-item:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item .app-select-page-item-image-container {
|
||||
height: 64px;
|
||||
width: 100%;
|
||||
border: 1px solid #e9e9e9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item .app-select-page-item-image-container .app-select-page-item-image {
|
||||
width: 86px;
|
||||
height: 62px;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item.selected .app-select-page-item-image-container,
|
||||
.add-page-dialog .app-select-page-item.selected .app-select-page-item-image-container:hover {
|
||||
border-color: #ff8000;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item .app-select-page-item-image-container:hover {
|
||||
border-color: #bdbdbd;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item .app-select-page-item-name {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.add-page-dialog .app-select-page-item.selected .app-select-page-item-name {
|
||||
color: #ff8000;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover {
|
||||
width: 204px;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover .app-select-page-item-popover-container {
|
||||
padding-left: 13px;
|
||||
padding-right: 13px;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover .app-select-page-item-popover-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover .app-select-page-item-popover-tip {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.app-select-page-item-popover .popover .app-select-page-item-popover-image-container {
|
||||
border: 2px solid #ff8000;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
76
frontend/src/pages/wiki/css/view-edit-popover.css
Normal file
@@ -0,0 +1,76 @@
|
||||
.view-edit-popover .popover {
|
||||
max-width: 460px;
|
||||
width: 460px;
|
||||
left: 140px !important;
|
||||
}
|
||||
|
||||
.view-edit-popover .view-edit-content {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.view-edit-popover .view-name-editor {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.view-edit-popover .view-name-editor-input {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.view-edit-popover .view-icon-editor {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.view-edit-popover .view-icon-item-editor {
|
||||
overflow: hidden;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.view-edit-popover .view-icon-item-editor .svg-item {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.view-edit-popover .view-icon-item-editor .view-icon-color-white {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.view-edit-popover .view-icon-item-editor:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.view-edit-popover .view-icon-item-editor .view-icon-item-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-edit-popover-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid rgba(0, 40, 100, 0.12);
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.view-edit-popover-header .header-text {
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.view-edit-popover-header .remove-icon-button {
|
||||
color: #ff8000;
|
||||
}
|
||||
|
||||
.view-edit-popover-header .remove-icon-button:hover {
|
||||
cursor: pointer;
|
||||
}
|
402
frontend/src/pages/wiki/css/view-structure.css
Normal file
@@ -0,0 +1,402 @@
|
||||
.view-structure {
|
||||
height: calc(100% - 50px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.views-structure-header {
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
padding: 0.25rem 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.view-structure-body {
|
||||
margin-top: 10px;
|
||||
padding-bottom: 0.5rem;
|
||||
overflow: auto;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.view-structure .view-folder {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-structure .view-folder.can-drop::after,
|
||||
.view-structure .view-folder.can-drop-top::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 40px;
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.view-structure .view-folder .view-folder-children {
|
||||
transition: height 0.25s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.view-structure .view-drop-target {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
border-bottom: 1px solid #666;
|
||||
}
|
||||
|
||||
.view-structure .view-folder.can-drop::after {
|
||||
top: unset;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.view-structure .view-folder.can-drop-top::after {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.view-structure .view-folder-wrapper,
|
||||
.view-structure .view-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 8px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.view-structure .view-item.selected-view,
|
||||
.view-structure .view-folder-wrapper:hover,
|
||||
.view-structure .view-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.view-structure .folder-main,
|
||||
.view-structure .view-item-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view-structure .more-views .folder-main {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.view-structure .rdg-drag-handle {
|
||||
width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.view-structure .rdg-drag-handle:hover {
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.view-structure .view-folder-wrapper:hover .rdg-drag-handle,
|
||||
.view-structure .view-item:hover .rdg-drag-handle {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.view-structure .view-folder-wrapper .icon-expand-folder {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
transform: scale(0.8);
|
||||
color: #b5b5b5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-structure .folder-content,
|
||||
.view-structure .view-content {
|
||||
height: 100%;
|
||||
padding-right: 8px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view-structure .in-folder .view-content {
|
||||
padding-left: calc(12 * 0.8px + 0.5rem);
|
||||
}
|
||||
|
||||
.view-structure .folder-content:hover,
|
||||
.view-structure .view-content:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-structure .folder-content .dtable-font,
|
||||
.view-structure .view-content .dtable-font {
|
||||
margin-right: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.view-structure .folder-content .folder-name,
|
||||
.view-structure .view-content .view-title {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.view-structure .more-view-folder-operation,
|
||||
.view-structure .more-view-operation {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.view-structure .view-folder-wrapper .more-view-folder-operation .seafile-multicolor-icon-more-level,
|
||||
.view-structure .view-item .more-view-operation .seafile-multicolor-icon-more-level {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.view-structure .view-folder-wrapper:hover .more-view-folder-operation .seafile-multicolor-icon-more-level,
|
||||
.view-structure .view-item:hover .more-view-operation .seafile-multicolor-icon-more-level {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.view-structure .more-view-folder-operation:hover,
|
||||
.view-structure .more-view-operation:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-structure .more-view-folder-operation .dtable-font,
|
||||
.view-structure .more-view-operation .dtable-font {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.view-structure .folder-list .view-folder.fold-freezed .btn-folder-operation,
|
||||
.view-structure .view-item.view-freezed .seafile-multicolor-icon-more-level {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.view-structure-footer {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.view-structure-footer.return-to-app {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.add-view-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-structure-footer .dropdown {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.add-view-dropdown-menu {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.view-structure-footer .dropdown button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.view-structure-footer .dropdown button::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.view-structure-footer .dropdown button:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.view-structure-footer .add-view-btn {
|
||||
padding-left: 20px;
|
||||
border-top: none;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.view-structure-footer .add-view-btn .dtable-icon-add-table {
|
||||
margin-left: 1px;
|
||||
margin-right: 0.5rem;
|
||||
transform: unset;
|
||||
}
|
||||
|
||||
.view-sidebar .view-structure-footer .add-view-btn {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* view operation dropdown */
|
||||
.view-structure .view-operation-dropdown-toggle {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.view-structure .view-operation-dropdown .view-operation-dropdown-menu {
|
||||
margin-left: -15px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* folders dropdown */
|
||||
.view-structure .more-view-operation .btn-move-to-folder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-structure .more-view-operation .move-to-folders-toggle {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
margin-left: -12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.view-structure .more-view-operation .folders-dropdown-menu {
|
||||
margin-top: -16px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.view-structure .folders-dropdown-menu .dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-structure .more-view-operation .folders-dropdown .dtable-icon-right-slide {
|
||||
display: inline-flex;
|
||||
font-size: 12px;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.view-structure .more-view-operation .btn-move-to-folder:focus .dtable-icon-insert-right,
|
||||
.view-structure .more-view-operation .btn-move-to-folder:focus .dtable-icon-right-slide {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.view-structure .folders-dropdown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.view-structure .folders-dropdown-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-structure .folders-dropdown .item-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.view-structure .folders-dropdown .dropdown-menu {
|
||||
max-width: 180px;
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.view-structure .folders-dropdown .dropdown-menu .folder-name {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.view-structure .folders-dropdown .icon-dropdown-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.view-structure .folders-dropdown .icon-dropdown-toggle .item-icon {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.view-structure .view-item.view-can-drop::after,
|
||||
.view-structure .view-item.view-can-drop-top::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 39px;
|
||||
background-color: #666 !important;
|
||||
}
|
||||
|
||||
.view-structure .view-item.view-can-drop-top::after {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* dropdown icon */
|
||||
.dtable-dropdown-menu .dropdown-item .item-icon,
|
||||
.dtable-dropdown-menu .dropdown-item .seafile-multicolor-icon {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.dtable-dropdown-menu .dropdown-item:hover .item-icon,
|
||||
.dtable-dropdown-menu .dropdown-item:hover .seafile-multicolor-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* dark mode */
|
||||
.view-structure-dark.view-structure,
|
||||
.view-structure-dark.view-structure .view-folder .icon-expand-folder {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* light mode */
|
||||
.view-structure-light.view-structure,
|
||||
.view-structure-light.view-structure .view-item .seafile-multicolor-icon-more-level:hover,
|
||||
.view-structure-light.view-structure .view-folder .seafile-multicolor-icon-more-level:hover,
|
||||
.view-structure-light.view-structure .view-folder .icon-expand-folder:hover {
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.view-structure-light.view-structure .view-item .seafile-multicolor-icon-more-level,
|
||||
.view-structure-light.view-structure .view-folder .seafile-multicolor-icon-more-level,
|
||||
.view-structure-light.view-structure .view-folder .icon-expand-folder {
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.view-structure .view-folder-wrapper.can-drop-top::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.view-structure .view-folder-wrapper.can-drop::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background-color: #666;
|
||||
top: unset;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.svg-item {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-size: 16px;
|
||||
}
|
@@ -3,13 +3,17 @@ import moment from 'moment';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { Modal } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { slug, siteRoot, initialPath, isDir, sharedToken, hasIndex, lang } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import wikiAPI from '../../utils/wiki-api';
|
||||
import { slug, 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';
|
||||
@@ -34,10 +38,14 @@ class Wiki extends Component {
|
||||
lastModified: '',
|
||||
latestContributor: '',
|
||||
isTreeDataLoading: true,
|
||||
isConfigLoading: true,
|
||||
treeData: treeHelper.buildTree(),
|
||||
currentNode: null,
|
||||
indexNode: null,
|
||||
indexContent: '',
|
||||
currentPageId: '',
|
||||
config: {},
|
||||
repoId: '',
|
||||
};
|
||||
|
||||
window.onpopstate = this.onpopstate;
|
||||
@@ -53,6 +61,7 @@ class Wiki extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getWikiConfig();
|
||||
this.loadSidePanel(initialPath);
|
||||
this.loadWikiData(initialPath);
|
||||
|
||||
@@ -64,6 +73,40 @@ class Wiki extends Component {
|
||||
this.links.forEach(link => link.removeEventListener('click', this.onConentLinkClick));
|
||||
}
|
||||
|
||||
handlePath = () => {
|
||||
return isEditWiki ? 'edit-wiki/' : 'published/';
|
||||
};
|
||||
|
||||
getWikiConfig = () => {
|
||||
wikiAPI.getWikiConfig(slug).then(res => {
|
||||
const { wiki_config, repo_id } = res.data.wiki;
|
||||
this.setState({
|
||||
config: new WikiConfig(JSON.parse(wiki_config) || {}),
|
||||
isConfigLoading: false,
|
||||
repoId: repo_id,
|
||||
});
|
||||
}).catch((error) => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
this.setState({
|
||||
isConfigLoading: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
saveWikiConfig = (wikiConfig, onSuccess, onError) => {
|
||||
wikiAPI.updateWikiConfig(slug, JSON.stringify(wikiConfig)).then(res => {
|
||||
this.setState({
|
||||
config: new WikiConfig(wikiConfig || {}),
|
||||
});
|
||||
onSuccess && onSuccess();
|
||||
}).catch((error) => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
onError && onError();
|
||||
});
|
||||
};
|
||||
|
||||
loadSidePanel = (initialPath) => {
|
||||
if (hasIndex) {
|
||||
this.loadIndexNode();
|
||||
@@ -103,17 +146,17 @@ class Wiki extends Component {
|
||||
|
||||
if (isDir === 'None') {
|
||||
this.setState({pathExist: false});
|
||||
let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(initialPath);
|
||||
let fileUrl = siteRoot + this.handlePath() + slug + Utils.encodePath(initialPath);
|
||||
window.history.pushState({url: fileUrl, path: initialPath}, initialPath, fileUrl);
|
||||
}
|
||||
};
|
||||
|
||||
loadIndexNode = () => {
|
||||
seafileAPI.listWikiDir(slug, '/').then(res => {
|
||||
wikiAPI.listWikiDir(slug, '/').then(res => {
|
||||
let tree = this.state.treeData;
|
||||
this.addFirstResponseListToNode(res.data.dirent_list, tree.root);
|
||||
let indexNode = tree.getNodeByPath(this.indexPath);
|
||||
seafileAPI.getWikiFileContent(slug, indexNode.path).then(res => {
|
||||
wikiAPI.getWikiFileContent(slug, indexNode.path).then(res => {
|
||||
this.setState({
|
||||
treeData: tree,
|
||||
indexNode: indexNode,
|
||||
@@ -131,7 +174,7 @@ class Wiki extends Component {
|
||||
this.loadDirentList(dirPath);
|
||||
|
||||
// update location url
|
||||
let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(dirPath);
|
||||
let fileUrl = siteRoot + this.handlePath() + slug + Utils.encodePath(dirPath);
|
||||
window.history.pushState({url: fileUrl, path: dirPath}, dirPath, fileUrl);
|
||||
};
|
||||
|
||||
@@ -143,7 +186,7 @@ class Wiki extends Component {
|
||||
});
|
||||
|
||||
this.removePythonWrapper();
|
||||
seafileAPI.getWikiFileContent(slug, filePath).then(res => {
|
||||
wikiAPI.getWikiFileContent(slug, filePath).then(res => {
|
||||
let data = res.data;
|
||||
this.setState({
|
||||
isDataLoading: false,
|
||||
@@ -152,10 +195,13 @@ 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 + 'published/' + slug + Utils.encodePath(filePath) + hash;
|
||||
let fileUrl = `${siteRoot}${this.handlePath()}${slug}${Utils.encodePath(filePath)}${hash}`;
|
||||
if (filePath === '/home.md') {
|
||||
window.history.replaceState({url: fileUrl, path: filePath}, filePath, fileUrl);
|
||||
} else {
|
||||
@@ -165,7 +211,7 @@ class Wiki extends Component {
|
||||
|
||||
loadDirentList = (dirPath) => {
|
||||
this.setState({isDataLoading: true});
|
||||
seafileAPI.listWikiDir(slug, dirPath).then(res => {
|
||||
wikiAPI.listWikiDir(slug, dirPath).then(res => {
|
||||
let direntList = res.data.dirent_list.map(item => {
|
||||
let dirent = new Dirent(item);
|
||||
return dirent;
|
||||
@@ -195,7 +241,7 @@ class Wiki extends Component {
|
||||
let tree = this.state.treeData.clone();
|
||||
let node = tree.getNodeByPath(path);
|
||||
if (!node.isLoaded) {
|
||||
seafileAPI.listWikiDir(slug, node.path).then(res => {
|
||||
wikiAPI.listWikiDir(slug, node.path).then(res => {
|
||||
this.addResponseListToNode(res.data.dirent_list, node);
|
||||
let parentNode = tree.getNodeByPath(node.parentNode.path);
|
||||
parentNode.isExpanded = true;
|
||||
@@ -216,7 +262,7 @@ class Wiki extends Component {
|
||||
if (Utils.isMarkdownFile(path)) {
|
||||
path = Utils.getDirName(path);
|
||||
}
|
||||
seafileAPI.listWikiDir(slug, path, true).then(res => {
|
||||
wikiAPI.listWikiDir(slug, path, true).then(res => {
|
||||
let direntList = res.data.dirent_list;
|
||||
let results = {};
|
||||
for (let i = 0; i < direntList.length; i++) {
|
||||
@@ -379,7 +425,7 @@ class Wiki extends Component {
|
||||
if (!node.isLoaded) {
|
||||
let tree = this.state.treeData.clone();
|
||||
node = tree.getNodeByPath(node.path);
|
||||
seafileAPI.listWikiDir(slug, node.path).then(res => {
|
||||
wikiAPI.listWikiDir(slug, node.path).then(res => {
|
||||
this.addResponseListToNode(res.data.dirent_list, node);
|
||||
tree.collapseNode(node);
|
||||
this.setState({treeData: tree});
|
||||
@@ -427,7 +473,7 @@ class Wiki extends Component {
|
||||
let tree = this.state.treeData.clone();
|
||||
node = tree.getNodeByPath(node.path);
|
||||
if (!node.isLoaded) {
|
||||
seafileAPI.listWikiDir(slug, node.path).then(res => {
|
||||
wikiAPI.listWikiDir(slug, node.path).then(res => {
|
||||
this.addResponseListToNode(res.data.dirent_list, node);
|
||||
this.setState({treeData: tree});
|
||||
});
|
||||
@@ -472,11 +518,45 @@ class Wiki extends Component {
|
||||
node.addChildren(nodeList);
|
||||
};
|
||||
|
||||
setCurrentPage = (pageId, callback) => {
|
||||
const { currentPageId, config } = this.state;
|
||||
if (pageId === currentPageId) {
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
const { pages } = config;
|
||||
const currentPage = PageUtils.getPageById(pages, pageId);
|
||||
const path = currentPage.path;
|
||||
if (Utils.isMarkdownFile(path) || Utils.isSdocFile(path)) {
|
||||
if (path !== this.state.path) {
|
||||
this.showFile(path);
|
||||
}
|
||||
this.onCloseSide();
|
||||
} else {
|
||||
const w = window.open('about:blank');
|
||||
const url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(path);
|
||||
w.location.href = url;
|
||||
}
|
||||
this.setState({
|
||||
currentPageId: pageId,
|
||||
path: path,
|
||||
}, () => {
|
||||
callback && callback();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="main" className="wiki-main">
|
||||
{isEditWiki &&
|
||||
<WikiLeftBar
|
||||
config={this.state.config}
|
||||
repoId={this.state.repoId}
|
||||
updateConfig={(data) => this.saveWikiConfig(Object.assign({}, this.state.config, data))}
|
||||
/>
|
||||
}
|
||||
<SidePanel
|
||||
isTreeDataLoading={this.state.isTreeDataLoading}
|
||||
isLoading={this.state.isTreeDataLoading || this.state.isConfigLoading}
|
||||
closeSideBar={this.state.closeSideBar}
|
||||
currentPath={this.state.path}
|
||||
treeData={this.state.treeData}
|
||||
@@ -487,6 +567,10 @@ class Wiki extends Component {
|
||||
onNodeCollapse={this.onNodeCollapse}
|
||||
onNodeExpanded={this.onNodeExpanded}
|
||||
onLinkClick={this.onLinkClick}
|
||||
config={this.state.config}
|
||||
saveWikiConfig={this.saveWikiConfig}
|
||||
setCurrentPage={this.setCurrentPage}
|
||||
currentPageId={this.state.currentPageId}
|
||||
/>
|
||||
<MainPanel
|
||||
path={this.state.path}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext, repoID, siteRoot, username, isPro } from '../../utils/constants';
|
||||
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';
|
||||
@@ -82,7 +82,7 @@ class MainPanel extends Component {
|
||||
const errMessage = (<div className="message err-tip">{gettext('Folder does not exist.')}</div>);
|
||||
const isViewingFile = this.props.pathExist && !this.props.isDataLoading && this.props.isViewFile;
|
||||
return (
|
||||
<div className="main-panel wiki-main-panel">
|
||||
<div className="main-panel wiki-main-panel" style={{flex: isEditWiki ? '1 0 76%' : '1 0 80%'}}>
|
||||
<div className="main-panel-hide hide">{this.props.content}</div>
|
||||
<div className={`main-panel-north panel-top ${this.props.permission === 'rw' ? 'border-left-show' : ''}`}>
|
||||
{!username &&
|
||||
|
9
frontend/src/pages/wiki/models/folder.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default class Folder {
|
||||
constructor(object) {
|
||||
this.type = 'folder';
|
||||
this.id = object.id;
|
||||
this.name = object.name;
|
||||
this.icon = object.icon;
|
||||
this.children = object.children || [];
|
||||
}
|
||||
}
|
8
frontend/src/pages/wiki/models/page.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default class Page {
|
||||
constructor(object) {
|
||||
this.id = object.id;
|
||||
this.name = object.name;
|
||||
this.path = object.path;
|
||||
this.icon = object.icon;
|
||||
}
|
||||
}
|
9
frontend/src/pages/wiki/models/wiki-config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default class WikiConfig {
|
||||
constructor(object) {
|
||||
this.version = object.version || 1;
|
||||
this.wiki_name = object.wiki_name || '';
|
||||
this.wiki_icon = object.wiki_icon || '';
|
||||
this.navigation = object.navigation || [];
|
||||
this.pages = object.pages || [];
|
||||
}
|
||||
}
|
@@ -1,14 +1,26 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext, siteRoot, repoID, slug, username, permission } from '../../utils/constants';
|
||||
import Logo from '../../components/logo';
|
||||
import deepCopy from 'deep-copy';
|
||||
import { gettext, siteRoot, repoID, slug, username, permission, mediaUrl, isEditWiki } from '../../utils/constants';
|
||||
import toaster from '../../components/toast';
|
||||
import Loading from '../../components/loading';
|
||||
import TreeView from '../../components/tree-view/tree-view';
|
||||
// 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 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,
|
||||
isTreeDataLoading: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
treeData: PropTypes.object.isRequired,
|
||||
indexNode: PropTypes.object,
|
||||
indexContent: PropTypes.string.isRequired,
|
||||
@@ -18,6 +30,10 @@ 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 {
|
||||
@@ -25,6 +41,10 @@ class SidePanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.isNodeMenuShow = false;
|
||||
this.state = {
|
||||
isShowNewFolderDialog: false,
|
||||
isShowAddPageDialog: false,
|
||||
};
|
||||
}
|
||||
|
||||
renderIndexView = () => {
|
||||
@@ -42,7 +62,7 @@ class SidePanel extends Component {
|
||||
renderTreeView = () => {
|
||||
return (
|
||||
<div className="wiki-pages-container">
|
||||
{this.props.treeData && (
|
||||
{/* {this.props.treeData && (
|
||||
<TreeView
|
||||
treeData={this.props.treeData}
|
||||
currentPath={this.props.currentPath}
|
||||
@@ -51,22 +71,336 @@ class SidePanel extends Component {
|
||||
onNodeCollapse={this.props.onNodeCollapse}
|
||||
onNodeExpanded={this.props.onNodeExpanded}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
{isEditWiki &&
|
||||
<ViewStructureFooter
|
||||
onToggleAddView={this.openAddPageDialog.bind(this, null)}
|
||||
onToggleAddFolder={this.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowNewFolderDialog &&
|
||||
<NewFolderDialog
|
||||
onAddFolder={this.addPageFolder}
|
||||
onToggleAddFolderDialog={this.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowAddPageDialog &&
|
||||
<AddPageDialog
|
||||
toggle={this.closeAddPageDialog}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
confirmDeletePage = (pageId) => {
|
||||
const config = deepCopy(this.props.config);
|
||||
const { pages, navigation } = config;
|
||||
const index = PageUtils.getPageIndexById(pageId, pages);
|
||||
config.pages.splice(index, 1);
|
||||
PageUtils.deletePage(navigation, pageId);
|
||||
this.props.saveWikiConfig(config);
|
||||
if (config.pages.length > 0) {
|
||||
this.props.setCurrentPage(config.pages[0].id);
|
||||
} else {
|
||||
this.props.setCurrentPage('');
|
||||
}
|
||||
};
|
||||
|
||||
onAddNewPage = async ({name, icon, path, successCallback, errorCallback}) => {
|
||||
const { config } = this.props;
|
||||
const navigation = config.navigation;
|
||||
const pageId = generateUniqueId(navigation);
|
||||
const newPage = new Page({ id: pageId, name, icon, path});
|
||||
this.addPage(newPage, successCallback, errorCallback);
|
||||
};
|
||||
|
||||
duplicatePage = async (fromPageConfig, successCallback, errorCallback) => {
|
||||
const { config } = this.props;
|
||||
const { name, from_page_id } = fromPageConfig;
|
||||
const { navigation, pages } = config;
|
||||
const fromPage = PageUtils.getPageById(pages, from_page_id);
|
||||
const newPageId = generateUniqueId(navigation);
|
||||
let newPageConfig = {
|
||||
...fromPage,
|
||||
id: newPageId,
|
||||
name,
|
||||
};
|
||||
const newPage = new Page({ ...newPageConfig });
|
||||
this.addPage(newPage, successCallback, errorCallback);
|
||||
};
|
||||
|
||||
addPage = (page, successCallback, errorCallback) => {
|
||||
const { config } = this.props;
|
||||
const navigation = config.navigation;
|
||||
const pageId = page.id;
|
||||
config.pages.push(page);
|
||||
PageUtils.addPage(navigation, pageId, this.current_folder_id);
|
||||
config.navigation = navigation;
|
||||
const onSuccess = () => {
|
||||
this.props.setCurrentPage(pageId, successCallback);
|
||||
successCallback();
|
||||
};
|
||||
this.props.saveWikiConfig(config, onSuccess, errorCallback);
|
||||
};
|
||||
|
||||
onUpdatePage = (pageId, newPage) => {
|
||||
if (newPage.name === '') {
|
||||
toaster.danger(gettext('Page name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
const { config } = this.props;
|
||||
let pages = config.pages;
|
||||
let currentPage = pages.find(page => page.id === pageId);
|
||||
Object.assign(currentPage, newPage);
|
||||
config.pages = pages;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
movePage = ({ moved_view_id, target_view_id, source_view_folder_id, target_view_folder_id, move_position }) => {
|
||||
let config = deepCopy(this.props.config);
|
||||
let { navigation } = config;
|
||||
PageUtils.movePage(navigation, moved_view_id, target_view_id, source_view_folder_id, target_view_folder_id, move_position);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
movePageOut = (moved_page_id, source_folder_id, target_folder_id, move_position) => {
|
||||
let config = deepCopy(this.props.config);
|
||||
let { navigation } = config;
|
||||
PageUtils.movePageOut(navigation, moved_page_id, source_folder_id, target_folder_id, move_position);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
// Create a new folder in the root directory (not supported to create a new subfolder in the folder)
|
||||
addPageFolder = (folder_data, parent_folder_id) => {
|
||||
const { config } = this.props;
|
||||
const { navigation } = config;
|
||||
let folder_id = generateUniqueId(navigation);
|
||||
let newFolder = new Folder({ id: folder_id, ...folder_data });
|
||||
// No parent folder, directly add to the root directory
|
||||
if (!parent_folder_id) {
|
||||
config.navigation.push(newFolder);
|
||||
} else { // Recursively find the parent folder and add
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._addFolder(item, newFolder, parent_folder_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
_addFolder(folder, newFolder, parent_folder_id) {
|
||||
if (folder.id === parent_folder_id) {
|
||||
folder.children.push(newFolder);
|
||||
return;
|
||||
}
|
||||
folder.children.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._addFolder(item, newFolder, parent_folder_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onModifyFolder = (folder_id, folder_data) => {
|
||||
const { config } = this.props;
|
||||
const { navigation } = config;
|
||||
PageUtils.modifyFolder(navigation, folder_id, folder_data);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
onDeleteFolder = (page_folder_id) => {
|
||||
const { config } = this.props;
|
||||
const { navigation, pages } = config;
|
||||
PageUtils.deleteFolder(navigation, pages, page_folder_id);
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
// Drag a folder to the front and back of another folder
|
||||
onMoveFolder = (moved_folder_id, target_folder_id, move_position) => {
|
||||
const { config } = this.props;
|
||||
const { navigation } = config;
|
||||
let updatedNavigation = deepCopy(navigation);
|
||||
|
||||
// Get the moved folder first and delete the original location
|
||||
let moved_folder;
|
||||
let moved_folder_index = PageUtils.getFolderIndexById(updatedNavigation, moved_folder_id);
|
||||
if (moved_folder_index === -1) {
|
||||
updatedNavigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
moved_folder_index = PageUtils.getFolderIndexById(item.children, moved_folder_id);
|
||||
if (moved_folder_index > -1) {
|
||||
moved_folder = item.children[moved_folder_index];
|
||||
item.children.splice(moved_folder_index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
moved_folder = updatedNavigation[moved_folder_index];
|
||||
updatedNavigation.splice(moved_folder_index, 1);
|
||||
}
|
||||
let indexOffset = 0;
|
||||
if (move_position === 'move_below') {
|
||||
indexOffset++;
|
||||
}
|
||||
// Get the location of the release
|
||||
let target_folder_index = PageUtils.getFolderIndexById(updatedNavigation, target_folder_id);
|
||||
if (target_folder_index === -1) {
|
||||
updatedNavigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
target_folder_index = PageUtils.getFolderIndexById(item.children, target_folder_id);
|
||||
if (target_folder_index > -1) {
|
||||
item.children.splice(target_folder_index + indexOffset, 0, moved_folder);
|
||||
}
|
||||
} else {
|
||||
// not changed
|
||||
updatedNavigation = navigation;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updatedNavigation.splice(target_folder_index + indexOffset, 0, moved_folder);
|
||||
}
|
||||
config.navigation = updatedNavigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
// Not support yet: Move a folder into another folder
|
||||
moveFolderToFolder = (moved_folder_id, target_folder_id) => {
|
||||
let { config } = this.props;
|
||||
let { navigation } = config;
|
||||
|
||||
// Find the folder and move it to this new folder
|
||||
let target_folder = PageUtils.getFolderById(navigation, target_folder_id);
|
||||
if (!target_folder) {
|
||||
toaster.danger('Only_support_two_level_folders');
|
||||
return;
|
||||
}
|
||||
|
||||
let moved_folder;
|
||||
let moved_folder_index = PageUtils.getFolderIndexById(navigation, moved_folder_id);
|
||||
|
||||
// The original directory is in the root directory
|
||||
if (moved_folder_index > -1) {
|
||||
moved_folder = PageUtils.getFolderById(navigation, moved_folder_id);
|
||||
// If moved folder There are other directories under the ID, and dragging is not supported
|
||||
if (moved_folder.children.some(item => item.type === FOLDER)) {
|
||||
toaster.danger('Only_support_two_level_folders');
|
||||
return;
|
||||
}
|
||||
target_folder.children.push(moved_folder);
|
||||
navigation.splice(moved_folder_index, 1);
|
||||
} else { // The original directory is not in the root directory
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
let moved_folder_index = PageUtils.getFolderIndexById(item.children, moved_folder_id);
|
||||
if (moved_folder_index > -1) {
|
||||
moved_folder = item.children[moved_folder_index];
|
||||
target_folder.children.push(moved_folder);
|
||||
item.children.splice(moved_folder_index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
config.navigation = navigation;
|
||||
this.props.saveWikiConfig(config);
|
||||
};
|
||||
|
||||
onToggleAddFolder = () => {
|
||||
this.setState({ isShowNewFolderDialog: !this.state.isShowNewFolderDialog });
|
||||
};
|
||||
|
||||
openAddPageDialog = (folder_id) => {
|
||||
this.current_folder_id = folder_id;
|
||||
this.setState({ isShowAddPageDialog: true });
|
||||
};
|
||||
|
||||
closeAddPageDialog = () => {
|
||||
this.current_folder_id = null;
|
||||
this.setState({ isShowAddPageDialog: false });
|
||||
};
|
||||
|
||||
onSetFolderId = (folder_id) => {
|
||||
this.current_folder_id = folder_id;
|
||||
};
|
||||
|
||||
renderFolderView = () => {
|
||||
const { config } = this.props;
|
||||
const { pages, navigation } = config;
|
||||
return (
|
||||
<div className="wiki-pages-container">
|
||||
<ViewStructure
|
||||
isEditMode={isEditWiki}
|
||||
navigation={navigation}
|
||||
views={pages}
|
||||
onToggleAddView={this.openAddPageDialog}
|
||||
onDeleteView={this.confirmDeletePage}
|
||||
onUpdatePage={this.onUpdatePage}
|
||||
onSelectView={this.props.setCurrentPage}
|
||||
onMoveView={this.movePage}
|
||||
movePageOut={this.movePageOut}
|
||||
onToggleAddFolder={this.onToggleAddFolder}
|
||||
onModifyFolder={this.onModifyFolder}
|
||||
onDeleteFolder={this.onDeleteFolder}
|
||||
onMoveFolder={this.onMoveFolder}
|
||||
moveFolderToFolder={this.moveFolderToFolder}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
duplicatePage={this.duplicatePage}
|
||||
onSetFolderId={this.onSetFolderId}
|
||||
currentPageId={this.props.currentPageId}
|
||||
/>
|
||||
{this.state.isShowNewFolderDialog &&
|
||||
<NewFolderDialog
|
||||
onAddFolder={this.addPageFolder}
|
||||
onToggleAddFolderDialog={this.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
{this.state.isShowAddPageDialog &&
|
||||
<AddPageDialog
|
||||
toggle={this.closeAddPageDialog}
|
||||
onAddNewPage={this.onAddNewPage}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderContent = () => {
|
||||
const { isLoading, indexNode, config } = this.props;
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (indexNode) {
|
||||
return this.renderIndexView();
|
||||
}
|
||||
if (isObjectNotEmpty(config)) {
|
||||
return this.renderFolderView();
|
||||
}
|
||||
return this.renderTreeView();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { wiki_name, wiki_icon } = this.props.config;
|
||||
const src = wiki_icon ? getIconURL(repoID, wiki_icon) : `${mediaUrl}img/wiki/default.png`;
|
||||
return (
|
||||
<div className={`side-panel wiki-side-panel ${this.props.closeSideBar ? '': 'left-zero'}`}>
|
||||
<div className="side-panel-top panel-top">
|
||||
<Logo onCloseSidePanel={this.props.onCloseSide} />
|
||||
{wiki_icon && <img src={src} width="32" height="32" alt='' className='mr-2' />}
|
||||
<h4 className="ml-0 mb-0">{wiki_name || slug}</h4>
|
||||
</div>
|
||||
<div id="side-nav" className="wiki-side-nav" role="navigation">
|
||||
{this.props.isTreeDataLoading && <Loading /> }
|
||||
{!this.props.isTreeDataLoading && this.props.indexNode && this.renderIndexView() }
|
||||
{!this.props.isTreeDataLoading && !this.props.indexNode && this.renderTreeView() }
|
||||
{(username && permission) && <div className="text-left p-2"><a href={siteRoot + 'library/' + repoID + '/' + slug + '/'} className="text-dark text-decoration-underline">{gettext('Go to Library')}</a></div>}
|
||||
{this.renderContent() }
|
||||
{(username && permission) && (
|
||||
<div className="text-left p-2">
|
||||
<a href={siteRoot + 'library/' + repoID + '/' + slug + '/'} className="text-dark text-decoration-underline">
|
||||
{gettext('Go to Library')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
40
frontend/src/pages/wiki/utils/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import {serviceURL} from '../../../utils/constants';
|
||||
|
||||
const generatorBase64Code = (keyLength = 4) => {
|
||||
let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let key = '';
|
||||
for (let i = 0; i < keyLength; i++) {
|
||||
key += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
const generateUniqueId = (navigation, length = 4) => {
|
||||
let idMap = {};
|
||||
function recurseItem(item) {
|
||||
if (!item) return;
|
||||
idMap[item.id] = true;
|
||||
if (Array.isArray(item.children)) {
|
||||
item.children.forEach(item => {
|
||||
recurseItem(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
navigation.forEach(item => recurseItem(item));
|
||||
|
||||
let _id = generatorBase64Code(length);
|
||||
while (idMap[_id]) {
|
||||
_id = generatorBase64Code(length);
|
||||
}
|
||||
return _id;
|
||||
};
|
||||
|
||||
const isObjectNotEmpty = (obj) => {
|
||||
return obj && Object.keys(obj).length > 0;
|
||||
};
|
||||
|
||||
const getIconURL = (repoId, fileName) => {
|
||||
return serviceURL + '/lib/' + repoId + '/file/_Internal/Wiki/Icon/' + fileName + '?raw=1';
|
||||
};
|
||||
|
||||
export { generatorBase64Code, generateUniqueId, isObjectNotEmpty, getIconURL };
|
233
frontend/src/pages/wiki/view-structure/add-page-dialog.js
Normal file
@@ -0,0 +1,233 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input, Button } from 'reactstrap';
|
||||
import { gettext, repoID } from '../../../utils/constants';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import toaster from '../../../components/toast';
|
||||
import Loading from '../../../components/loading';
|
||||
import { SeahubSelect } from '../../../components/common/select';
|
||||
import FileChooser from '../../../components/file-chooser/file-chooser';
|
||||
|
||||
import '../css/add-page-dialog.css';
|
||||
|
||||
const propTypes = {
|
||||
toggle: PropTypes.func.isRequired,
|
||||
onAddNewPage: PropTypes.func,
|
||||
};
|
||||
|
||||
const DIALOG_MAX_HEIGHT = window.innerHeight - 56; // Dialog margin is 3.5rem (56px)
|
||||
|
||||
class AddPageDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.options = this.getOptions();
|
||||
this.state = {
|
||||
pageName: '',
|
||||
iconClassName: '',
|
||||
isLoading: false,
|
||||
repo: null,
|
||||
selectedPath: '',
|
||||
errMessage: '',
|
||||
newFileName: '',
|
||||
selectedOption: this.options[0],
|
||||
};
|
||||
}
|
||||
|
||||
getOptions = () => {
|
||||
return (
|
||||
[
|
||||
{
|
||||
value: 'existing',
|
||||
label: gettext('Select an existing file'),
|
||||
},
|
||||
{
|
||||
value: '.md',
|
||||
label: gettext('Create a markdown file'),
|
||||
},
|
||||
{
|
||||
value: '.sdoc',
|
||||
label: gettext('Create a sdoc file'),
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
handleChange = (event) => {
|
||||
let value = event.target.value;
|
||||
if (value === this.state.pageName) {
|
||||
return;
|
||||
}
|
||||
this.setState({ pageName: value });
|
||||
};
|
||||
|
||||
onFileNameChange = (event) => {
|
||||
this.setState({ newFileName: event.target.value });
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.props.toggle();
|
||||
};
|
||||
|
||||
onIconChange = (className) => {
|
||||
this.setState({ iconClassName: className });
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
let {
|
||||
iconClassName,
|
||||
selectedPath,
|
||||
selectedOption,
|
||||
} = this.state;
|
||||
const pageName = this.state.pageName.trim();
|
||||
if (pageName === '') {
|
||||
toaster.danger(gettext('Page name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
if (selectedOption.value === 'existing') {
|
||||
if (selectedPath.endsWith('.sdoc') === false && selectedPath.endsWith('.md') === false) {
|
||||
toaster.danger(gettext('Please select an existing sdoc or markdown file'));
|
||||
return;
|
||||
}
|
||||
this.props.onAddNewPage({
|
||||
name: pageName,
|
||||
icon: iconClassName,
|
||||
path: selectedPath,
|
||||
successCallback: this.onSuccess,
|
||||
errorCallback: this.onError,
|
||||
});
|
||||
this.setState({ isLoading: true });
|
||||
}
|
||||
else {
|
||||
const newFileName = this.state.newFileName.trim();
|
||||
if (newFileName === '') {
|
||||
toaster.danger(gettext('New file name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
if (newFileName.includes('/')) {
|
||||
toaster.danger(gettext('Name cannot contain slash'));
|
||||
return;
|
||||
}
|
||||
if (newFileName.includes('\\')) {
|
||||
toaster.danger(gettext('Name cannot contain backslash'));
|
||||
return;
|
||||
}
|
||||
this.setState({ isLoading: true });
|
||||
seafileAPI.createFile(repoID, `${selectedPath}/${newFileName}${selectedOption.value}`).then(res => {
|
||||
const { obj_name, parent_dir } = res.data;
|
||||
this.props.onAddNewPage({
|
||||
name: pageName,
|
||||
icon: iconClassName,
|
||||
path: parent_dir === '/' ? `/${obj_name}` : `${parent_dir}/${obj_name}`,
|
||||
successCallback: this.onSuccess,
|
||||
errorCallback: this.onError,
|
||||
});
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
this.onError();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSuccess = () => {
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
this.setState({ isLoading: false });
|
||||
};
|
||||
|
||||
onDirentItemClick = (repo, selectedPath) => {
|
||||
this.setState({
|
||||
repo: repo,
|
||||
selectedPath: selectedPath,
|
||||
errMessage: ''
|
||||
});
|
||||
};
|
||||
|
||||
onRepoItemClick = (repo) => {
|
||||
this.setState({
|
||||
repo: repo,
|
||||
selectedPath: '/',
|
||||
errMessage: ''
|
||||
});
|
||||
};
|
||||
|
||||
handleSelectChange = (selectedOption) => {
|
||||
this.setState({
|
||||
selectedOption,
|
||||
selectedPath: '',
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle} autoFocus={false} className="add-page-dialog" style={{ maxHeight: DIALOG_MAX_HEIGHT }}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('Add page')}</ModalHeader>
|
||||
<ModalBody className={'pr-4'}>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Page name')}</Label>
|
||||
<Input value={this.state.pageName} onChange={this.handleChange} autoFocus={true} />
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>{gettext('The file corresponding to this page')}</Label>
|
||||
<SeahubSelect
|
||||
value={this.state.selectedOption}
|
||||
options={this.options}
|
||||
onChange={this.handleSelectChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
{this.state.selectedOption.value !== 'existing' &&
|
||||
<>
|
||||
<FormGroup>
|
||||
<Label>{gettext('New file name')}</Label>
|
||||
<Input value={this.state.newFileName} onChange={this.onFileNameChange} autoFocus={true} />
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Select a directory to save new file')}</Label>
|
||||
<FileChooser
|
||||
isShowFile={false}
|
||||
repoID={repoID}
|
||||
currentPath={this.state.selectedPath}
|
||||
onDirentItemClick={this.onDirentItemClick}
|
||||
onRepoItemClick={this.onRepoItemClick}
|
||||
mode={'only_current_library'}
|
||||
hideLibraryName={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
}
|
||||
{this.state.selectedOption.value === 'existing' &&
|
||||
<FormGroup>
|
||||
<Label>{gettext('Select an existing file')}</Label>
|
||||
<FileChooser
|
||||
isShowFile={true}
|
||||
repoID={repoID}
|
||||
currentPath={this.state.selectedPath}
|
||||
onDirentItemClick={this.onDirentItemClick}
|
||||
onRepoItemClick={this.onRepoItemClick}
|
||||
mode={'only_current_library'}
|
||||
hideLibraryName={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||
{this.state.isLoading ?
|
||||
<Button color="primary" disabled><Loading/></Button> :
|
||||
<Button color="primary" onClick={this.onSubmit}>{gettext('Submit')}</Button>
|
||||
}
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddPageDialog.propTypes = propTypes;
|
||||
|
||||
export default AddPageDialog;
|
@@ -0,0 +1,53 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, DropdownMenu, DropdownItem, DropdownToggle } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
class AddViewDropdownMenu extends Component {
|
||||
|
||||
toggle = event => {
|
||||
this.onStopPropagation(event);
|
||||
this.props.onToggleAddViewDropdown();
|
||||
};
|
||||
|
||||
onToggleAddView = event => {
|
||||
this.onStopPropagation(event);
|
||||
this.props.onToggleAddView();
|
||||
};
|
||||
|
||||
onToggleAddFolder = event => {
|
||||
this.onStopPropagation(event);
|
||||
this.props.onToggleAddFolder();
|
||||
};
|
||||
|
||||
onStopPropagation = event => {
|
||||
event && event.nativeEvent && event.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Dropdown isOpen toggle={this.toggle}>
|
||||
<DropdownToggle caret></DropdownToggle>
|
||||
<DropdownMenu container="body" className='dtable-dropdown-menu large add-view-dropdown-menu' style={{ zIndex: 1061 }}>
|
||||
<DropdownItem onClick={this.onToggleAddView}>
|
||||
<Icon symbol={'main-view'}/>
|
||||
<span className='item-text'>{gettext('Add page')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this.onToggleAddFolder}>
|
||||
<Icon symbol={'folders'}/>
|
||||
<span className='item-text'>{gettext('Add folder')}</span>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddViewDropdownMenu.propTypes = {
|
||||
onToggleAddViewDropdown: PropTypes.func,
|
||||
onToggleAddView: PropTypes.func,
|
||||
onToggleAddFolder: PropTypes.func,
|
||||
};
|
||||
|
||||
export default AddViewDropdownMenu;
|
2
frontend/src/pages/wiki/view-structure/constant.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export const DRAGGED_FOLDER_MODE = 'view-folder';
|
||||
export const DRAGGED_VIEW_MODE = 'view';
|
@@ -0,0 +1,103 @@
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
import { DRAGGED_FOLDER_MODE, DRAGGED_VIEW_MODE } from '../constant';
|
||||
import FolderItem from './folder-item';
|
||||
|
||||
const dragSource = {
|
||||
beginDrag(props, monitor) {
|
||||
const { folderIndex, folder } = props;
|
||||
return {
|
||||
idx: folderIndex,
|
||||
data: folder,
|
||||
mode: DRAGGED_FOLDER_MODE,
|
||||
};
|
||||
},
|
||||
endDrag(props, monitor) {
|
||||
const sourceRow = monitor.getItem();
|
||||
const didDrop = monitor.didDrop();
|
||||
let targetRow = {};
|
||||
if (!didDrop) {
|
||||
return { sourceRow, targetRow };
|
||||
}
|
||||
},
|
||||
isDragging(props, monitor) {
|
||||
const { folderIndex: currentIndex, draggedRow } = props;
|
||||
const { idx } = draggedRow;
|
||||
return idx > currentIndex;
|
||||
},
|
||||
};
|
||||
|
||||
const dropTarget = {
|
||||
drop(props, monitor) {
|
||||
const sourceRow = monitor.getItem();
|
||||
const { folder: targetFolder } = props;
|
||||
const targetFolderId = targetFolder.id;
|
||||
const className = props.getClassName();
|
||||
if (!className) return;
|
||||
let move_position;
|
||||
if (className.includes('can-drop')) {
|
||||
move_position = 'move_below';
|
||||
}
|
||||
if (className.includes('can-drop-top')) {
|
||||
move_position = 'move_above';
|
||||
}
|
||||
let moveInto = className.includes('dragged-view-over');
|
||||
|
||||
// 1. drag source is page
|
||||
if (sourceRow.mode === DRAGGED_VIEW_MODE) {
|
||||
const sourceFolderId = sourceRow.folderId;
|
||||
const draggedViewId = sourceRow.data.id;
|
||||
// 1.1 move page into folder
|
||||
if (moveInto) {
|
||||
props.onMoveView({
|
||||
moved_view_id: draggedViewId,
|
||||
target_view_id: null,
|
||||
source_view_folder_id: sourceFolderId,
|
||||
target_view_folder_id: targetFolderId,
|
||||
move_position,
|
||||
});
|
||||
return;
|
||||
} else { // 1.2 Drag the page above or below the folder
|
||||
props.movePageOut(draggedViewId, sourceFolderId, targetFolderId, move_position);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 2. drag source is folder
|
||||
if (sourceRow.mode === DRAGGED_FOLDER_MODE) {
|
||||
const draggedFolderId = sourceRow.data.id;
|
||||
// 2.0 If dragged folder and target folder are the same folder, return
|
||||
if (targetFolderId === draggedFolderId) {
|
||||
return;
|
||||
}
|
||||
// 2.1 Do not support drag folder into another folder
|
||||
if (moveInto) {
|
||||
// props.moveFolderToFolder(draggedFolderId, targetFolderId);
|
||||
return;
|
||||
} else {
|
||||
// 2.2 Drag folder above or below another folder
|
||||
props.onMoveFolder(draggedFolderId, targetFolderId, move_position);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 3. Drag other dom
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const dragCollect = (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
connectDragPreview: connect.dragPreview(),
|
||||
isDragging: monitor.isDragging(),
|
||||
});
|
||||
|
||||
const dropCollect = (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
draggedRow: monitor.getItem(),
|
||||
connect,
|
||||
monitor,
|
||||
});
|
||||
|
||||
export default DropTarget('ViewStructure', dropTarget, dropCollect)(
|
||||
DragSource('ViewStructure', dragSource, dragCollect)(FolderItem)
|
||||
);
|
298
frontend/src/pages/wiki/view-structure/folders/folder-item.js
Normal file
@@ -0,0 +1,298 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import FolderOperationDropdownMenu from './folder-operation-dropdownmenu';
|
||||
import ViewItem from '../views/view-item';
|
||||
import DraggedFolderItem from './dragged-folder-item';
|
||||
import ViewEditPopover from '../../view-structure/views/view-edit-popover';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
const FOLDER = 'folder';
|
||||
|
||||
class FolderItem extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { name, icon } = props.folder;
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
icon: icon || '',
|
||||
name: name || '',
|
||||
};
|
||||
}
|
||||
|
||||
onToggleExpandFolder = (e) => {
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
this.props.onToggleExpandFolder(this.props.folder.id);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onToggleExpandSubfolder = (subfolderId) => {
|
||||
this.props.onToggleExpandFolder(subfolderId);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onClickFolderChildren = (e) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
openFolderEditor = () => {
|
||||
this.setState({ isEditing: true });
|
||||
};
|
||||
|
||||
closeFolderEditor = () => {
|
||||
if (this.state.isEditing) {
|
||||
const { name, icon } = this.state;
|
||||
this.props.onModifyFolder(this.props.folder.id, { name, icon });
|
||||
this.setState({ isEditing: false });
|
||||
}
|
||||
};
|
||||
|
||||
onChangeName = (name) => {
|
||||
this.setState({ name });
|
||||
};
|
||||
|
||||
onChangeIcon = (icon) => {
|
||||
this.setState({ icon });
|
||||
};
|
||||
|
||||
changeItemFreeze = (isFreeze) => {
|
||||
this.isFreeze = true;
|
||||
// if (isFreeze) {
|
||||
// this.foldRef.classList.add('fold-freezed');
|
||||
// } else {
|
||||
// this.foldRef.classList.remove('fold-freezed');
|
||||
// }
|
||||
};
|
||||
|
||||
renderFolder = (folder, index, tableGridsLength, isOnlyOneView, id_view_map) => {
|
||||
const { isEditMode, views, foldersStr } = this.props;
|
||||
const { id: folderId } = folder;
|
||||
return (
|
||||
<DraggedFolderItem
|
||||
key={`view-folder-${folderId}`}
|
||||
isEditMode={isEditMode}
|
||||
folder={folder}
|
||||
folderIndex={index}
|
||||
tableGridsLength={tableGridsLength}
|
||||
isOnlyOneView={isOnlyOneView}
|
||||
id_view_map={id_view_map}
|
||||
renderFolderMenuItems={this.props.renderFolderMenuItems}
|
||||
onToggleExpandFolder={this.onToggleExpandSubfolder}
|
||||
onToggleAddView={this.props.onToggleAddView}
|
||||
onModifyFolder={this.props.onModifyFolder}
|
||||
onDeleteFolder={this.props.onDeleteFolder}
|
||||
onMoveFolder={this.props.onMoveFolder}
|
||||
onSelectView={this.props.onSelectView}
|
||||
onUpdatePage={this.props.onUpdatePage}
|
||||
duplicatePage={this.props.duplicatePage}
|
||||
onSetFolderId={this.props.onSetFolderId}
|
||||
onDeleteView={this.props.onDeleteView}
|
||||
onMoveViewToFolder={this.props.onMoveViewToFolder}
|
||||
onMoveView={this.props.onMoveView}
|
||||
moveFolderToFolder={this.props.moveFolderToFolder}
|
||||
views={views}
|
||||
foldersStr={foldersStr + '-' + folderId}
|
||||
setClassName={this.props.setClassName}
|
||||
getClassName={this.props.getClassName}
|
||||
movePageOut={this.props.movePageOut}
|
||||
layerDragProps={this.props.layerDragProps}
|
||||
getFolderState={this.props.getFolderState}
|
||||
currentPageId={this.props.currentPageId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderView = (view, index, tableGridsLength, isOnlyOneView) => {
|
||||
const { isEditMode, views, folder, foldersStr } = this.props;
|
||||
const id = view.id;
|
||||
if (!views.find(item => item.id === id)) return;
|
||||
return (
|
||||
<ViewItem
|
||||
key={id}
|
||||
tableGridsLength={tableGridsLength}
|
||||
isOnlyOneView={isOnlyOneView}
|
||||
infolder={false}
|
||||
view={views.find(item => item.id === id)}
|
||||
viewIndex={index}
|
||||
folderId={folder.id}
|
||||
isEditMode={isEditMode}
|
||||
renderFolderMenuItems={this.props.renderFolderMenuItems}
|
||||
duplicatePage={this.props.duplicatePage}
|
||||
onSetFolderId={this.props.onSetFolderId}
|
||||
onSelectView={() => this.props.onSelectView(id)}
|
||||
onUpdatePage={this.props.onUpdatePage}
|
||||
onDeleteView={this.props.onDeleteView.bind(this, id)}
|
||||
onMoveViewToFolder={(targetFolderId) => {
|
||||
this.props.onMoveViewToFolder(folder.id, view.id, targetFolderId);
|
||||
}}
|
||||
onMoveView={this.props.onMoveView}
|
||||
onMoveFolder={this.props.onMoveFolder}
|
||||
views={views}
|
||||
foldersStr={foldersStr}
|
||||
currentPageId={this.props.currentPageId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getFolderClassName = (layerDragProps, state) => {
|
||||
if (!state || ! layerDragProps || !layerDragProps.clientOffset) {
|
||||
return 'view-folder-wrapper';
|
||||
}
|
||||
let y = layerDragProps.clientOffset.y;
|
||||
let top = this.foldWrapprRef.getBoundingClientRect().y;
|
||||
let className = '';
|
||||
// middle
|
||||
if (top + 10 < y && y < top + 30) {
|
||||
className += ' dragged-view-over ';
|
||||
}
|
||||
// top
|
||||
if (top + 10 > y) {
|
||||
className += ' can-drop-top ';
|
||||
}
|
||||
// bottom
|
||||
if (top + 30 < y) {
|
||||
className += ' can-drop ';
|
||||
}
|
||||
this.props.setClassName(className);
|
||||
return className + 'view-folder-wrapper';
|
||||
};
|
||||
|
||||
getFolderChildrenHeight = () => {
|
||||
const { id: folderId, children } = this.props.folder;
|
||||
const folded = this.props.getFolderState(folderId);
|
||||
if (folded) return 0;
|
||||
|
||||
let height = 0;
|
||||
children.forEach((child) => {
|
||||
// just support two levels
|
||||
const { type, id: childId, children } = child;
|
||||
if (type === FOLDER) {
|
||||
height += (this.props.getFolderState(childId) || !Array.isArray(children))
|
||||
? 40 : (children.length + 1) * 40;
|
||||
} else {
|
||||
height += 40;
|
||||
}
|
||||
});
|
||||
return height;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
connectDropTarget, connectDragPreview, connectDragSource, isOver, canDrop,
|
||||
isEditMode, folder, tableGridsLength, id_view_map, isOnlyOneView, layerDragProps,
|
||||
} = this.props;
|
||||
const { isEditing } = this.state;
|
||||
const { id: folderId, name, children, icon } = folder;
|
||||
const folded = this.props.getFolderState(folderId);
|
||||
let viewEditorId = `folder-item-${folderId}`;
|
||||
return (
|
||||
<div
|
||||
className={classnames('view-folder', { 'readonly': !isEditMode })}
|
||||
ref={ref => this.foldRef = ref}
|
||||
onClick={this.onToggleExpandFolder}
|
||||
>
|
||||
{connectDropTarget(
|
||||
connectDragPreview(
|
||||
<div
|
||||
className={this.getFolderClassName(layerDragProps, isOver && canDrop)}
|
||||
ref={ref => this.foldWrapprRef = ref}
|
||||
>
|
||||
<div className='folder-main'>
|
||||
{isEditMode ?
|
||||
connectDragSource(
|
||||
<div className="rdg-drag-handle">
|
||||
<Icon symbol={'drag'}/>
|
||||
</div>
|
||||
)
|
||||
:
|
||||
<div className="rdg-drag-handle"></div>
|
||||
}
|
||||
<div
|
||||
className='folder-content'
|
||||
style={{ marginLeft: `${(this.props.foldersStr.split('-').length - 1) * 16}px` }}
|
||||
id={viewEditorId}
|
||||
>
|
||||
{icon && <svg className="mr-2 svg-item"><use xlinkHref={`#${icon}`}/></svg>}
|
||||
<span className='folder-name text-truncate' title={name}>{name}</span>
|
||||
{isEditing &&
|
||||
<ViewEditPopover
|
||||
viewName={this.state.name}
|
||||
viewEditorId={viewEditorId}
|
||||
viewIcon={this.state.icon}
|
||||
onChangeName={this.onChangeName}
|
||||
onChangeIcon={this.onChangeIcon}
|
||||
toggleViewEditor={this.closeFolderEditor}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{isEditMode &&
|
||||
<FolderOperationDropdownMenu
|
||||
changeItemFreeze={this.changeItemFreeze}
|
||||
openFolderEditor={this.openFolderEditor}
|
||||
onDeleteFolder={this.props.onDeleteFolder}
|
||||
onToggleAddView={this.props.onToggleAddView}
|
||||
folderId={folderId}
|
||||
/>
|
||||
}
|
||||
<Icon className="icon-expand-folder" symbol={folded ? 'left-slide' : 'drop-down'}/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className="view-folder-children"
|
||||
style={{ height: this.getFolderChildrenHeight() }}
|
||||
onClick={this.onClickFolderChildren}
|
||||
>
|
||||
{!folded && children &&
|
||||
children.map((item, index) => {
|
||||
return item.type === 'folder' ? this.renderFolder(item, index, tableGridsLength, isOnlyOneView, id_view_map) : this.renderView(item, index, tableGridsLength, isOnlyOneView);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FolderItem.propTypes = {
|
||||
isEditMode: PropTypes.bool,
|
||||
folder: PropTypes.object,
|
||||
folderIndex: PropTypes.number,
|
||||
tableGridsLength: PropTypes.number,
|
||||
id_view_map: PropTypes.object,
|
||||
isOver: PropTypes.bool,
|
||||
canDrop: PropTypes.bool,
|
||||
isDragging: PropTypes.bool,
|
||||
draggedRow: PropTypes.object,
|
||||
connectDropTarget: PropTypes.func,
|
||||
connectDragPreview: PropTypes.func,
|
||||
connectDragSource: PropTypes.func,
|
||||
renderFolderMenuItems: PropTypes.func,
|
||||
duplicatePage: PropTypes.func,
|
||||
onSetFolderId: PropTypes.func,
|
||||
onToggleExpandFolder: PropTypes.func,
|
||||
onToggleAddView: PropTypes.func,
|
||||
onModifyFolder: PropTypes.func,
|
||||
onDeleteFolder: PropTypes.func,
|
||||
onSelectView: PropTypes.func,
|
||||
onUpdatePage: PropTypes.func,
|
||||
onDeleteView: PropTypes.func,
|
||||
onMoveViewToFolder: PropTypes.func,
|
||||
onMoveView: PropTypes.func,
|
||||
isOnlyOneView: PropTypes.bool,
|
||||
views: PropTypes.array,
|
||||
onMoveFolder: PropTypes.func,
|
||||
moveFolderToFolder: PropTypes.func,
|
||||
foldersStr: PropTypes.string,
|
||||
setClassName: PropTypes.func,
|
||||
getClassName: PropTypes.func,
|
||||
movePageOut: PropTypes.func,
|
||||
layerDragProps: PropTypes.object,
|
||||
getFolderState: PropTypes.func,
|
||||
currentPageId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FolderItem;
|
@@ -0,0 +1,112 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
|
||||
// import DeleteTip from '@/common/delete-tip';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
export default class FolderOperationDropdownMenu extends Component {
|
||||
|
||||
static propTypes = {
|
||||
changeItemFreeze: PropTypes.func,
|
||||
openFolderEditor: PropTypes.func,
|
||||
onDeleteFolder: PropTypes.func,
|
||||
onToggleAddView: PropTypes.func,
|
||||
onToggleAddArchiveView: PropTypes.func,
|
||||
folderId: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isMenuShow: false,
|
||||
showTip: false,
|
||||
};
|
||||
// this.isDesktop = checkDesktop();
|
||||
this.position = {};
|
||||
}
|
||||
|
||||
onDropdownToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
const isMenuShow = !this.state.isMenuShow;
|
||||
this.props.changeItemFreeze(isMenuShow);
|
||||
this.setState({ isMenuShow });
|
||||
};
|
||||
|
||||
openFolderEditor = (evt) => {
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
this.props.openFolderEditor();
|
||||
};
|
||||
|
||||
onDeleteFolder = (evt) => {
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
this.props.onDeleteFolder(this.props.folderId);
|
||||
};
|
||||
|
||||
// onClickDelete = (e) => {
|
||||
// if (this.isDesktop) {
|
||||
// e.stopPropagation();
|
||||
// const { top, left } = this.iconRef.getBoundingClientRect();
|
||||
// this.position = {
|
||||
// top: top,
|
||||
// left: left,
|
||||
// };
|
||||
// setTimeout(() => {
|
||||
// this.setState({ showTip: true });
|
||||
// }, 100);
|
||||
// } else {
|
||||
// this.onDeleteFolder(e);
|
||||
// }
|
||||
// };
|
||||
|
||||
closeTip = () => {
|
||||
this.setState({ showTip: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
isOpen={this.state.isMenuShow}
|
||||
toggle={this.onDropdownToggle}
|
||||
className="more-view-folder-operation"
|
||||
>
|
||||
<DropdownToggle tag="span" data-toggle="dropdown" aria-expanded={this.state.isMenuShow}>
|
||||
<Icon symbol={'more-level'}/>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu
|
||||
className="dtable-dropdown-menu large"
|
||||
flip={false}
|
||||
modifiers={{ preventOverflow: { boundariesElement: document.body } }}
|
||||
positionFixed={true}
|
||||
style={{ zIndex: 1051 }}
|
||||
>
|
||||
<DropdownItem onClick={this.props.onToggleAddView.bind(this, this.props.folderId)}>
|
||||
<Icon symbol={'main-view'}/>
|
||||
<span className="item-text">{gettext('Add page')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this.openFolderEditor}>
|
||||
<Icon symbol={'edit'}/>
|
||||
<span className="item-text">{gettext('Modify name')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
// onMouseDown={this.onClickDelete}
|
||||
onMouseDown={this.onDeleteFolder}
|
||||
>
|
||||
<Icon symbol={'delete'}/>
|
||||
<span className="item-text">{gettext('Delete folder')}</span>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{/* {this.isDesktop && this.state.showTip &&
|
||||
<DeleteTip
|
||||
position={this.position}
|
||||
toggle={this.closeTip}
|
||||
onDelete={this.onDeleteFolder}
|
||||
deleteTip={gettext('Are_you_sure_you_want_to_delete_this_folder_and_the_pages_in_it')}
|
||||
/>
|
||||
} */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
|
||||
export default DragDropContext(HTML5Backend);
|
3
frontend/src/pages/wiki/view-structure/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import ViewStructure from './view-structure';
|
||||
|
||||
export default ViewStructure;
|
98
frontend/src/pages/wiki/view-structure/new-folder-dialog.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input, Button, Alert } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
export default class NewFolderDialog extends Component {
|
||||
|
||||
static propTypes = {
|
||||
onAddFolder: PropTypes.func,
|
||||
onToggleAddFolderDialog: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
folderName: '',
|
||||
errMessage: '',
|
||||
iconClassName: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onHotKey);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onHotKey);
|
||||
}
|
||||
|
||||
onHotKey = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
this.handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = (event) => {
|
||||
let { folderName } = this.state;
|
||||
let value = event.target.value;
|
||||
if (value === folderName) {
|
||||
return;
|
||||
}
|
||||
this.setState({ folderName: value });
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
let { folderName, iconClassName } = this.state;
|
||||
if (!folderName) {
|
||||
this.setState({ errMessage: gettext('Name_is_required') });
|
||||
return;
|
||||
}
|
||||
this.props.onAddFolder({ name: folderName, icon: iconClassName });
|
||||
this.props.onToggleAddFolderDialog();
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.props.onToggleAddFolderDialog();
|
||||
};
|
||||
|
||||
onIconChange = (className) => {
|
||||
this.setState({ iconClassName: className });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { folderName, errMessage } = this.state;
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
toggle={this.toggle}
|
||||
autoFocus={false}
|
||||
className="new-folder-dialog"
|
||||
>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('New folder')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label for="folderName">{gettext('Name')}</Label>
|
||||
<Input
|
||||
id="folderName"
|
||||
value={folderName}
|
||||
innerRef={input => {
|
||||
this.newInput = input;
|
||||
}}
|
||||
onChange={this.handleChange}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
{errMessage && <Alert color="danger" className="mt-2">{errMessage}</Alert>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
232
frontend/src/pages/wiki/view-structure/page-utils.js
Normal file
@@ -0,0 +1,232 @@
|
||||
const FOLDER = 'folder';
|
||||
const PAGE = 'page';
|
||||
|
||||
export default class PageUtils {
|
||||
|
||||
static getPageById = (pages, page_id) => {
|
||||
if (!page_id || !Array.isArray(pages)) return null;
|
||||
return pages.find((page) => page.id === page_id) || null;
|
||||
};
|
||||
|
||||
static getPageFromNavigationById = (navigation, page_id) => {
|
||||
if (!page_id || !Array.isArray(navigation)) return null;
|
||||
let page_index = navigation.indexOf(item => item.id === page_id);
|
||||
if (page_index > -1) {
|
||||
return navigation[page_index];
|
||||
}
|
||||
for (let i = 0; i < navigation.length; i++) {
|
||||
const currNavigation = navigation[i];
|
||||
if (currNavigation.id === page_id) {
|
||||
return currNavigation;
|
||||
}
|
||||
|
||||
if (Array.isArray(currNavigation.children)) {
|
||||
for (let j = 0; j < currNavigation.children.length; j++) {
|
||||
if (currNavigation.children[j].id === page_id) {
|
||||
return currNavigation.children[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
static getPageIndexById = (pageId, pages) => {
|
||||
return pages.findIndex(page => page.id === pageId);
|
||||
};
|
||||
|
||||
static getFolderById = (list, folder_id) => {
|
||||
if (!folder_id || !Array.isArray(list)) return null;
|
||||
return list.find(item => item.type === FOLDER && item.id === folder_id);
|
||||
};
|
||||
|
||||
static getFolderIndexById = (list, folder_id) => {
|
||||
if (!folder_id || !Array.isArray(list)) return -1;
|
||||
return list.findIndex(folder => folder.id === folder_id);
|
||||
};
|
||||
|
||||
static modifyFolder(navigation, folder_id, folder_data) {
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._modifyFolder(item, folder_id, folder_data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static _modifyFolder(folder, folder_id, folder_data) {
|
||||
if (folder.id === folder_id) {
|
||||
for (let key in folder_data) {
|
||||
folder[key] = folder_data[key];
|
||||
}
|
||||
return;
|
||||
}
|
||||
folder.children.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._modifyFolder(item, folder_id, folder_data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static deleteFolder(navigation, pages, folder_id) {
|
||||
// delete folder and pages within it
|
||||
const folderIndex = this.getFolderIndexById(navigation, folder_id);
|
||||
if (folderIndex > -1) {
|
||||
this._deletePagesInFolder(pages, navigation[folderIndex]);
|
||||
navigation.splice(folderIndex, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// delete subfolder and pages within it
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
const folderIndex = this.getFolderIndexById(item.children, folder_id);
|
||||
if (folderIndex > -1) {
|
||||
const subfolder = item.children[folderIndex];
|
||||
this._deletePagesInFolder(pages, subfolder);
|
||||
item.children.splice(folderIndex, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
static _deletePagesInFolder(pages, folder) {
|
||||
folder.children.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._deletePagesInFolder(pages, item);
|
||||
}
|
||||
if (item.type === PAGE) {
|
||||
let index = this.getPageIndexById(item.id, pages);
|
||||
pages.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static addPage(navigation, page_id, target_folder_id) {
|
||||
// 1. Add pages directly under the root directory
|
||||
if (!target_folder_id) {
|
||||
navigation.push({ id: page_id, type: PAGE });
|
||||
return;
|
||||
} else {
|
||||
// 2. Add pages to the folder
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._addPageInFolder(page_id, item, target_folder_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static _addPageInFolder(page_id, folder, target_folder_id) {
|
||||
if (folder.id === target_folder_id) {
|
||||
folder.children.push({ id: page_id, type: PAGE });
|
||||
return true;
|
||||
}
|
||||
folder.children.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._addPageInFolder(page_id, item, target_folder_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static insertPage(navigation, page_id, target_page_id, folder_id, move_position) {
|
||||
// 1. No folder, insert page in root directory
|
||||
if (!folder_id) {
|
||||
let insertIndex = target_page_id ? navigation.findIndex(item => item.id === target_page_id) : -1;
|
||||
if (insertIndex < 0) {
|
||||
this.addPage(navigation, page_id, folder_id);
|
||||
return true;
|
||||
}
|
||||
if (move_position === 'move_below') {
|
||||
insertIndex++;
|
||||
}
|
||||
navigation.splice(insertIndex, 0, { id: page_id, type: PAGE });
|
||||
return;
|
||||
}
|
||||
// 2. If there is a folder, find it and insert it
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._insertPageIntoFolder(item, page_id, target_page_id, folder_id, move_position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static _insertPageIntoFolder(folder, page_id, target_page_id, folder_id, move_position) {
|
||||
if (folder.id === folder_id) {
|
||||
let insertIndex = target_page_id ? folder.children.findIndex(item => item.id === target_page_id) : -1;
|
||||
if (move_position === 'move_below') {
|
||||
insertIndex++;
|
||||
}
|
||||
folder.children.splice(insertIndex, 0, { id: page_id, type: PAGE });
|
||||
return;
|
||||
}
|
||||
folder.children.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._insertPageIntoFolder(item, page_id, target_page_id, folder_id, move_position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Move the page to the top or bottom of the folder
|
||||
static insertPageOut(navigation, page_id, folder_id, move_position) {
|
||||
let indexOffset = 0;
|
||||
if (move_position === 'move_below') {
|
||||
indexOffset++;
|
||||
}
|
||||
let page = { id: page_id, type: PAGE };
|
||||
let folder_index = this.getFolderIndexById(navigation, folder_id);
|
||||
if (folder_index > -1) {
|
||||
navigation.splice(folder_index + indexOffset, 0, page);
|
||||
} else {
|
||||
navigation.forEach((item) => {
|
||||
if (item.type === FOLDER) {
|
||||
let folder_index = this.getFolderIndexById(item.children, folder_id);
|
||||
if (folder_index > -1) {
|
||||
item.children.splice(folder_index + indexOffset, 0, page);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static deletePage(navigation, page_id) {
|
||||
// 1. Delete pages directly under the root directory
|
||||
const pageIndex = navigation.findIndex(item => item.id === page_id);
|
||||
if (pageIndex > -1) {
|
||||
navigation.splice(pageIndex, 1);
|
||||
return true;
|
||||
}
|
||||
// 2. Delete Page in Folder
|
||||
navigation.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._deletePageInFolder(item, page_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static _deletePageInFolder(folder, page_id) {
|
||||
let pageIndex = folder.children.findIndex(item => item.id === page_id);
|
||||
if (pageIndex > -1) {
|
||||
folder.children.splice(pageIndex, 1);
|
||||
return true;
|
||||
}
|
||||
folder.children.forEach(item => {
|
||||
if (item.type === FOLDER) {
|
||||
this._deletePageInFolder(item, page_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// movePageintoFolder
|
||||
static movePage(navigation, moved_page_id, target_page_id, source_folder_id, target_folder_id, move_position) {
|
||||
this.deletePage(navigation, moved_page_id, source_folder_id);
|
||||
this.insertPage(navigation, moved_page_id, target_page_id, target_folder_id, move_position);
|
||||
}
|
||||
|
||||
// movePageOutsideFolder
|
||||
static movePageOut(navigation, moved_page_id, source_folder_id, target_folder_id, move_position) {
|
||||
this.deletePage(navigation, moved_page_id, source_folder_id);
|
||||
this.insertPageOut(navigation, moved_page_id, target_folder_id, move_position);
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CommonAddTool from '../../../components/common/common-add-tool';
|
||||
import AddViewDropdownMenu from './add-view-dropdownmenu';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
class ViewStructureFooter extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isShowAddViewDropdownMenu: false,
|
||||
isAddToolHover: false,
|
||||
};
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({ isAddToolHover: true });
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({ isAddToolHover: false });
|
||||
};
|
||||
|
||||
onToggleAddViewDropdown = (event) => {
|
||||
event && event.stopPropagation();
|
||||
this.setState({ isShowAddViewDropdownMenu: !this.state.isShowAddViewDropdownMenu });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className='view-structure-footer'
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
ref={ref => this.viewFooterRef = ref}
|
||||
>
|
||||
<div className='add-view-wrapper'>
|
||||
<CommonAddTool
|
||||
className='add-view-btn'
|
||||
callBack={this.onToggleAddViewDropdown}
|
||||
footerName={gettext('Add page or folder')}
|
||||
/>
|
||||
{this.state.isShowAddViewDropdownMenu &&
|
||||
<AddViewDropdownMenu
|
||||
onToggleAddViewDropdown={this.onToggleAddViewDropdown}
|
||||
onToggleAddView={this.props.onToggleAddView}
|
||||
onToggleAddFolder={this.props.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewStructureFooter.propTypes = {
|
||||
onToggleAddView: PropTypes.func,
|
||||
onToggleAddFolder: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ViewStructureFooter;
|
233
frontend/src/pages/wiki/view-structure/view-structure.js
Normal file
@@ -0,0 +1,233 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { DropTarget, DragLayer } from 'react-dnd';
|
||||
import html5DragDropContext from './html5DragDropContext';
|
||||
import DraggedFolderItem from './folders/dragged-folder-item';
|
||||
import ViewItem from './views/view-item';
|
||||
import ViewStructureFooter from './view-structure-footer';
|
||||
import { repoID } from '../../../utils/constants';
|
||||
|
||||
import '../css/view-structure.css';
|
||||
|
||||
export const FOLDER = 'folder';
|
||||
export const PAGE = 'page';
|
||||
|
||||
class ViewStructure extends Component {
|
||||
|
||||
static propTypes = {
|
||||
isEditMode: PropTypes.bool,
|
||||
navigation: PropTypes.array,
|
||||
views: PropTypes.array,
|
||||
onTogglePinViewList: PropTypes.func,
|
||||
onToggleAddView: PropTypes.func,
|
||||
onToggleAddFolder: PropTypes.func,
|
||||
onModifyFolder: PropTypes.func,
|
||||
onDeleteFolder: PropTypes.func,
|
||||
onMoveFolder: PropTypes.func,
|
||||
onSelectView: PropTypes.func,
|
||||
onUpdatePage: PropTypes.func,
|
||||
onDeleteView: PropTypes.func,
|
||||
onMoveView: PropTypes.func,
|
||||
moveFolderToFolder: PropTypes.func,
|
||||
movePageOut: PropTypes.func,
|
||||
duplicatePage: PropTypes.func,
|
||||
onSetFolderId: PropTypes.func,
|
||||
currentPageId: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.folderClassNameCache = '';
|
||||
this.idFoldedStatusMap = this.getFoldedFoldersFromBase();
|
||||
}
|
||||
|
||||
getFoldedFoldersFromBase = () => {
|
||||
const foldedFolders = window.localStorage.getItem(`wiki-folded-folders-${repoID}`);
|
||||
return foldedFolders ? JSON.parse(foldedFolders) : {};
|
||||
};
|
||||
|
||||
setFoldedFolders = (foldedFolders) => {
|
||||
window.localStorage.setItem(`wiki-folded-folders-${repoID}`, JSON.stringify(foldedFolders));
|
||||
};
|
||||
|
||||
getFolderState = (folderId) => {
|
||||
return this.idFoldedStatusMap[folderId];
|
||||
};
|
||||
|
||||
onToggleExpandFolder = (folderId) => {
|
||||
const idFoldedStatusMap = this.getFoldedFoldersFromBase();
|
||||
if (idFoldedStatusMap[folderId]) {
|
||||
delete idFoldedStatusMap[folderId];
|
||||
} else {
|
||||
idFoldedStatusMap[folderId] = true;
|
||||
}
|
||||
this.setFoldedFolders(idFoldedStatusMap);
|
||||
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,
|
||||
source_view_folder_id,
|
||||
target_view_folder_id,
|
||||
target_view_id: null,
|
||||
move_position: 'move_below'
|
||||
});
|
||||
};
|
||||
|
||||
renderFolderMenuItems = ({ currentFolderId, onMoveViewToFolder }) => {
|
||||
// folder lists (in the root directory)
|
||||
const { navigation } = this.props;
|
||||
let renderFolders = navigation.filter(item => item.type === 'folder' && item.id !== currentFolderId);
|
||||
return renderFolders.map(folder => {
|
||||
const { id, name } = folder;
|
||||
return (
|
||||
<DropdownItem key={`move-to-folder-${id}`} onClick={onMoveViewToFolder.bind(this, id)}>
|
||||
<span className="folder-name text-truncate" title={name}>{name}</span>
|
||||
</DropdownItem>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
setClassName = (name) => {
|
||||
this.folderClassNameCache = name;
|
||||
};
|
||||
|
||||
getClassName = () => {
|
||||
return this.folderClassNameCache;
|
||||
};
|
||||
|
||||
renderFolder = (folder, index, tableGridsLength, isOnlyOneView, id_view_map, layerDragProps) => {
|
||||
const { isEditMode, views } = this.props;
|
||||
const folderId = folder.id;
|
||||
return (
|
||||
<DraggedFolderItem
|
||||
key={`view-folder-${folderId}`}
|
||||
isEditMode={isEditMode}
|
||||
folder={folder}
|
||||
folderIndex={index}
|
||||
tableGridsLength={tableGridsLength}
|
||||
isOnlyOneView={isOnlyOneView}
|
||||
id_view_map={id_view_map}
|
||||
renderFolderMenuItems={this.renderFolderMenuItems}
|
||||
onToggleExpandFolder={this.onToggleExpandFolder}
|
||||
onToggleAddView={this.props.onToggleAddView}
|
||||
onDeleteFolder={this.props.onDeleteFolder}
|
||||
onMoveFolder={this.props.onMoveFolder}
|
||||
onSelectView={this.props.onSelectView}
|
||||
onUpdatePage={this.props.onUpdatePage}
|
||||
duplicatePage={this.props.duplicatePage}
|
||||
onSetFolderId={this.props.onSetFolderId}
|
||||
onDeleteView={this.props.onDeleteView}
|
||||
onMoveViewToFolder={this.onMoveViewToFolder}
|
||||
onMoveView={this.props.onMoveView}
|
||||
views={views}
|
||||
moveFolderToFolder={this.props.moveFolderToFolder}
|
||||
foldersStr={folderId}
|
||||
layerDragProps={layerDragProps}
|
||||
setClassName={this.setClassName}
|
||||
getClassName={this.getClassName}
|
||||
movePageOut={this.props.movePageOut}
|
||||
onModifyFolder={this.props.onModifyFolder}
|
||||
getFolderState={this.getFolderState}
|
||||
currentPageId={this.props.currentPageId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderView = (view, index, tableGridsLength, isOnlyOneView, id_view_map) => {
|
||||
const { isEditMode, views } = this.props;
|
||||
const id = view.id;
|
||||
if (!views.find(item => item.id === id)) return;
|
||||
const folderId = null; // Pages in the root directory, no folders, use null
|
||||
return (
|
||||
<ViewItem
|
||||
key={id}
|
||||
tableGridsLength={tableGridsLength}
|
||||
isOnlyOneView={isOnlyOneView}
|
||||
infolder={false}
|
||||
view={views.find(item => item.id === id)}
|
||||
views={views}
|
||||
viewIndex={index}
|
||||
folderId={folderId}
|
||||
isEditMode={isEditMode}
|
||||
renderFolderMenuItems={this.renderFolderMenuItems}
|
||||
duplicatePage={this.props.duplicatePage}
|
||||
onSetFolderId={this.props.onSetFolderId}
|
||||
onSelectView={() => this.props.onSelectView(id)}
|
||||
onUpdatePage={this.props.onUpdatePage}
|
||||
onDeleteView={this.props.onDeleteView.bind(this, id)}
|
||||
onMoveViewToFolder={(targetFolderId) => {
|
||||
this.onMoveViewToFolder(folderId, view.id, targetFolderId);
|
||||
}}
|
||||
onMoveView={this.props.onMoveView}
|
||||
onMoveFolder={this.props.onMoveFolder}
|
||||
foldersStr={''}
|
||||
currentPageId={this.props.currentPageId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
renderStructureBody = React.forwardRef((layerDragProps, ref) => {
|
||||
const { navigation, views, isEditMode } = this.props;
|
||||
let isOnlyOneView = false;
|
||||
if (views.length === 1) {
|
||||
isOnlyOneView = true;
|
||||
}
|
||||
const tableGridsLength = views.length;
|
||||
let id_view_map = {};
|
||||
views.forEach(view => id_view_map[view.id] = view);
|
||||
const style = { maxHeight: isEditMode ? 'calc(100% - 40px)' : '100%' };
|
||||
return (
|
||||
<div className='view-structure-body' style={style}>
|
||||
{navigation.map((item, index) => {
|
||||
return item.type === 'folder' ?
|
||||
this.renderFolder(item, index, tableGridsLength, isOnlyOneView, id_view_map, layerDragProps) :
|
||||
this.renderView(item, index, tableGridsLength, isOnlyOneView, id_view_map);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
collect = (monitor) => {
|
||||
return {
|
||||
item: monitor.getItem(),
|
||||
itemType: monitor.getItemType(),
|
||||
clientOffset: monitor.getClientOffset(),
|
||||
isDragging: monitor.isDragging()
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const StructureBody = html5DragDropContext(
|
||||
DropTarget('ViewStructure', {}, connect => ({
|
||||
connectDropTarget: connect.dropTarget()
|
||||
}))(DragLayer(this.collect)(this.renderStructureBody))
|
||||
);
|
||||
const isSpecialInstance = false;
|
||||
const isDarkMode = false;
|
||||
return (
|
||||
<div className={classnames('view-structure',
|
||||
{ 'view-structure-dark': isDarkMode },
|
||||
{ 'view-structure-light': !isDarkMode },
|
||||
)}>
|
||||
<StructureBody />
|
||||
{(this.props.isEditMode && !isSpecialInstance) &&
|
||||
<ViewStructureFooter
|
||||
onToggleAddView={this.onToggleAddView.bind(this, null)}
|
||||
onToggleAddFolder={this.props.onToggleAddFolder}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ViewStructure;
|
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
|
||||
export default class DeleteDialog extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
closeDeleteDialog: PropTypes.func.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.props.closeDeleteDialog();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('Delete page')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure to delete this page?')}</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.props.handleSubmit}>{gettext('Delete')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { DropTarget } from 'react-dnd';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const DropTargetTopView = (Placeholder) => class extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
connectDropTarget: PropTypes.func.isRequired,
|
||||
isOver: PropTypes.bool,
|
||||
canDrop: PropTypes.bool,
|
||||
draggedRow: PropTypes.object,
|
||||
targetFolderId: PropTypes.string,
|
||||
targetViewId: PropTypes.string,
|
||||
onMoveView: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { connectDropTarget, isOver, canDrop, draggedRow } = this.props;
|
||||
const { mode } = draggedRow || {};
|
||||
if (mode !== 'view') {
|
||||
return null;
|
||||
}
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
zIndex: canDrop ? 1 : -1,
|
||||
};
|
||||
return connectDropTarget(
|
||||
<div style={style}>
|
||||
<Placeholder />
|
||||
{isOver && canDrop && <div className="view-drop-target" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const target = {
|
||||
drop(props, monitor) {
|
||||
const sourceRow = monitor.getItem();
|
||||
if (sourceRow.mode !== 'view') {
|
||||
return;
|
||||
}
|
||||
const { targetFolderId, targetViewId } = props;
|
||||
const sourceFolderId = sourceRow.folderId;
|
||||
const draggedViewId = sourceRow.data.id;
|
||||
if (draggedViewId !== targetViewId) {
|
||||
props.onMoveView({
|
||||
moved_view_id: draggedViewId,
|
||||
target_view_id: targetViewId,
|
||||
source_view_folder_id: sourceFolderId,
|
||||
target_view_folder_id: targetFolderId,
|
||||
move_position: 'move_above'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function collect(connect, monitor) {
|
||||
return {
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
draggedRow: monitor.getItem(),
|
||||
};
|
||||
}
|
||||
|
||||
class Placeholder extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
key: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div key={this.props.key} style={{ height: 40, width: '100%' }}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DropTarget('ViewStructure', target, collect)(DropTargetTopView(Placeholder));
|
@@ -0,0 +1,190 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
|
||||
import toaster from '../../../../components/toast';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
export default class PageDropdownMenu extends Component {
|
||||
|
||||
static propTypes = {
|
||||
view: PropTypes.object.isRequired,
|
||||
views: PropTypes.array,
|
||||
tableGridsLength: PropTypes.number,
|
||||
folderId: PropTypes.string,
|
||||
canDelete: PropTypes.bool,
|
||||
canDuplicate: PropTypes.bool,
|
||||
renderFolderMenuItems: PropTypes.func,
|
||||
toggle: PropTypes.func,
|
||||
toggleViewEditor: PropTypes.func,
|
||||
duplicatePage: PropTypes.func,
|
||||
onSetFolderId: PropTypes.func,
|
||||
onDeleteView: PropTypes.func,
|
||||
onModifyViewType: PropTypes.func,
|
||||
onMoveViewToFolder: PropTypes.func,
|
||||
isOnlyOneView: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isShowMenu: false,
|
||||
};
|
||||
this.pageNameMap = this.calculateNameMap();
|
||||
}
|
||||
|
||||
calculateNameMap = () => {
|
||||
const { views } = this.props;
|
||||
return views.reduce((map, view) => {
|
||||
map[view.name] = true;
|
||||
return map;
|
||||
}, {});
|
||||
};
|
||||
|
||||
onDropdownToggle = (evt) => {
|
||||
if (evt.target && this.foldersDropdownToggle && this.foldersDropdownToggle.contains(evt.target)) {
|
||||
return;
|
||||
}
|
||||
evt.stopPropagation();
|
||||
this.props.toggle();
|
||||
};
|
||||
|
||||
onRenameView = (event) => {
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
this.props.toggleViewEditor();
|
||||
};
|
||||
|
||||
onDeleteView = (event) => {
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
this.props.onDeleteView();
|
||||
};
|
||||
|
||||
onModifyViewType = (event) => {
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
this.props.onModifyViewType();
|
||||
};
|
||||
|
||||
onMoveViewToFolder = (targetFolderId) => {
|
||||
this.props.onMoveViewToFolder(targetFolderId);
|
||||
};
|
||||
|
||||
onRemoveFromFolder = (evt) => {
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
this.props.onMoveViewToFolder(null);
|
||||
};
|
||||
|
||||
onToggleFoldersMenu = () => {
|
||||
this.setState({ isShowMenu: !this.state.isShowMenu });
|
||||
};
|
||||
|
||||
duplicatePage = () => {
|
||||
const { view, folderId } = this.props;
|
||||
const { id: from_page_id, name } = view;
|
||||
let duplicateCount = 1;
|
||||
let newName = name + '(copy)';
|
||||
while (this.pageNameMap[newName]) {
|
||||
newName = `${name}(copy${duplicateCount})`;
|
||||
duplicateCount++;
|
||||
}
|
||||
const onsuccess = () => {};
|
||||
this.props.onSetFolderId(folderId);
|
||||
this.props.duplicatePage({ name: newName, from_page_id }, onsuccess, this.duplicatePageFailure);
|
||||
};
|
||||
|
||||
duplicatePageFailure = () => {
|
||||
toaster.danger(gettext('Failed_to_duplicate_page'));
|
||||
};
|
||||
|
||||
showMenu = () => {
|
||||
this.setState({ isShowMenu: true });
|
||||
};
|
||||
|
||||
hideMenu = () => {
|
||||
this.setState({ isShowMenu: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
folderId, canDelete, canDuplicate, renderFolderMenuItems, tableGridsLength, isOnlyOneView,
|
||||
} = this.props;
|
||||
const folderMenuItems = renderFolderMenuItems && renderFolderMenuItems({ currentFolderId: folderId, onMoveViewToFolder: this.onMoveViewToFolder });
|
||||
return (
|
||||
<Dropdown
|
||||
isOpen={true}
|
||||
toggle={this.onDropdownToggle}
|
||||
className="view-operation-dropdown"
|
||||
>
|
||||
<DropdownToggle className="view-operation-dropdown-toggle" tag="span" data-toggle="dropdown"></DropdownToggle>
|
||||
<DropdownMenu
|
||||
className="view-operation-dropdown-menu dtable-dropdown-menu large"
|
||||
flip={false}
|
||||
modifiers={{ preventOverflow: { boundariesElement: document.body } }}
|
||||
positionFixed={true}
|
||||
style={{ zIndex: 1051 }}
|
||||
>
|
||||
<DropdownItem onClick={this.onRenameView}>
|
||||
<Icon symbol={'edit'}/>
|
||||
<span className="item-text">{gettext('Modify name')}</span>
|
||||
</DropdownItem>
|
||||
{canDuplicate &&
|
||||
<DropdownItem onClick={this.duplicatePage}>
|
||||
<Icon symbol={'copy'}/>
|
||||
<span className="item-text">{gettext('Duplicate page')}</span>
|
||||
</DropdownItem>
|
||||
}
|
||||
{(isOnlyOneView || tableGridsLength === 1 || !canDelete) ? '' : (
|
||||
<DropdownItem onClick={this.onDeleteView}>
|
||||
<Icon symbol={'delete'}/>
|
||||
<span className="item-text">{gettext('Delete page')}</span>
|
||||
</DropdownItem>
|
||||
)}
|
||||
{folderId &&
|
||||
<DropdownItem onClick={this.onRemoveFromFolder}>
|
||||
<Icon symbol={'remove-from-folder'}/>
|
||||
<span className="item-text">{gettext('Remove from folder')}</span>
|
||||
</DropdownItem>
|
||||
}
|
||||
{renderFolderMenuItems && folderMenuItems.length > 0 &&
|
||||
<DropdownItem
|
||||
className="pr-2 btn-move-to-folder"
|
||||
tag="div"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
this.showMenu();
|
||||
}}
|
||||
onMouseEnter={this.showMenu}
|
||||
onMouseLeave={this.hideMenu}
|
||||
>
|
||||
<Dropdown
|
||||
className="folders-dropdown"
|
||||
direction="right"
|
||||
isOpen={this.state.isShowMenu}
|
||||
toggle={this.onToggleFoldersMenu}
|
||||
>
|
||||
<div className="folders-dropdown-toggle" ref={ref => this.foldersDropdownToggle = ref}>
|
||||
<Icon symbol={'move-to'}/>
|
||||
<span className="item-text">{gettext('Move to')}</span>
|
||||
<span className="icon-dropdown-toggle">
|
||||
<Icon className="mr-0" symbol={'right-slide'}/>
|
||||
</span>
|
||||
<DropdownToggle className="move-to-folders-toggle"></DropdownToggle>
|
||||
</div>
|
||||
{this.state.isShowMenu &&
|
||||
<DropdownMenu
|
||||
className="folders-dropdown-menu"
|
||||
flip={false}
|
||||
modifiers={{ preventOverflow: { boundariesElement: document.body } }}
|
||||
positionFixed={true}
|
||||
>
|
||||
{folderMenuItems}
|
||||
</DropdownMenu>
|
||||
}
|
||||
</Dropdown>
|
||||
</DropdownItem>
|
||||
}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PopoverBody } from 'reactstrap';
|
||||
import SeahubPopover from '../../../../components/common/seahub-popover';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
|
||||
import '../../css/view-edit-popover.css';
|
||||
|
||||
|
||||
class ViewEditPopover extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.viewInputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const txtLength = this.props.viewName.length;
|
||||
this.viewInputRef.current.setSelectionRange(0, txtLength);
|
||||
}
|
||||
|
||||
onChangeName = (e) => {
|
||||
let name = e.target.value;
|
||||
this.props.onChangeName(name);
|
||||
};
|
||||
|
||||
onEnter = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.toggleViewEditor();
|
||||
};
|
||||
|
||||
renderViewName = () => {
|
||||
const { viewName } = this.props;
|
||||
return (
|
||||
<div className="view-name-editor">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control view-name-editor-input"
|
||||
value={viewName}
|
||||
onChange={this.onChangeName}
|
||||
autoFocus={true}
|
||||
ref={this.viewInputRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SeahubPopover
|
||||
placement='bottom-end'
|
||||
target={this.props.viewEditorId}
|
||||
hideSeahubPopover={this.props.toggleViewEditor}
|
||||
hideSeahubPopoverWithEsc={this.props.toggleViewEditor}
|
||||
onEnter={this.onEnter}
|
||||
hideArrow={true}
|
||||
popoverClassName="view-edit-popover"
|
||||
>
|
||||
<div className="view-edit-popover-header">
|
||||
<span className='header-text'>{gettext('Modify Name')}</span>
|
||||
</div>
|
||||
<PopoverBody className="view-edit-content">
|
||||
{this.renderViewName()}
|
||||
</PopoverBody>
|
||||
</SeahubPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewEditPopover.propTypes = {
|
||||
viewName: PropTypes.string,
|
||||
onChangeName: PropTypes.func,
|
||||
toggleViewEditor: PropTypes.func,
|
||||
viewEditorId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ViewEditPopover;
|
324
frontend/src/pages/wiki/view-structure/views/view-item.js
Normal file
@@ -0,0 +1,324 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
import ViewEditPopover from './view-edit-popover';
|
||||
import PageDropdownMenu from './page-dropdownmenu';
|
||||
import DeleteDialog from './delete-dialog';
|
||||
import { DRAGGED_FOLDER_MODE, DRAGGED_VIEW_MODE } from '../constant';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
const dragSource = {
|
||||
beginDrag: props => {
|
||||
return {
|
||||
idx: props.viewIndex,
|
||||
data: { ...props.view, index: props.viewIndex },
|
||||
folderId: props.folderId,
|
||||
mode: DRAGGED_VIEW_MODE,
|
||||
};
|
||||
},
|
||||
endDrag(props, monitor) {
|
||||
const viewSource = monitor.getItem();
|
||||
const didDrop = monitor.didDrop();
|
||||
let viewTarget = {};
|
||||
if (!didDrop) {
|
||||
return { viewSource, viewTarget };
|
||||
}
|
||||
},
|
||||
isDragging(props) {
|
||||
const { draggedRow, infolder, viewIndex: targetIndex } = props;
|
||||
if (infolder) {
|
||||
return false;
|
||||
}
|
||||
const { idx } = draggedRow;
|
||||
return idx > targetIndex;
|
||||
}
|
||||
};
|
||||
|
||||
const dropTarget = {
|
||||
drop(props, monitor) {
|
||||
const sourceRow = monitor.getItem();
|
||||
// 1 drag page
|
||||
if (sourceRow.mode === DRAGGED_VIEW_MODE) {
|
||||
const { infolder, viewIndex: targetIndex, view: targetView, folderId: targetFolderId } = props;
|
||||
const sourceFolderId = sourceRow.folderId;
|
||||
const draggedViewId = sourceRow.data.id;
|
||||
const targetViewId = targetView.id;
|
||||
|
||||
if (draggedViewId !== targetViewId) {
|
||||
const sourceIndex = sourceRow.idx;
|
||||
let move_position;
|
||||
if (infolder) {
|
||||
move_position = 'move_below';
|
||||
} else {
|
||||
move_position = sourceIndex > targetIndex ? 'move_above' : 'move_below';
|
||||
}
|
||||
|
||||
props.onMoveView({
|
||||
moved_view_id: draggedViewId,
|
||||
target_view_id: targetViewId,
|
||||
source_view_folder_id: sourceFolderId,
|
||||
target_view_folder_id: targetFolderId,
|
||||
move_position,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 1 drag folder
|
||||
if (sourceRow.mode === DRAGGED_FOLDER_MODE) {
|
||||
const { viewIndex: targetIndex, view: targetView } = props;
|
||||
const draggedFolderId = sourceRow.data.id;
|
||||
const targetViewId = targetView.id;
|
||||
const sourceIndex = sourceRow.idx;
|
||||
// Drag the parent folder to the child page, return
|
||||
if (props.foldersStr.split('-').includes(draggedFolderId)) return;
|
||||
props.onMoveFolder(
|
||||
draggedFolderId,
|
||||
targetViewId,
|
||||
sourceIndex > targetIndex ? 'move_above' : 'move_below',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dragCollect = (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
connectDragPreview: connect.dragPreview(),
|
||||
isDragging: monitor.isDragging()
|
||||
});
|
||||
|
||||
const dropCollect = (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
draggedRow: monitor.getItem()
|
||||
});
|
||||
|
||||
class ViewItem extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isShowViewEditor: false,
|
||||
isShowViewOperationDropdown: false,
|
||||
isShowDeleteDialog: false,
|
||||
viewName: props.view.name || '',
|
||||
viewIcon: props.view.icon,
|
||||
isSelected: props.currentPageId === props.view.id,
|
||||
};
|
||||
this.viewItemRef = React.createRef();
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
if (this.state.isSelected) return;
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
if (this.state.isSelected) return;
|
||||
};
|
||||
|
||||
onCurrentPageChanged = (currentPageId) => {
|
||||
const { isSelected } = this.state;
|
||||
if (currentPageId === this.props.view.id && isSelected === false) {
|
||||
this.setState({ isSelected: true });
|
||||
} else if (currentPageId !== this.props.view.id && isSelected === true) {
|
||||
this.setState({ isSelected: false });
|
||||
}
|
||||
};
|
||||
|
||||
toggleViewEditor = (e) => {
|
||||
if (e) e.stopPropagation();
|
||||
this.setState({ isShowViewEditor: !this.state.isShowViewEditor }, () => {
|
||||
if (!this.state.isShowViewEditor) {
|
||||
this.saveViewProperties();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
saveViewProperties = () => {
|
||||
const { name, icon, id } = this.props.view;
|
||||
const { viewIcon } = this.state;
|
||||
let viewName = this.state.viewName.trim();
|
||||
if (viewIcon !== icon || viewName !== name) {
|
||||
let newView = {};
|
||||
if (viewName !== name) {
|
||||
newView.name = viewName;
|
||||
}
|
||||
if (viewIcon !== icon) {
|
||||
newView.icon = viewIcon;
|
||||
}
|
||||
this.props.onUpdatePage(id, newView);
|
||||
}
|
||||
};
|
||||
|
||||
onChangeName = (newViewName) => {
|
||||
this.setState({ viewName: newViewName });
|
||||
};
|
||||
|
||||
onChangeIcon = (newViewIcon) => {
|
||||
this.setState({ viewIcon: newViewIcon });
|
||||
};
|
||||
|
||||
openDeleteDialog = () => {
|
||||
this.setState({ isShowDeleteDialog: true });
|
||||
};
|
||||
|
||||
closeDeleteDialog = () => {
|
||||
this.setState({ isShowDeleteDialog: false });
|
||||
};
|
||||
|
||||
onViewOperationDropdownToggle = () => {
|
||||
const isShowViewOperationDropdown = !this.state.isShowViewOperationDropdown;
|
||||
this.setState({ isShowViewOperationDropdown });
|
||||
this.changeItemFreeze(isShowViewOperationDropdown);
|
||||
};
|
||||
|
||||
changeItemFreeze = (isFreeze) => {
|
||||
if (isFreeze) {
|
||||
this.viewItemRef.classList.add('view-freezed');
|
||||
} else {
|
||||
this.viewItemRef.classList.remove('view-freezed');
|
||||
}
|
||||
};
|
||||
|
||||
renderIcon = (icon) => {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
if (icon.includes('dtable-icon')) {
|
||||
return <span className={`mr-2 dtable-font ${icon}`}></span>;
|
||||
} else {
|
||||
return <Icon className="mr-2" symbol={icon}/>;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
connectDragSource, connectDragPreview, connectDropTarget, isOver, canDrop, isDragging,
|
||||
infolder, view, tableGridsLength, isEditMode, folderId, isOnlyOneView, foldersStr,
|
||||
} = this.props;
|
||||
const { isShowViewEditor, viewName, viewIcon, isSelected } = this.state;
|
||||
const isOverView = isOver && canDrop;
|
||||
|
||||
const isSpecialInstance = false;
|
||||
|
||||
let viewCanDropTop;
|
||||
let viewCanDrop;
|
||||
if (infolder) {
|
||||
viewCanDropTop = false;
|
||||
viewCanDrop = isOverView;
|
||||
} else {
|
||||
viewCanDropTop = isOverView && isDragging;
|
||||
viewCanDrop = isOverView && !isDragging;
|
||||
}
|
||||
let viewEditorId = `view-editor-${view.id}`;
|
||||
|
||||
return connectDropTarget(
|
||||
connectDragPreview(
|
||||
<div
|
||||
className={classnames('view-item', 'view',
|
||||
{ 'selected-view': isSelected },
|
||||
{ 'view-can-drop-top': viewCanDropTop },
|
||||
{ 'view-can-drop': viewCanDrop },
|
||||
{ 'readonly': !isEditMode },
|
||||
)}
|
||||
ref={ref => this.viewItemRef = ref}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
id={viewEditorId}
|
||||
>
|
||||
<div className="view-item-main" onClick={isShowViewEditor ? () => {} : this.props.onSelectView}>
|
||||
{(isEditMode && !isSpecialInstance) ?
|
||||
connectDragSource(
|
||||
<div className="rdg-drag-handle">
|
||||
<Icon symbol={'drag'}/>
|
||||
</div>
|
||||
)
|
||||
:
|
||||
<div className="rdg-drag-handle"></div>
|
||||
}
|
||||
<div className='view-content' style={foldersStr ? { marginLeft: `${(foldersStr.split('-').length) * 24}px` } : {}}>
|
||||
{this.renderIcon(view.icon)}
|
||||
<span className="view-title text-truncate" title={view.name}>{view.name}</span>
|
||||
{isShowViewEditor && (
|
||||
<ViewEditPopover
|
||||
viewName={viewName}
|
||||
viewIcon={viewIcon}
|
||||
viewEditorId={viewEditorId}
|
||||
onChangeName={this.onChangeName}
|
||||
onChangeIcon={this.onChangeIcon}
|
||||
toggleViewEditor={this.toggleViewEditor}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
{isEditMode &&
|
||||
<div className="more-view-operation" onClick={this.onViewOperationDropdownToggle}>
|
||||
<Icon symbol={'more-level'}/>
|
||||
{this.state.isShowViewOperationDropdown &&
|
||||
<PageDropdownMenu
|
||||
view={view}
|
||||
views={this.props.views}
|
||||
tableGridsLength={tableGridsLength}
|
||||
isOnlyOneView={isOnlyOneView}
|
||||
folderId={folderId}
|
||||
canDelete={!isSpecialInstance}
|
||||
canDuplicate={!isSpecialInstance}
|
||||
toggle={this.onViewOperationDropdownToggle}
|
||||
renderFolderMenuItems={this.props.renderFolderMenuItems}
|
||||
toggleViewEditor={this.toggleViewEditor}
|
||||
duplicatePage={this.props.duplicatePage}
|
||||
onSetFolderId={this.props.onSetFolderId}
|
||||
onDeleteView={this.openDeleteDialog}
|
||||
onMoveViewToFolder={this.props.onMoveViewToFolder}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{this.state.isShowDeleteDialog &&
|
||||
<DeleteDialog
|
||||
closeDeleteDialog={this.closeDeleteDialog}
|
||||
handleSubmit={this.props.onDeleteView}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewItem.propTypes = {
|
||||
isOver: PropTypes.bool,
|
||||
canDrop: PropTypes.bool,
|
||||
isDragging: PropTypes.bool,
|
||||
draggedRow: PropTypes.object,
|
||||
isEditMode: PropTypes.bool,
|
||||
infolder: PropTypes.bool,
|
||||
view: PropTypes.object,
|
||||
views: PropTypes.array,
|
||||
viewIndex: PropTypes.number,
|
||||
folderId: PropTypes.string,
|
||||
tableGridsLength: PropTypes.number,
|
||||
connectDragSource: PropTypes.func,
|
||||
connectDragPreview: PropTypes.func,
|
||||
connectDropTarget: PropTypes.func,
|
||||
renderFolderMenuItems: PropTypes.func,
|
||||
duplicatePage: PropTypes.func,
|
||||
onSetFolderId: PropTypes.func,
|
||||
onSelectView: PropTypes.func,
|
||||
onUpdatePage: PropTypes.func,
|
||||
onDeleteView: PropTypes.func,
|
||||
onMoveViewToFolder: PropTypes.func,
|
||||
onMoveView: PropTypes.func,
|
||||
isOnlyOneView: PropTypes.bool,
|
||||
onMoveFolder: PropTypes.func,
|
||||
foldersStr: PropTypes.string,
|
||||
currentPageId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default DropTarget('ViewStructure', dropTarget, dropCollect)(
|
||||
DragSource('ViewStructure', dragSource, dragCollect)(ViewItem)
|
||||
);
|
@@ -0,0 +1,128 @@
|
||||
.app-settings-dialog .nav .nav-item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-settings-dialog .nav .nav-item .nav-link {
|
||||
padding: 0.5rem 0;
|
||||
font-weight: normal;
|
||||
transition: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-settings-dialog .nav .nav-item .nav-link.active {
|
||||
color: #ff8000;
|
||||
text-decoration: none;
|
||||
border-bottom: 0.125rem solid #ff8000;
|
||||
}
|
||||
|
||||
.app-settings-dialog .nav-pills .nav-item .nav-link {
|
||||
padding: 0.3125rem 1rem 0.3125rem 8px;
|
||||
}
|
||||
|
||||
.app-settings-dialog .nav-pills .nav-item .nav-link:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.app-settings-dialog .nav-pills .nav-item .nav-link.active {
|
||||
background-color: #ff8000;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.app-settings-dialog .ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.app-settings-dialog {
|
||||
max-width: 800px;
|
||||
height: calc(100% - 56px);
|
||||
}
|
||||
|
||||
.app-settings-dialog .modal-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-settings-dialog .app-settings-dialog-content {
|
||||
padding: 0;
|
||||
min-height: 27rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-settings-dialog-content .app-settings-dialog-side {
|
||||
display: flex;
|
||||
flex: 0 0 25%;
|
||||
padding: 12px 8px;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.app-settings-dialog-content .app-settings-dialog-main {
|
||||
display: flex;
|
||||
flex: 0 0 75%;
|
||||
padding: 12px 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.app-settings-dialog-content .app-settings-dialog-main .tab-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-settings-dialog-content .app-settings-dialog-main .tab-pane {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-settings-dialog-content .app-settings-dialog-main .no-search-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.app-settings-dialog-content .app-settings-dialog-main .no-search-result span {
|
||||
color: #aaa;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.app-settings-dialog-content .app-settings-dialog-main .search-text-clear {
|
||||
line-height: 38px;
|
||||
height: 38px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.app-setting-dialog-icon {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-setting-dialog-icon img {
|
||||
max-height: 128px;
|
||||
}
|
||||
|
||||
.app-setting-dialog-icon-description {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.app-setting-dialog-name {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.app-setting-dialog-name .rename-area-input {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.app-setting-dialog-name .rename-area-submit {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: #808080;
|
||||
margin-bottom: 1rem;
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'reactstrap';
|
||||
import { seafileAPI } from '../../../../utils/seafile-api';
|
||||
import { gettext, mediaUrl } from '../../../../utils/constants';
|
||||
import { getIconURL } from '../../utils';
|
||||
|
||||
class AppSettingsDialogCustomIcon extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
iconName: this.props.config.wiki_icon,
|
||||
};
|
||||
this.fileInput = React.createRef();
|
||||
}
|
||||
|
||||
openFileInput = () => {
|
||||
this.fileInput.current.click();
|
||||
};
|
||||
|
||||
uploadFile = () => {
|
||||
if (!this.fileInput.current.files.length) {
|
||||
return;
|
||||
}
|
||||
const file = this.fileInput.current.files[0];
|
||||
this.uploadLocalFile(file).then((iconName) => {
|
||||
let wikiConfig = Object.assign({}, this.props.config, {
|
||||
wiki_icon: iconName,
|
||||
});
|
||||
this.props.updateConfig(wikiConfig);
|
||||
this.props.onToggle();
|
||||
this.setState({
|
||||
iconName: iconName,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
uploadLocalFile = (imageFile) => {
|
||||
let repoID = this.props.repoId;
|
||||
const name = 'wiki-icon-image-' + Date.now().toString() + '.png';
|
||||
return (
|
||||
seafileAPI.getFileServerUploadLink(repoID, '/').then((res) => {
|
||||
const uploadLink = res.data + '?ret-json=1';
|
||||
const newFile = new File([imageFile], name, {type: imageFile.type});
|
||||
const formData = new FormData();
|
||||
formData.append('parent_dir', '/');
|
||||
formData.append('relative_path', '_Internal/Wiki/Icon');
|
||||
formData.append('file', newFile);
|
||||
return seafileAPI.uploadImage(uploadLink, formData);
|
||||
}).then ((res) => {
|
||||
return name;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const hasIcon = false;
|
||||
let { iconName } = this.state;
|
||||
const iconUrl = iconName ? getIconURL(this.props.repoId, iconName) : `${mediaUrl}img/wiki/default.png`;
|
||||
if (hasIcon) {
|
||||
return (
|
||||
<div className="app-setting-dialog-icon">
|
||||
<img src={iconUrl} alt="" width={128} height={128} ></img>
|
||||
<p className="mt-2 mb-1 app-setting-dialog-icon-description">
|
||||
{gettext('Please select a png image within 5MB.')}
|
||||
</p>
|
||||
<p className="app-setting-dialog-icon-description">
|
||||
{gettext('Recommended size is 256x256 px.')}
|
||||
</p>
|
||||
<Button color="primary" outline size="sm" onClick={this.openFileInput}>{gettext('Change icon')}</Button>
|
||||
<input className="d-none" type="file" accept="image/png" onChange={this.uploadFile} ref={this.fileInput} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="app-setting-dialog-icon mb-8">
|
||||
<p className="mt-2 mb-1 app-setting-dialog-icon-description">
|
||||
{gettext('Please select a png image within 5MB.')}
|
||||
</p>
|
||||
<p className="app-setting-dialog-icon-description">
|
||||
{gettext('Recommended size is 256x256 px.')}
|
||||
</p>
|
||||
<Button color="primary" outline size="sm" onClick={this.openFileInput}>{gettext('Upload icon')}</Button>
|
||||
<input className="d-none" type="file" accept="image/png" onChange={this.uploadFile} ref={this.fileInput} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppSettingsDialogCustomIcon.propTypes = {
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
updateConfig: PropTypes.func.isRequired,
|
||||
repoId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AppSettingsDialogCustomIcon;
|
@@ -0,0 +1,71 @@
|
||||
.app-settings-dialog-icon-color {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color .seafile-multicolor-icon-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color .theme-color-backdrop {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color .theme-color-backdrop .dtable-font {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color .theme-color-backdrop:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#app-settings-dialog-theme-color-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color .dtable-icon-drop-down {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
color: #b5b5b5;
|
||||
-webkit-transform: scale(0.8);
|
||||
transform: scale(0.8);
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color #app-settings-dialog-theme-color-input:hover {
|
||||
border-color: rgb(179, 179, 179);
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color .app-theme-colors-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color .app-theme-colors-content .app-theme-color-item {
|
||||
display: inline-flex;
|
||||
height: fit-content;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color .app-theme-colors-content .colorinput-color {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.app-settings-dialog-theme-color .app-theme-colors-content .colorinput-color.light {
|
||||
color: #555;
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup, Label, Tooltip, Button } from 'reactstrap';
|
||||
import IconSettingsPopover from './icon-settings-popover';
|
||||
import { gettext, mediaUrl } from '../../../../utils/constants';
|
||||
import { getIconURL } from '../../utils';
|
||||
|
||||
import './app-settings-dialog-icon-color.css';
|
||||
|
||||
class AppSettingsDialogIconColor extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isShowIconPopover: false,
|
||||
isTooltipOpen: false,
|
||||
};
|
||||
this.renameRef = React.createRef();
|
||||
}
|
||||
|
||||
onIconPopoverToggle = () => {
|
||||
this.setState({ isShowIconPopover: !this.state.isShowIconPopover });
|
||||
};
|
||||
|
||||
onRenameIconToggle = () => {
|
||||
this.setState({ isTooltipOpen: !this.state.isTooltipOpen });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { wiki_icon } = this.props.config;
|
||||
const src = wiki_icon ? getIconURL(this.props.repoId, wiki_icon) : `${mediaUrl}img/wiki/default.png`;
|
||||
return (
|
||||
<div className="app-settings-dialog-icon-color">
|
||||
<FormGroup className="app-settings-dialog-theme-color">
|
||||
<Label>{gettext('Wiki icon')}</Label>
|
||||
<div className='position-relative'>
|
||||
{wiki_icon ?
|
||||
<>
|
||||
<img src={src} width={60} height={60} alt="" />
|
||||
<div className="theme-color-backdrop" onClick={this.onIconPopoverToggle} id='app-settings-dialog-icon-backdrop'>
|
||||
<span className="iconfont icon-edit" ref={this.renameRef} style={{color: '#fff', fontSize: '24px'}}></span>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<div onClick={this.onIconPopoverToggle} id='app-settings-dialog-icon-backdrop'>
|
||||
<Button onClick={this.onIconPopoverToggle} size="sm" color="primary">{gettext('Select icon')}</Button>
|
||||
</div>
|
||||
}
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
isOpen={this.state.isTooltipOpen}
|
||||
target={this.renameRef}
|
||||
toggle={this.onRenameIconToggle}
|
||||
>
|
||||
{gettext('Change icon')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</FormGroup>
|
||||
{this.state.isShowIconPopover &&
|
||||
<IconSettingsPopover
|
||||
onToggle={this.onIconPopoverToggle}
|
||||
targetId='app-settings-dialog-icon-backdrop'
|
||||
config={this.props.config}
|
||||
updateConfig={this.props.updateConfig}
|
||||
repoId={this.props.repoId}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppSettingsDialogIconColor.propTypes = {
|
||||
config: PropTypes.object.isRequired,
|
||||
repoId: PropTypes.string.isRequired,
|
||||
updateConfig: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AppSettingsDialogIconColor;
|
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { mediaUrl } from '../../../../utils/constants';
|
||||
|
||||
const APP_ICON_CLASSNAMES = [
|
||||
'default',
|
||||
];
|
||||
|
||||
class AppSettingsDialogIcons extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
iconClass: '',
|
||||
};
|
||||
}
|
||||
|
||||
onClickIcon = (iconClass) => {
|
||||
this.setState({ iconClass }, () => {
|
||||
this.props.updateConfig({ wiki_icon: '' });
|
||||
this.props.onToggle();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="app-settings-dialog-icons">
|
||||
<div className='d-flex flex-wrap'>
|
||||
{APP_ICON_CLASSNAMES.map((name, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
this.onClickIcon(name, e);
|
||||
}}
|
||||
className={`seafile-multicolor-icon-container ${index < 5 ? 'top' : ''}`}
|
||||
>
|
||||
<img src={`${mediaUrl}img/wiki/${name}.png`} className={`${name === this.state.iconClass ? 'active' : ''}`} alt='' />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppSettingsDialogIcons.propTypes = {
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
updateConfig: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AppSettingsDialogIcons;
|
@@ -0,0 +1,71 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Label } from 'reactstrap';
|
||||
import toaster from '../../../../components/toast';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
|
||||
class AppSettingsDialogName extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
name: props.config.wiki_name || '',
|
||||
};
|
||||
}
|
||||
|
||||
onChange = (event) => {
|
||||
this.setState({ name: event.target.value });
|
||||
};
|
||||
|
||||
validateName = (name) => {
|
||||
name = name.trim();
|
||||
if (name === '') {
|
||||
return { isValid: false, message: gettext('Name is required') };
|
||||
}
|
||||
if (name.includes('/')) {
|
||||
return { isValid: false, message: gettext('Name cannot contain slash') };
|
||||
}
|
||||
if (name.includes('\\')) {
|
||||
return { isValid: false, message: gettext('Name cannot contain backslash') };
|
||||
}
|
||||
return { isValid: true, message: name };
|
||||
};
|
||||
|
||||
onCommit = () => {
|
||||
const { name } = this.state;
|
||||
const { isValid, message } = this.validateName(name);
|
||||
if (!isValid) {
|
||||
toaster.danger(message);
|
||||
return;
|
||||
} else {
|
||||
this.props.updateConfig({ wiki_name: message });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name } = this.state;
|
||||
return (
|
||||
<div className="app-setting-dialog-name">
|
||||
<Label>{gettext('Wiki name')}</Label>
|
||||
<div className="d-flex">
|
||||
<input
|
||||
className="form-control rename-area-input"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<Button className="rename-area-submit" color="primary ml-4" onClick={this.onCommit}>
|
||||
{gettext('Submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppSettingsDialogName.propTypes = {
|
||||
config: PropTypes.object.isRequired,
|
||||
updateConfig: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AppSettingsDialogName;
|
@@ -0,0 +1,64 @@
|
||||
.dtable-icon-settings-popover .popover {
|
||||
max-width: 314px;
|
||||
width: 314px;
|
||||
}
|
||||
|
||||
.app-icon-settings-popover-nav .nav {
|
||||
border-bottom: 1px solid #efefef;
|
||||
}
|
||||
|
||||
.app-icon-settings-popover-nav .nav .nav-item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-icon-settings-popover-nav .nav .nav-item .nav-link {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 0.125rem solid transparent;
|
||||
}
|
||||
|
||||
.app-icon-settings-popover-nav .nav .nav-item .nav-link.active {
|
||||
color: #ff8000;
|
||||
text-decoration: none;
|
||||
border-bottom: 0.125rem solid #ff8000;
|
||||
}
|
||||
|
||||
.app-icon-settings-popover-main {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-settings-dialog-icons {
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.app-settings-dialog-icons .seafile-multicolor-icon-container {
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app-settings-dialog-icons .seafile-multicolor-icon-container.top {
|
||||
margin: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
.app-settings-dialog-icons .seafile-multicolor-icon-container img {
|
||||
width: 40px;
|
||||
max-width: fit-content;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.app-settings-dialog-icons .seafile-multicolor-icon-container img.active {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 2px solid #b6e8e4;
|
||||
border-radius: 50%;
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Nav, NavItem, NavLink, TabContent, TabPane, PopoverBody } from 'reactstrap';
|
||||
import SeahubPopover from '../../../../components/common/seahub-popover';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import AppSettingsDialogIcons from './app-settings-dialog-icons';
|
||||
import AppSettingsDialogCustomIcon from './app-settings-dialog-custom-icon';
|
||||
|
||||
import './icon-settings-popover.css';
|
||||
|
||||
export default class IconSettingsPopover extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
targetId: PropTypes.string.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
updateConfig: PropTypes.func.isRequired,
|
||||
repoId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activeTab: 'system',
|
||||
};
|
||||
}
|
||||
|
||||
toggle = (tab) => {
|
||||
if (this.state.activeTab !== tab) {
|
||||
this.setState({ activeTab: tab });
|
||||
}
|
||||
};
|
||||
|
||||
onEnter = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.onToggle();
|
||||
};
|
||||
|
||||
renderContent = () => {
|
||||
let { activeTab } = this.state;
|
||||
return (
|
||||
<>
|
||||
<div className="app-icon-settings-popover-nav">
|
||||
<Nav className="w-100">
|
||||
<NavItem className="w-50">
|
||||
<NavLink
|
||||
className={activeTab === 'system' ? 'active w-100' : 'w-100'}
|
||||
onClick={this.toggle.bind(this, 'system')}
|
||||
>
|
||||
{gettext('System icon')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem className="w-50">
|
||||
<NavLink
|
||||
className={activeTab === 'custom' ? 'active w-100' : 'w-100'}
|
||||
onClick={this.toggle.bind(this, 'custom')}
|
||||
>
|
||||
{gettext('Custom icon')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</div>
|
||||
<div className="app-icon-settings-popover-main">
|
||||
<TabContent activeTab={activeTab}>
|
||||
<TabPane tabId='custom'>
|
||||
<AppSettingsDialogCustomIcon
|
||||
onToggle={this.props.onToggle}
|
||||
config={this.props.config}
|
||||
updateConfig={this.props.updateConfig}
|
||||
repoId={this.props.repoId}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tabId='system'>
|
||||
<AppSettingsDialogIcons
|
||||
onToggle={this.props.onToggle}
|
||||
config={this.props.config}
|
||||
updateConfig={this.props.updateConfig}
|
||||
/>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SeahubPopover
|
||||
placement='bottom-start'
|
||||
target={this.props.targetId}
|
||||
hideSeahubPopover={this.props.onToggle}
|
||||
hideSeahubPopoverWithEsc={this.props.onToggle}
|
||||
onEnter={this.onEnter}
|
||||
hideArrow={true}
|
||||
popoverClassName="dtable-icon-settings-popover"
|
||||
>
|
||||
<PopoverBody className="p-0">
|
||||
{this.renderContent()}
|
||||
</PopoverBody>
|
||||
</SeahubPopover>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody, ModalHeader, Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap';
|
||||
import AppSettingsDialogIconColor from './app-settings-dialog-icon-color';
|
||||
import AppSettingsDialogName from './app-settings-dialog-name';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
|
||||
import './app-left-bar-dialog.css';
|
||||
|
||||
class AppSettingsDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activeTab: 'Name',
|
||||
};
|
||||
}
|
||||
|
||||
toggle = (tab) => {
|
||||
if (this.state.activeTab !== tab) {
|
||||
this.setState({ activeTab: tab });
|
||||
}
|
||||
};
|
||||
|
||||
renderContent = () => {
|
||||
const { activeTab } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="app-settings-dialog-side">
|
||||
<Nav pills vertical className="w-100">
|
||||
<NavItem>
|
||||
<NavLink
|
||||
className={activeTab === 'Name' ? 'active' : ''}
|
||||
onClick={this.toggle.bind(this, 'Name')}
|
||||
>
|
||||
{gettext('Wiki name')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
className={activeTab === 'Icon' ? 'active' : ''}
|
||||
onClick={this.toggle.bind(this, 'Icon')}
|
||||
>
|
||||
{gettext('Icons')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</div>
|
||||
<div className="app-settings-dialog-main">
|
||||
<TabContent activeTab={activeTab}>
|
||||
<TabPane tabId="Icon">
|
||||
<AppSettingsDialogIconColor
|
||||
config={this.props.config}
|
||||
repoId={this.props.repoId}
|
||||
updateConfig={this.props.updateConfig}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tabId="Name">
|
||||
<AppSettingsDialogName
|
||||
config={this.props.config}
|
||||
updateConfig={this.props.updateConfig}
|
||||
/>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.props.toggle} className="app-settings-dialog">
|
||||
<ModalHeader toggle={this.props.toggle}>{gettext('Wiki settings')}</ModalHeader>
|
||||
<ModalBody className="app-settings-dialog-content">
|
||||
{this.renderContent()}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppSettingsDialog.propTypes = {
|
||||
repoId: PropTypes.string.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
updateConfig: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AppSettingsDialog;
|
53
frontend/src/pages/wiki/wiki-left-bar/wiki-left-bar-icon.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tooltip } from 'reactstrap';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
function WikiLeftBarIcon(props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const inputEl = useRef(null);
|
||||
|
||||
function onMouseEnter() {
|
||||
if (inputEl && inputEl.current) {
|
||||
inputEl.current.style.backgroundColor = '#dedede';
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
if (inputEl && inputEl.current) {
|
||||
inputEl.current.style.backgroundColor = '';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="left-bar-button"
|
||||
ref={inputEl}
|
||||
onClick={props.onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<Icon symbol={props.iconClass}/>
|
||||
</div>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
isOpen={open}
|
||||
target={inputEl}
|
||||
toggle={() => setOpen(!open)}
|
||||
hideArrow={true}
|
||||
fade={false}
|
||||
>
|
||||
{props.tipText}
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
WikiLeftBarIcon.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
iconClass: PropTypes.string.isRequired,
|
||||
tipText: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default WikiLeftBarIcon;
|
29
frontend/src/pages/wiki/wiki-left-bar/wiki-left-bar.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.seatable-app-universal-left-bar {
|
||||
flex: 0 0 4%;
|
||||
width: 50px;
|
||||
background: #f5f5f5;
|
||||
border-right: 1px solid #ddd;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.seatable-app-universal-left-bar .left-bar-button {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.seatable-app-universal-left-bar .left-bar-button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seatable-app-universal-left-bar .left-bar-button .seafile-multicolor-icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.seatable-app-universal-left-bar .left-bar-button .seafile-multicolor-icon {
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.seatable-app-universal-left-bar .left-bar-button:hover .seafile-multicolor-icon {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
52
frontend/src/pages/wiki/wiki-left-bar/wiki-left-bar.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AppSettingsDialog from './app-settings-dialog/index';
|
||||
import Icon from './wiki-left-bar-icon.jsx';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
import './wiki-left-bar.css';
|
||||
|
||||
export default class WikiLeftBar extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
config: PropTypes.object.isRequired,
|
||||
repoId: PropTypes.string.isRequired,
|
||||
updateConfig: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isShowSettingsDialog: false,
|
||||
};
|
||||
}
|
||||
|
||||
openPreviewApp = () => {
|
||||
window.open(window.location.href.replace('/edit-wiki/', '/published/'));
|
||||
};
|
||||
|
||||
openAppSettingsDialog = () => {
|
||||
this.setState({ isShowSettingsDialog: true });
|
||||
};
|
||||
|
||||
closeAppSettingsDialog = () => {
|
||||
this.setState({ isShowSettingsDialog: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="seatable-app-universal-left-bar">
|
||||
<Icon onClick={this.openAppSettingsDialog} iconClass="wiki-settings" tipText={gettext('Settings')}/>
|
||||
<Icon onClick={this.openPreviewApp} iconClass="wiki-preview" tipText={gettext('Go to wiki page to preview')}/>
|
||||
{this.state.isShowSettingsDialog &&
|
||||
<AppSettingsDialog
|
||||
toggle={this.closeAppSettingsDialog}
|
||||
config={this.props.config}
|
||||
repoId={this.props.repoId}
|
||||
updateConfig={this.props.updateConfig}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,13 +1,15 @@
|
||||
.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;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* for ff */
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.wiki-pages-heading {
|
||||
@@ -52,9 +54,9 @@ img[src=""] {
|
||||
|
||||
.wiki-side-panel {
|
||||
flex: 0 0 20%;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
overflow:hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@@ -65,8 +67,8 @@ img[src=""] {
|
||||
|
||||
.wiki-main-panel {
|
||||
flex: 1 0 80%;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@@ -120,6 +120,7 @@ export const sharedToken = window.wiki ? window.wiki.config.sharedToken : '';
|
||||
export const sharedType = window.wiki ? window.wiki.config.sharedType : '';
|
||||
export const hasIndex = window.wiki ? window.wiki.config.hasIndex : '';
|
||||
export const assetsUrl = window.wiki ? window.wiki.config.assetsUrl : '';
|
||||
export const isEditWiki = window.wiki ? window.wiki.config.isEditWiki : false;
|
||||
|
||||
// file history
|
||||
export const PER_PAGE = 25;
|
||||
|
141
frontend/src/utils/wiki-api.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import cookie from 'react-cookies';
|
||||
import axios from 'axios';
|
||||
import FormData from 'form-data';
|
||||
import { siteRoot } from './constants';
|
||||
|
||||
class WikiAPI {
|
||||
|
||||
init({ server, username, password, token }) {
|
||||
this.server = server;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.token = token; //none
|
||||
if (this.token && this.server) {
|
||||
this.req = axios.create({
|
||||
baseURL: this.server,
|
||||
headers: { 'Authorization': 'Token ' + this.token },
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
initForSeahubUsage({ siteRoot, xcsrfHeaders }) {
|
||||
if (siteRoot && siteRoot.charAt(siteRoot.length-1) === '/') {
|
||||
var server = siteRoot.substring(0, siteRoot.length-1);
|
||||
this.server = server;
|
||||
} else {
|
||||
this.server = siteRoot;
|
||||
}
|
||||
|
||||
this.req = axios.create({
|
||||
headers: {
|
||||
'X-CSRFToken': xcsrfHeaders,
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
_sendPostRequest(url, form) {
|
||||
if (form.getHeaders) {
|
||||
return this.req.post(url, form, {
|
||||
headers:form.getHeaders()
|
||||
});
|
||||
} else {
|
||||
return this.req.post(url, form);
|
||||
}
|
||||
}
|
||||
|
||||
listWikiDir(slug, dirPath, withParents) {
|
||||
const path = encodeURIComponent(dirPath);
|
||||
let url = this.server + '/api/v2.1/wikis/' + encodeURIComponent(slug) + '/dir/?p=' + path;
|
||||
if (withParents) {
|
||||
url = this.server + '/api/v2.1/wikis/' + encodeURIComponent(slug) + '/dir/?p=' + path + '&with_parents=' + withParents;
|
||||
}
|
||||
return this.req.get(url);
|
||||
}
|
||||
|
||||
|
||||
getWikiFileContent(slug, filePath) {
|
||||
const path = encodeURIComponent(filePath);
|
||||
const time = new Date().getTime();
|
||||
const url = this.server + '/api/v2.1/wikis/' + encodeURIComponent(slug) + '/content/' + '?p=' + path + '&_=' + time;
|
||||
return this.req.get(url);
|
||||
}
|
||||
|
||||
|
||||
listWikis(options) {
|
||||
/*
|
||||
* options: `{type: 'shared'}`, `{type: ['mine', 'shared', ...]}`
|
||||
*/
|
||||
let url = this.server + '/api/v2.1/wikis/';
|
||||
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('&');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addWiki(repoID) {
|
||||
const url = this.server + '/api/v2.1/wikis/';
|
||||
let form = new FormData();
|
||||
form.append('repo_id', repoID);
|
||||
return this._sendPostRequest(url, form);
|
||||
}
|
||||
|
||||
renameWiki(slug, name) {
|
||||
const url = this.server + '/api/v2.1/wikis/' + slug + '/';
|
||||
let form = new FormData();
|
||||
form.append('wiki_name', name);
|
||||
return this._sendPostRequest(url, form);
|
||||
}
|
||||
|
||||
updateWikiPermission(wikiSlug, permission) {
|
||||
const url = this.server + '/api/v2.1/wikis/' + wikiSlug + '/';
|
||||
let params = {
|
||||
permission: permission
|
||||
};
|
||||
return this.req.put(url, params);
|
||||
}
|
||||
|
||||
deleteWiki(slug) {
|
||||
const url = this.server + '/api/v2.1/wikis/' + slug + '/';
|
||||
return this.req.delete(url);
|
||||
}
|
||||
|
||||
updateWikiConfig(wikiSlug, wikiConfig) {
|
||||
const url = this.server + '/api/v2.1/wiki-config/' + wikiSlug + '/';
|
||||
let params = {
|
||||
wiki_config: wikiConfig
|
||||
};
|
||||
return this.req.put(url, params);
|
||||
}
|
||||
|
||||
getWikiConfig(wikiSlug) {
|
||||
const url = this.server + '/api/v2.1/wiki-config/' + wikiSlug + '/';
|
||||
return this.req.get(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let wikiAPI = new WikiAPI();
|
||||
let xcsrfHeaders = cookie.load('sfcsrftoken');
|
||||
wikiAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
|
||||
|
||||
export default wikiAPI;
|