1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-10-21 02:42:26 +00:00

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>
This commit is contained in:
Michael An
2024-05-06 14:24:39 +08:00
committed by GitHub
parent 1580206f7e
commit fa1157ce8a
73 changed files with 5143 additions and 62 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}
/>
);
})}

View File

@@ -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>
</>
);
}
}

View 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;
}

View 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;
}

View 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;
}

View File

@@ -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}

View File

@@ -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 &&

View 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 || [];
}
}

View 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;
}
}

View 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 || [];
}
}

View File

@@ -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>
);

View 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 };

View 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;

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
export const DRAGGED_FOLDER_MODE = 'view-folder';
export const DRAGGED_VIEW_MODE = 'view';

View File

@@ -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)
);

View 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;

View File

@@ -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')}
/>
} */}
</>
);
}
}

View File

@@ -0,0 +1,4 @@
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
export default DragDropContext(HTML5Backend);

View File

@@ -0,0 +1,3 @@
import ViewStructure from './view-structure';
export default ViewStructure;

View 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>
);
}
}

View 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);
}
}

View File

@@ -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;

View 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;

View File

@@ -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>
);
}
}

View File

@@ -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));

View File

@@ -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>
);
}
}

View File

@@ -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;

View 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)
);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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%;
}

View File

@@ -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>
);
}
}

View File

@@ -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;

View 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;

View 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);
}

View 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>
);
}
}

View File

@@ -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;
}

View File

@@ -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;

View 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;