1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-04-28 03:10:45 +00:00

Compare commits

...

222 Commits

Author SHA1 Message Date
欢乐马
388a28c00e
Merge pull request #7779 from PPazderski/master
fix typo in seahub scripts
2025-04-28 09:54:06 +08:00
Paul Pazderski
c0e252d53b fix typo in seahub scripts 2025-04-27 18:40:57 +02:00
llj
ea580c6569
[dir view] added 'star/unstar' to the context menu of items in grid mode, and to the dropdown menu in the top toolbar for the single selected item; and other improments (#7776) 2025-04-27 17:51:40 +08:00
Aries
281b469b78
aggregate tags search results (#7775)
Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-04-27 13:44:59 +08:00
Michael An
f46da268ff Revert "fix import code bug"
This reverts commit 371dd4a057.
2025-04-27 11:07:32 +08:00
Huang Junxiang
5fc9b4877c
perf: read db & redis conf from .env (#7774)
* perf: read db & redis conf from .env

* perf: remove locmeme cache

* perf: remove db conf in init
2025-04-27 10:49:11 +08:00
llj
2006ff57d6 [share admin] added a missing file 2025-04-27 10:32:32 +08:00
Michael An
371dd4a057 fix import code bug 2025-04-27 09:53:43 +08:00
r350178982
09ea1ce72d Merge branch '12.0' 2025-04-25 18:16:11 +08:00
Ranjiwei
11734fe181
update (#7753)
* update

* code optimize

* Update mail.py
2025-04-25 18:12:27 +08:00
Michael An
52f71ffae1
Merge pull request #7771 from haiwen/feature/create_tag
add create tag btn on tags tree header
2025-04-25 18:11:30 +08:00
llj
66d923ccca
Share admin links batch delete (#7766)
* [Share Admin] Share Links: added 'select links and delete them in batch'

* [Share Admin] Upload Links: added 'select links and delete them in batch'

* [Share Admin] Share / Upload Links: replaced 'Cancel' & 'Delete' buttons with 'xx Selected' button & 'delete' icon

* [Share Admin] Share / Upload Links: added background color for highlighted & selected items
2025-04-25 17:07:15 +08:00
Michael An
3488ea86b0 Merge branch '12.0' 2025-04-25 16:30:34 +08:00
Michael An
29fdd70f99
update translation (#7772) 2025-04-25 16:27:40 +08:00
小强
6549f48998 update sdoc editor version 2025-04-25 14:32:31 +08:00
Michael An
04aec222ce
Merge pull request #7770 from haiwen/remove-svg-sprite-loader
remove svg-sprite-loader to fix npm audit
2025-04-25 13:43:38 +08:00
zhouwenxuan
dda7d350be add create tag btn on tags tree header 2025-04-25 10:38:09 +08:00
Michael An
726e34d47c remove svg-sprite-loader 2025-04-25 10:08:50 +08:00
Michael An
065ecb0b1f
Merge pull request #7768 from haiwen/fix-external-link-page-do-not-show-migrate-tag-info
fix external link page do not show migrate tag info
2025-04-24 21:59:40 +08:00
Michael An
0b00f08e3f fix external link page do not show migrate tag info 2025-04-24 21:53:06 +08:00
欢乐马
b43ad4132b
seafile.sh IS_PRO_VERSION (#7765) 2025-04-24 12:15:57 +08:00
Aries
2ec7bc4da9
Optimize/search filters (#7761)
* optimize ui

* optimize filters

* update date filter

* optimize

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-04-23 16:11:03 +08:00
Michael An
75cde31370
Merge pull request #7751 from haiwen/upload-replace-fixup
[dir view] fixed 'upload & replace a file'(so that the related upload…
2025-04-23 15:51:30 +08:00
Michael An
bffdcdfa7e
Merge pull request #7764 from haiwen/fix-svg-package-npm-audit
fix npm audit
2025-04-23 15:21:42 +08:00
Michael An
f2952cb6fb fix npm audit 2025-04-23 15:18:03 +08:00
小强
f10c51c461 fix exdraw bug 2025-04-23 14:58:52 +08:00
awu0403
c6afca8eba
fix link invite group member (#7763)
* fix link invite group member

* Update group-op-menu.js

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
2025-04-23 14:20:44 +08:00
zhichaona
8e2be63d3f
Add excl draw module 4 (#7740)
* add exdraw apis

* temporarily submit

* rebase exdraw_apis

* Interacting with the exdraw-server

* update

* optimize code

* optimize code

---------

Co-authored-by: ‘JoinTyang’ <yangtong1009@163.com>
Co-authored-by: 小强 <shuntian@xiaoqiangdeMacBook-Pro.local>
2025-04-23 14:10:11 +08:00
Michael An
444e690255
Merge pull request #7754 from haiwen/dependabot/npm_and_yarn/frontend/http-proxy-middleware-2.0.9
Bump http-proxy-middleware from 2.0.7 to 2.0.9 in /frontend
2025-04-23 12:07:15 +08:00
Michael An
0cd14ab806
Merge pull request #7759 from haiwen/windows-ui
fixed windows-ui
2025-04-23 11:58:09 +08:00
Ranjiwei
1a4d7038a9
Update lib-sub-folder-set-user-permission-dialog.js (#7762) 2025-04-23 11:43:52 +08:00
llj
bfc2812a4f
[login] 2fa: redesigned 2 pages(2fa with a token; 2fa with a backup code) (#7760) 2025-04-22 21:23:11 +08:00
llj
1ba5c44839
[dir view] the top toolbar for 1 selected dirent: display 'share' icon (remove 'share' from the downdown menu), replaced the 'vertical dots' icon with a 'horizontal dots' icon; other improvements (#7758) 2025-04-22 21:22:29 +08:00
小强
45c9411973 update sdoc translate 2025-04-22 16:29:26 +08:00
zhichaona
36af8862b1 fixed windows-ui 2025-04-22 16:28:37 +08:00
小强
145f2d17db update sdoc editor version 2025-04-22 16:07:01 +08:00
小强
e68a442a3e update sdoc editor version 2025-04-22 16:06:05 +08:00
Ranjiwei
6874c2a40c
Update onlyoffice_file_view_react.html (#7757) 2025-04-22 16:00:18 +08:00
欢乐马
52e020e227
13.0.2 sql (#7756) 2025-04-22 10:49:22 +08:00
dependabot[bot]
d7e4866fc0
Bump http-proxy-middleware from 2.0.7 to 2.0.9 in /frontend
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.7 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-22 01:59:51 +00:00
Michael An
7ff4b52005
basic file support comment (#7731)
* basic file support comment

* 01 add init loading icon

* delete useless comment

* 02 delete comment tip

* update api validation

* 03 update API params

* 04 delete useless api

* 05 remove read all notification

* 06 change comment and reply permission

* 07 change docUuid to fileUuid

---------

Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
2025-04-21 21:33:13 +08:00
Aries
1cf26c3d2c
list repo tags regardless of whether metadata tag is enabled (#7752)
Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-04-21 18:25:43 +08:00
llj
84e763cddf [dir view] fixed 'upload & replace a file'(so that the related upload data can be displayed in the popover) 2025-04-21 17:32:27 +08:00
Aries
63f51d6d2a
Feature/search filters controller (#7739)
* add search filter controller

* update custom date

* optimize ui

* update bg color

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-04-21 15:50:06 +08:00
awu0403
8cc5815107
Send notification to cloud user (#6415)
* send subscription expire notification

* optimize code

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
2025-04-21 13:30:35 +08:00
Michael An
0b8aa00f4d
Merge pull request #7743 from haiwen/fix/dirent_selection
fix dirent state in grid view
2025-04-21 10:42:14 +08:00
Michael An
4c3cc1ae19 fix code format 2025-04-21 10:34:49 +08:00
zhouwenxuan
920b7fe430 fix dirent state in grid view 2025-04-21 10:27:09 +08:00
awu0403
e11dd9e34c
fix special folder judgement (#7749)
Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
2025-04-20 09:18:54 +08:00
awu0403
f24516e88a
Add deactivation option (#7626)
* sql delete share relation

* update rpc remove share

* update

* use sql remove share

* code optimize

* code optimize

* update selector

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
2025-04-20 08:54:41 +08:00
awu0403
38c6ea36ae
Optimize dir size (#7732)
* add dir size

* update

* use sql query file md

* optimize sql

* update sql

* exclude special dir

* update

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
2025-04-20 08:53:12 +08:00
lian
e20902279e set org setting is_active default True 2025-04-19 16:07:07 +08:00
JoinTyang
62f31eee77
fix update records bug (#7747) 2025-04-19 14:34:02 +08:00
lian
cb05c2390e
add org_created_callback (#7746) 2025-04-19 14:32:51 +08:00
Michael An
de09b014a0
Merge pull request #7744 from haiwen/optimize/selection_ui_on_safari
disable text selection in grid view
2025-04-19 09:43:05 +08:00
Michael An
d733bbccc8
Merge pull request #7745 from haiwen/fix/selection_state_reset_after_switch_mode
reset dirent selection state after switch mode
2025-04-19 09:38:31 +08:00
zhouwenxuan
8cb5ecf83c reset dirent selection state after switch mode 2025-04-18 17:41:03 +08:00
zhouwenxuan
b3602c6fa5 disable text selection in grid view 2025-04-18 17:32:52 +08:00
Michael An
d7c3b459d2
Merge pull request #7741 from haiwen/optimize/enable_ocr_configuration
add ENABLE_SEAFILE_OCR, false by default
2025-04-17 17:25:12 +08:00
zhouwenxuan
d988c3f0cb optimize 2025-04-17 16:54:03 +08:00
zhichaona
a524757a92
set exdraw favicon (#7742) 2025-04-17 10:31:17 +08:00
zhouwenxuan
a3df5ddd0e add ENABLE_OCR, false by default 2025-04-16 17:08:15 +08:00
JoinTyang
b398c493c2
fix tag invalid bug (#7738) 2025-04-16 11:28:47 +08:00
Michael An
759189ae78 Merge branch '12.0' 2025-04-15 15:25:11 +08:00
Michael An
ca4aa8b0cb delete error translation 2025-04-15 15:24:47 +08:00
Michael An
344cd865b2 Merge branch '12.0' 2025-04-15 14:19:50 +08:00
Michael An
e157f8675e 12.0 update translation (#7737) 2025-04-15 14:16:22 +08:00
Michael An
ef8eb9137f
12.0 update translation (#7737) 2025-04-15 14:01:02 +08:00
Aries
c23a153818
Feature/show tags in search dialog (#7727)
* show related tags

* optimize

* fix eslint warning

* change searched tags background

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
Co-authored-by: Michael An <1822852997@qq.com>
2025-04-15 13:52:37 +08:00
Michael An
31e0b24e07
12.0 department set quota (#7736)
* 01 system admin department set quota

* 02 org admin department set quota
2025-04-15 13:46:17 +08:00
小强
da86a8e1b0 Fix reg compatibility issues 2025-04-15 11:02:42 +08:00
Michael An
1ec93e6f5a
Merge pull request #7735 from haiwen/fix-file-activity-page-UI
fix activity page scroll and css format
2025-04-14 17:30:01 +08:00
Michael An
6e6f49beed fix activity page scroll and css format 2025-04-14 17:10:58 +08:00
Michael An
1f77db68e5
Merge pull request #7733 from haiwen/fix-search-in-tags-page
fix search folder in tag view
2025-04-14 16:45:30 +08:00
Michael An
9249e4da17 fix search folder in tag view 2025-04-14 15:44:55 +08:00
Michael An
7a7079ed48
Merge pull request #7715 from haiwen/fix/file_tree_update_after_moving
update tree nodes after move files
2025-04-14 14:43:04 +08:00
Michael An
39f12f1279
Merge pull request #7730 from haiwen/optimize/libcontent_view_unnecessary_render
remove redundant path reset, remove unsafe lifecycle method
2025-04-14 14:29:20 +08:00
JoinTyang
59c719e64f
Merge pull request #7705 from haiwen/face_recognition_menu
face recognition menu
2025-04-14 11:45:58 +08:00
zheng.shen
17a4f2c637 update 2025-04-14 11:40:27 +08:00
lian
19d555880b
support view/edit csv file via onlyoffice (#7725)
* support view/edit csv file via onlyoffice

* update

* update
2025-04-11 20:11:22 +08:00
zheng.shen
8d837c8195 update 2025-04-11 17:20:59 +08:00
zhouwenxuan
8a18ada09a remove redundant path reset, remove unsafe lifecycle method 2025-04-11 17:03:28 +08:00
Michael An
8ebf4e7225
Merge pull request #7709 from haiwen/fix/file_tree
fix tree data update
2025-04-11 14:16:34 +08:00
Michael An
034f1e2a04
Merge pull request #7710 from haiwen/optimize/dropdown_menu
optimize dropdown menu open direction
2025-04-11 13:45:15 +08:00
Aries
c9984a8319
fix column data setting ui (#7728)
Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-04-11 13:05:09 +08:00
小强
05f5d13c20 update sdoc version 2025-04-11 11:20:38 +08:00
awu0403
6b51e54596
System metrics (#7700)
* add metric ui

* Update statistic-metrics.js

* handle metrics

* optimize ui

* update

* optimize ui

* update ui

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
2025-04-11 11:09:25 +08:00
Michael An
1134469495
Remove deduplicated css (#7726)
* remove deduplicated css

* fix code warnings
2025-04-11 11:06:49 +08:00
zhouwenxuan
466e8f3a40 fix repeated build tree 2025-04-11 10:03:29 +08:00
zhouwenxuan
885b60f566 fix tree data update failed when switch repo by click searched folder 2025-04-11 10:03:29 +08:00
JoinTyang
272010d55f
optimize shared repos search (#7722)
* optimize shared repo search

* Update utils.py

---------

Co-authored-by: Daniel Pan <freeplant@gmail.com>
2025-04-10 17:44:23 +08:00
小强
7fb3b29e3a update sdoc editor version 2025-04-10 14:13:42 +08:00
awu0403
8e36f412da
Optimize fileter log (#7669)
* filter log v1

* optimize log user selector

* optimize ui and filter repo

* optimize ui of file update and permission

* admin filter completed

* optimize selector component

* optimize lint

* optimize code

* optimize filter

* update

* add filter group audit

* update parameters

* Update log-user-selector.js

* Update log-filter.css

* update var name

* update func name

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
2025-04-10 13:55:15 +08:00
Michael An
efb1ac8286
Merge pull request #7724 from haiwen/fix-create-view-in-folder-bug
fix create view in folder bug
2025-04-09 23:00:33 +08:00
Michael An
c3fab488f6 01 fix create view in folder bug 2025-04-09 22:50:14 +08:00
小强
b599bed239 optimize code 2025-04-09 18:20:45 +08:00
Michael An
72f7e68bdc
fix readonly repo markdown editor (#7718) 2025-04-09 17:30:08 +08:00
Aries
3ec8c646b1
Optimize/seasearch filters (#7723)
* update seasearch filters

* seasearch support filter by owner and search_filename_only

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-04-09 17:02:16 +08:00
Ranjiwei
951698555d
Merge pull request #7701 from haiwen/org-register
send activity email when org register
2025-04-09 15:43:44 +08:00
awu0403
618f75ab13
Fix real lock (#7721)
* update

* fix get dirRouter

* Update lib-content-view.js

* Update lib-content-view.js

* Update lib-content-view.js

* update name

* Update lib-content-view.js

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
2025-04-09 14:59:58 +08:00
杨顺强
2e5b2797b3
add excalidraw viewer (#7719)
Co-authored-by: 小强 <shuntian@xiaoqiangdeMacBook-Pro.local>
2025-04-09 11:16:01 +08:00
r350178982
2be71dd00b Update models.py 2025-04-08 17:05:48 +08:00
Michael An
1be01e5186
Change generate tags UI (#7712)
* 01 change header icon class

* 02 change tags UI

* 03 change modal header title
2025-04-08 16:47:35 +08:00
杨国璇
e67fc4a3d9
fix: metadata switch classname (#7717)
Co-authored-by: 杨国璇 <ygx@Hello-word.local>
2025-04-08 16:35:47 +08:00
Aries
cb69c6662e
Feature/filters in search dialog (#7681)
* search results filters

* hide filters for seasearch

* optimize

* filter by suffix

* fix dropdown menu z-index

* update ui

* optimise css

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
Co-authored-by: Michael An <1822852997@qq.com>
2025-04-08 15:46:58 +08:00
杨国璇
53bedae485
feat: metadata status callback (#7716)
* feat: metadata status callback

* feat: optimzie code

---------

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
2025-04-08 15:00:02 +08:00
Lewis
a865aecb6f
Merge pull request #7695 from haiwen/change-notice-icon
change notice close icon style
2025-04-08 14:58:07 +08:00
zhouwenxuan
ef4fbafa04 update tree nodes after move files 2025-04-08 14:32:29 +08:00
欢乐马
a1686622b3
Merge pull request #7714 from haiwen/12.0-add-forwarder-header-in-gunicorn
Update setup-seafile-mysql.py
2025-04-08 14:06:13 +08:00
llj
cf262f09db
[group] added full operation menus for all the department/group items… (#7706)
* [group] added full operation menus for all the department/group items in 'Files' page; fixed & improved all the operations

* remove useless state

* fix test lib

---------

Co-authored-by: Michael An <1822852997@qq.com>
2025-04-08 13:47:28 +08:00
r350178982
ee61bbd7c5 Update setup-seafile-mysql.py 2025-04-08 13:21:12 +08:00
Aries
b28e97970f
optimize view tree render (#7713)
Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-04-08 11:21:36 +08:00
Ranjiwei
baddc4bad7
Update single-selector.js (#7707) 2025-04-08 11:20:42 +08:00
Aries
641eb1fca5
check tags status before initialize repo tags (#7711)
Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-04-08 10:40:10 +08:00
小强
dfa86ebc45 update sdoc editor version 2025-04-07 17:28:54 +08:00
Michael An
ee09f7b0f8
Fix search tags not by RGB color (#7708)
* remove search tag by RGB color

* remove search tag by tag RGB color
2025-04-07 16:04:14 +08:00
zhouwenxuan
538bf10d18 optimize dropdown menu open direction 2025-04-07 15:52:37 +08:00
lian
04f948f7e4 send activity email when org register 2025-04-07 11:20:07 +08:00
zheng.shen
b58e48db0c update 2025-04-03 18:06:21 +08:00
zheng.shen
b27bbf6cce face recognition menu 2025-04-03 16:29:42 +08:00
Ranjiwei
09581a961a
Merge pull request #7026 from haiwen/lock-notification
Lock notification
2025-04-03 13:23:59 +08:00
Michael An
247a5b06ae change format 2025-04-03 09:28:42 +08:00
孙永强
6d32b4409b handle repo-update 2025-04-03 09:28:41 +08:00
孙永强
b27198bf02 update 2025-04-03 09:28:41 +08:00
孙永强
acd1a4a957 listen notification
optimize code

Update user-api.js

optimize code

optimize code

Update lib-content-view.js

Update lib-content-view.js

remove-userless-code

update settings

optimize cur code

add max number of reconnections
2025-04-03 09:28:36 +08:00
Michael An
56d4ebc785
migrate tags UI and delete old tags (#7699)
* basic codes

remove useless

* remove useless

* remove useless codes

* remove useless codes
2025-04-02 16:04:39 +08:00
小强
894679436c update sdoc-editor version 2025-04-02 15:59:12 +08:00
awu0403
073af84027
Add wiki freeze (#7673)
* freeze wiki page

* optimize

* optimize

* update

* update

* update

* update

* update

* Update wiki2.py

* update ui

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
2025-04-02 13:05:17 +08:00
JoinTyang
cc99ce2e90
seasearch support fulltext search (#7696) 2025-04-02 11:47:06 +08:00
杨国璇
e332873d8e
Merge pull request #7697 from haiwen/fix-metadata-status
fix: metadata status
2025-04-01 18:36:10 +08:00
杨国璇
75460d1d7c fix: metadata status 2025-04-01 17:30:16 +08:00
lian
792135a224
remove share/upload links when user is deleted (#7694) 2025-04-01 16:54:25 +08:00
Michael An
f6ede9c8a7 change notice close icon style 2025-04-01 14:40:27 +08:00
Michael An
6614fd20a6
change register page style (#7693)
* change register page style

* remove useless file
2025-04-01 12:23:39 +08:00
awu0403
efe9ecce29
remove xmind (#7688)
Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
2025-03-31 18:28:47 +08:00
Michael An
d58584e0d7
hide AI icon when SeafileAI is false (#7692)
* hide AI icon when SeafileAI is false

* fix test env
2025-03-31 18:27:31 +08:00
Ranjiwei
13098287d3
Update share_links.py (#7691) 2025-03-31 16:20:48 +08:00
Huang Junxiang
6d9d952079
revert: Python 3.8 compatibility (#7690)
* revert: Python 3.8 compatibility

* update(settings.py): default value for CACHE

* fix(settings.py): fetch redis cfg locations from env

* update requirements

* feat(gitcli::test.yml): support redis test

* feat(gitcli::test.yml): support redis test

* style(gitcli::test.yml): trailing blank line
2025-03-31 15:50:11 +08:00
Michael An
dd3f25e216
remove useless lib (#7689) 2025-03-31 15:01:19 +08:00
lian
c39c7c1f34
fix Uncontrolled data used in path expression (#7686)
* fix Uncontrolled data used in path expression

* Potential fix for code scanning alert no. 195: Uncontrolled data used in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Potential fix for code scanning alert no. 196: Uncontrolled data used in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-03-31 13:48:08 +08:00
zhichaona
700e933863
exdraw icon (#7687) 2025-03-31 11:48:13 +08:00
Huang Junxiang
71da2e685e
perf: read conf from env (#7680) 2025-03-31 10:50:25 +08:00
Ranjiwei
4ef9496557
update (#7682) 2025-03-31 09:53:39 +08:00
Michael An
2ed3b81934
Merge pull request #7684 from haiwen/fix/delete_image_failed
fix delete image failed when previewing in tag files view
2025-03-29 17:33:53 +08:00
Aries
8eb1c65ae3
fix edit detail failed in image previewer (#7685)
Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-03-29 17:27:24 +08:00
小强
da16173f35 update sdoc editor version 2025-03-29 16:07:39 +08:00
zhouwenxuan
a4835a9d7b fix delete image failed when previewing in tag files view 2025-03-29 15:57:34 +08:00
JoinTyang
beef890a47
fix file type (#7679) 2025-03-29 13:59:05 +08:00
JoinTyang
7723ef6fb7
add exdraw (#7678) 2025-03-29 13:40:23 +08:00
seafile-dev
5eb303c76b
fix Use of a broken or weak cryptographic hashing (#7671)
Co-authored-by: lian <imwhatiam123@gmail.com>
2025-03-29 13:16:01 +08:00
llj
c8026ddb6c
2fa redesign (#7677)
* ['enable 2fa' page] redesigned it

* ['2fa - backup tokens' page] redesigned it.

* ['disable 2fa' page] redesigned it
2025-03-29 13:14:29 +08:00
llj
872ae595b8
[shared dir view] display the loading icon in the content area when the users visit a folder (#7676) 2025-03-29 11:07:08 +08:00
Aries
796600eef6
collapse tag node by default (#7675)
Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-03-29 10:34:10 +08:00
小强
281a81cbd2 update sdoc translate 2025-03-28 18:01:13 +08:00
awu0403
ebe1c54153
Add group audit log 13 (#7661)
* add group audit log

* Update mysql.sql

* optimize parameters

* update

* update

* update

* optimize

* code optimize

* update

* Update operation-logs.js

* update

* Update models.py

* update

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
2025-03-28 17:51:29 +08:00
Guodong SU
29c8c12fa8
update newly created wiki page name (#7666) 2025-03-28 16:27:19 +08:00
小强
f7aaa0bff4 update sdoc editor version 2025-03-28 16:01:57 +08:00
欢乐马
f06981267d
rm CCNET_CONF_DIR (#7670) 2025-03-28 14:40:42 +08:00
JoinTyang
30036bf83f
fix invisable permission bug (#7672) 2025-03-28 14:27:15 +08:00
Aries
005ddb4dca
optimize tag editor ui (#7647)
* optimize tag editor ui

* optimize selection by up/down key

* optimize popover position

* fix searched tree nodes folding

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-03-28 11:45:17 +08:00
Aries
0abb343b4b
set fallback video thumbnail (#7668)
Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-03-27 16:26:18 +08:00
llj
0666b7a303
[system admin / users] redesigned the 'user filter' toolbar for the 'Database' tab (#7667) 2025-03-27 14:15:45 +08:00
zhichaona
3ec456fff5
time-base saving & toastTips (#7662)
* time-base saving & toastTips

* refactor hasChanged

---------

Co-authored-by: First <first@FirstdeMacBook-Pro.local>
2025-03-26 10:25:36 +08:00
Michael An
055bd575b9
change dirent detail value style (#7664) 2025-03-25 16:14:35 +08:00
Michael An
28fb4f4887
Change library setting entry permission (#7663)
* change lib setting AI entry

* change file name
2025-03-25 14:09:07 +08:00
Michael An
8d4377f85b Merge branch '12.0' 2025-03-24 17:33:12 +08:00
Michael An
18c72b9391
01 remove sort in share links page (#7660) 2025-03-24 16:50:07 +08:00
Ranjiwei
e872b4eff8
Merge pull request #7644 from haiwen/code-scan-db-api
fix "SQL query built from user-controlled sources"
2025-03-24 16:45:53 +08:00
r350178982
27c5d0294f remove-ch-characters 2025-03-24 16:31:40 +08:00
lian
5b63ef83f0 update 2025-03-24 16:31:40 +08:00
lian
db0e17b645 fix "SQL query built from user-controlled sources" 2025-03-24 16:31:40 +08:00
Michael An
2152dca689
gif image do not support rotate (#7641)
* fix image rotate file type

* fix code warning
2025-03-24 14:38:36 +08:00
lian
7b60cc38aa remove office convert test 2025-03-24 14:34:47 +08:00
lian
213927e1a7
fix Polynomial regular expression used on uncontrolled data (#7656)
* fix Polynomial regular expression used on uncontrolled data

* remove office convert code
2025-03-24 14:26:33 +08:00
zhichaona
ff7fd0f0d5
Add excl draw module 2 (#7658)
* init exceldraw module

* excalidraw demo

* i18n fixed bug

* exceldraw change to excalidraw

* lang setting

---------

Co-authored-by: 杨顺强 <978987373@qq.com>
Co-authored-by: First <first@FirstdeMacBook-Pro.local>
2025-03-24 14:11:58 +08:00
lian
ef9be3fed1 add new migrations files 2025-03-24 14:02:20 +08:00
JoinTyang
7d46c7aaa2
optimize wiki search permission (#7659) 2025-03-24 13:43:12 +08:00
小强
5e456c569a update sdoc version 2025-03-24 11:51:06 +08:00
Ranjiwei
75034bd9f1
Merge pull request #7655 from haiwen/code-scan-regex
fix Regular expression injection
2025-03-24 11:09:42 +08:00
awu0403
8151f7cf1c
fix clear text logging (#7640)
* fix clear text logging

* update

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
2025-03-24 11:01:49 +08:00
Ranjiwei
3c9394ced4
Merge pull request #7619 from haiwen/fix-Information-exposure-through-an-exception
fix Information exposure through an exception
2025-03-24 10:02:53 +08:00
Michael An
61cfae3d08
Merge pull request #7648 from haiwen/fix/path_navigation
reset state after navigation
2025-03-23 09:47:03 +08:00
Michael An
1841d799a2
Merge pull request #7650 from haiwen/feature/sort_tag_files
Feature/sort tag files
2025-03-23 09:41:50 +08:00
Michael An
b93e47b606 fix span key 2025-03-23 09:40:39 +08:00
zhouwenxuan
41a12fa90e optimize 2025-03-23 09:33:42 +08:00
zhouwenxuan
d27ea6be9f sort tag files 2025-03-23 09:33:42 +08:00
Michael An
e9c61f2bec
Merge pull request #7653 from haiwen/privacy-policy
update privacy-policy UI on login page
2025-03-21 18:22:55 +08:00
Michael An
baa144cf80 change font color 2025-03-21 18:21:56 +08:00
llj
cacef99651
[library decryption dialog] fixed the 'close' icon after the upgrade of seafile-ui.css (#7654) 2025-03-21 18:00:55 +08:00
lian
03e326d56f fix Regular expression injection 2025-03-21 17:44:56 +08:00
杨顺强
67e78e76e3 update dev script 2025-03-21 17:34:17 +08:00
lian
a998903f52 update 2025-03-21 17:05:39 +08:00
杨顺强
39aac08f3d update sdoc editor version 2025-03-21 16:29:33 +08:00
lian
0aa2d11f36 update privacy-policy UI on login page 2025-03-21 10:53:18 +08:00
欢乐马
592354b3cf
13.0 repo_metadata sql (#7652) 2025-03-20 18:36:34 +08:00
llj
db5a8b0695
[user settings] fixed 'Set WebDAV Password' dialog (#7651) 2025-03-20 17:42:28 +08:00
llj
34992f7ee7
[org admin / settings] fixed 'user default quota' setting and other 'input' setting items('org name', 'file ext white list'...) (#7649) 2025-03-20 17:12:44 +08:00
杨顺强
7365db5295 update sdoc editor version 2025-03-20 16:54:26 +08:00
zhouwenxuan
ec9e513699 reset state after navigation 2025-03-20 14:33:58 +08:00
Ranjiwei
fa79c2b3a3
Update internal_api.py (#7642)
* Update internal_api.py

* Update internal_api.py

* Update internal_api.py
2025-03-19 18:58:05 +08:00
lian
61426b04d9
fix Inclusion of functionality from an untrusted source (#7645) 2025-03-19 18:08:24 +08:00
llj
9a8b731780
[UI] fixed the color of .input-group-text's left border (#7643) 2025-03-19 16:34:53 +08:00
杨顺强
f6f39685a7 update sdoc editor version 2025-03-19 12:00:51 +08:00
欢乐马
a25bb24b05
github actions permission (#7624) 2025-03-18 18:56:30 +08:00
Aries
abf09b4593
show video thumbnail (#7639)
* show video thumbnail

* check preview permission

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
2025-03-18 17:58:43 +08:00
Aries
618e25d1ab
Feature/all tags sort (#7618)
* sort menu

* sort tag tree

* optimize

* optimize

* update codes

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
Co-authored-by: renjie-run <rj.aiyayao@gmail.com>
2025-03-18 17:40:02 +08:00
Michael An
88de887f82 update sdoc editor version 2025-03-18 15:11:55 +08:00
Michael An
4cc3250c4a
Change metadata tag language setting style (#7636)
* change metadata lang setting

* change tip text
2025-03-18 14:57:58 +08:00
Michael An
742d7eb311 remove useless socket.io 2025-03-18 11:59:37 +08:00
Michael An
34b63a771d
Merge pull request #7637 from haiwen/fix-audit
fix npm audit
2025-03-18 11:49:34 +08:00
Michael An
0dda9864d0 fix npm audit 2025-03-18 11:45:08 +08:00
Michael An
0b698bf13c
Add metadata kanban collapsed tip (#7628)
* 01 change tip color to # 666

* 02 show tips when Kanban is collapsed

* 03 fix warning
2025-03-18 10:09:28 +08:00
Ranjiwei
cba13592d0
update (#7627) 2025-03-17 14:45:35 +08:00
孙永强
3f0228e829 fix 2025-03-15 14:54:00 +08:00
Ranjiwei
d3c0a1c6ec
Update backends.py (#7605) 2025-03-14 09:40:41 +08:00
lian
bab0d71302
filter out wiki repos (#7608) 2025-03-13 18:20:54 +08:00
lian
578ede3e87
fix bug when get office feature (#7589) 2025-03-11 18:07:59 +08:00
杨顺强
634646bb36 update tldraw editor version 2025-03-11 16:25:44 +08:00
lian
d180e6e662
show library on Share Admin Folders page (#7586) 2025-03-11 16:24:01 +08:00
杨顺强
b78e6767e9 update deps 2025-03-11 16:15:14 +08:00
Ranjiwei
770cc7e049
Merge pull request #7581 from haiwen/fix-file-update-bugs
Update send_file_updates.py
2025-03-10 18:32:44 +08:00
r350178982
23e04a7431 Update send_file_updates.py 2025-03-10 18:28:43 +08:00
447 changed files with 18289 additions and 10092 deletions

View File

@ -10,6 +10,9 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest

View File

@ -6,10 +6,20 @@ on:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
REDIS_HOST: localhost
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
services:
redis:
image: redis:latest
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 3
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@ -25,6 +35,7 @@ jobs:
sudo apt-get install -y libfuse-dev cmake re2c flex sqlite3
sudo apt-get install -y libssl-dev libsasl2-dev libldap2-dev libonig-dev
sudo apt-get install -y libxml2 libxml2-dev libjwt-dev
sudo apt-get install -y libhiredis-dev
- name: clone and build
run: |
@ -39,6 +50,10 @@ jobs:
pip install -r test-requirements.txt
sudo rm -rf /usr/lib/python3/dist-packages/pytz/
- name: Set REDIS_HOST environment variable
run: |
echo "REDIS_HOST=localhost" >> $GITHUB_ENV
- name: run pytest
run: |
cd $GITHUB_WORKSPACE

View File

@ -22,8 +22,10 @@ const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
// );
// reset by custom
const HOST = '127.0.0.1';
const PORT = process.env.PORT || '3000';
const CONFIG_HOST = process.env.HOST;
const isRunInDocker = CONFIG_HOST === '0.0.0.0';
const HOST = isRunInDocker ? '127.0.0.1' : CONFIG_HOST;
const PORT = process.env.PORT || '3001';
const publicPath = process.env.PUBLIC_PATH || '/assets/bundles/';
const publicUrlOrPath = `http://${HOST}:${PORT}${publicPath}`;

View File

@ -438,12 +438,17 @@ module.exports = function (webpackEnv) {
ref: true,
},
},
{
loader: require.resolve('file-loader'),
{ loader: 'svgo-loader',
options: {
name: 'static/media/[name].[hash].[ext]',
},
},
plugins: [
'removeTitle',
'removeStyleElement',
'cleanupIDs',
'inlineStyles',
'removeXMLProcInst',
]
}
}
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
@ -596,7 +601,16 @@ module.exports = function (webpackEnv) {
test: /\.svg$/,
use: [
{
loader: 'svg-sprite-loader', options: {}
loader: require.resolve('@svgr/webpack'),
options: {
prettier: false,
svgo: false,
svgoConfig: {
plugins: [{ removeViewBox: false }],
},
titleProp: true,
ref: true,
},
},
{ loader: 'svgo-loader', options: {
plugins: [
@ -627,6 +641,12 @@ module.exports = function (webpackEnv) {
// Make sure to add the new loader(s) before the "file" loader.
],
},
{
test: /\.m?js$/,
resolve: {
fullySpecified: false
}
}
].filter(Boolean),
},
plugins: [

View File

@ -2,6 +2,7 @@ const paths = require('./paths');
const entryFiles = {
tldrawEditor: '/tldrawEditor.js',
excalidrawEditor: '/excalidraw-editor.js',
markdownEditor: '/index.js',
plainMarkdownEditor: '/pages/plain-markdown-editor/index.js',
TCAccept: '/tc-accept.js',
@ -24,6 +25,7 @@ const entryFiles = {
sharedFileViewAudio: '/shared-file-view-audio.js',
sharedFileViewDocument: '/shared-file-view-document.js',
sharedFileViewSpreadsheet: '/shared-file-view-spreadsheet.js',
sharedFileViewExdraw: '/shared-file-view-exdraw.js',
sharedFileViewSdoc: '/shared-file-view-sdoc.js',
sharedFileViewUnknown: '/shared-file-view-unknown.js',
historyTrashFileView: '/history-trash-file-view.js',

File diff suppressed because it is too large Load Diff

View File

@ -8,12 +8,13 @@
"@codemirror/view": "^6.34.1",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@excalidraw/excalidraw": "^0.18.0",
"@gatsbyjs/reach-router": "2.0.1",
"@seafile/react-image-lightbox": "4.0.2",
"@seafile/resumablejs": "1.1.16",
"@seafile/sdoc-editor": "2.0.28",
"@seafile/sdoc-editor": "2.0.54",
"@seafile/seafile-calendar": "0.0.28",
"@seafile/seafile-editor": "2.0.1",
"@seafile/seafile-editor": "2.0.2",
"@seafile/stldraw-editor": "1.0.1",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/codemirror-themes": "^4.23.5",
@ -32,6 +33,7 @@
"i18next-xhr-backend": "^3.1.2",
"is-hotkey": "0.2.0",
"MD5": "^1.3.0",
"mdast-util-gfm-autolink-literal": "2.0.0",
"object-assign": "4.1.1",
"prop-types": "^15.8.1",
"qrcode.react": "4.2.0",
@ -43,12 +45,12 @@
"react-dnd-html5-backend": "^2.6.0",
"react-dom": "18.3.1",
"react-i18next": "^10.12.2",
"react-mentions": "4.4.10",
"react-responsive": "10.0.0",
"react-select": "5.9.0",
"react-transition-group": "4.4.5",
"reactstrap": "9.2.3",
"socket.io-client": "^2.2.0",
"svg-sprite-loader": "^6.0.11",
"socket.io-client": "^4.8.1",
"svgo-loader": "^3.0.1",
"unified": "^7.0.0",
"url-parse": "^1.4.3",

View File

@ -8,6 +8,7 @@ import { Utils, isMobile } from './utils/utils';
import SystemNotification from './components/system-notification';
import EventBus from './components/common/event-bus';
import Header from './components/header';
import SystemUserNotification from './components/system-user-notification';
import SidePanel from './components/side-panel';
import ResizeBar from './components/resize-bar';
import {
@ -157,19 +158,6 @@ class App extends Component {
}
};
onGroupChanged = (groupID) => {
setTimeout(function () {
let url;
if (groupID) {
url = siteRoot + 'group/' + groupID + '/';
}
else {
url = siteRoot + 'libraries/';
}
window.location = url.toString();
}, 1);
};
tabItemClick = (tabName, groupID) => {
let pathPrefix = [];
if (groupID || this.dirViewPanels.indexOf(tabName) > -1) {
@ -291,6 +279,7 @@ class App extends Component {
return (
<React.Fragment>
<SystemNotification />
<SystemUserNotification />
<Header
isSidePanelClosed={isSidePanelClosed}
onCloseSidePanel={this.onCloseSidePanel}
@ -343,7 +332,7 @@ class App extends Component {
<InvitationsView path={siteRoot + 'invitations/'} />
<FilesActivities path={siteRoot + 'dashboard'} />
<MyFileActivities path={siteRoot + 'my-activities'} />
<GroupView path={siteRoot + 'group/:groupID'} onGroupChanged={this.onGroupChanged} />
<GroupView path={siteRoot + 'group/:groupID'} />
<LinkedDevices path={siteRoot + 'linked-devices'} />
<ShareAdminLibraries path={siteRoot + 'share-admin-libs'} />
<ShareAdminFolders path={siteRoot + 'share-admin-folders'} />

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:#999999;}
</style>
<title>filter-circled</title>
<g id="filter-circled">
<path id="形状结合" class="st0" d="M16,1c8.3,0,15,6.7,15,15s-6.7,15-15,15S1,24.3,1,16S7.7,1,16,1z M16,4C9.4,4,4,9.4,4,16
s5.4,12,12,12s12-5.4,12-12S22.6,4,16,4z M20,20c0.6,0,1,0.4,1,1s-0.4,1-1,1h-8c-0.6,0-1-0.4-1-1s0.4-1,1-1H20z M22,15
c0.6,0,1,0.4,1,1s-0.4,1-1,1H10c-0.6,0-1-0.4-1-1s0.4-1,1-1H22z M24,10c0.6,0,1,0.4,1,1s-0.4,1-1,1H8c-0.6,0-1-0.4-1-1s0.4-1,1-1
H24z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 846 B

View File

@ -21,7 +21,7 @@
.lds-ripple div {
position: absolute;
border: 4px solid #eb8205;
border: 4px solid #EC8000;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;

View File

@ -10,4 +10,14 @@ export const EVENT_BUS_TYPE = {
RESTORE_IMAGE: 'restore_image',
OPEN_MARKDOWN: 'open_markdown',
// migrate tags
OPEN_TREE_PANEL: 'open_tree_panel',
OPEN_LIBRARY_SETTINGS_TAGS: 'open_library_settings_tags',
// tags
TAG_STATUS: 'tag_status',
TAGS_DATA: 'tags_data',
SELECT_TAG: 'select_tag',
UPDATE_SELECTED_TAG: 'update_selected_tag',
};

View File

@ -19,6 +19,7 @@ const MSG_TYPE_REPO_SHARE_TO_GROUP = 'repo_share_to_group';
const MSG_TYPE_REPO_TRANSFER = 'repo_transfer';
const MSG_TYPE_FILE_UPLOADED = 'file_uploaded';
const MSG_TYPE_FOLDER_UPLOADED = 'folder_uploaded';
const MSG_TYPE_FILE_COMMENT = 'file_comment';
// const MSG_TYPE_GUEST_INVITATION_ACCEPTED = 'guest_invitation_accepted';
const MSG_TYPE_REPO_MONITOR = 'repo_monitor';
const MSG_TYPE_DELETED_FILES = 'deleted_files';
@ -38,6 +39,23 @@ class NoticeItem extends React.Component {
let noticeType = noticeItem.type;
let detail = noticeItem.detail;
if (noticeType === MSG_TYPE_FILE_COMMENT) {
let avatar_url = detail.author_avatar_url;
let author = detail.author_name;
let fileName = detail.file_name;
let fileUrl = siteRoot + 'lib/' + detail.repo_id + '/' + 'file' + detail.file_path;
// 1. handle translate
let notice = gettext('File {file_link} has a new comment form user {author}.');
// 2. handle xss(cross-site scripting)
notice = notice.replace('{file_link}', `{tagA}${fileName}{/tagA}`);
notice = notice.replace('{author}', author);
notice = Utils.HTMLescape(notice);
// 3. add jump link
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(fileUrl)}>`);
notice = notice.replace('{/tagA}', '</a>');
return { avatar_url, notice };
}
if (noticeType === MSG_TYPE_ADD_USER_TO_GROUP) {
let avatar_url = detail.group_staff_avatar_url;
let groupStaff = detail.group_staff_name;

View File

@ -27,22 +27,12 @@
font-size: 16px;
font-weight: 600;
position: relative;
overflow: hidden;
}
.notification-container .notification-header .notification-close-icon {
.notification-container .notification-header .seahub-modal-btn {
position: absolute;
right: 14px;
height: 24px;
width: 24px;
text-align: center;
cursor: pointer;
color: #000;
opacity: 0.5;
font-weight: 700;
}
.notification-container .notification-header .notification-close-icon:hover {
opacity: 0.75;
}
.notification-container .notification-body {

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Popover } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import SeahubModalCloseIcon from '../seahub-modal-close';
import './index.css';
@ -48,9 +49,9 @@ class NotificationPopover extends React.Component {
placement="bottom"
>
<div className="notification-container" ref={ref => this.notificationContainerRef = ref}>
<div className="notification-header">
<div className="notification-header modal">
{headerText}
<span className="sf3-font sf3-font-x-01 notification-close-icon" onClick={this.props.onNotificationListToggle}></span>
<SeahubModalCloseIcon toggle={this.props.onNotificationListToggle} />
</div>
<div className="notification-body">
<div className="mark-notifications">

View File

@ -0,0 +1,15 @@
import React from 'react';
import { gettext } from '../../utils/constants';
import '../../css/seahub-modal-header.css';
const SeahubModalCloseIcon = (props) => {
return (
<button type="button" className={`close seahub-modal-btn ${props.className ? props.className : ''}`} data-dismiss="modal" aria-label={gettext('Close')} onClick={props.toggle}>
<span className="seahub-modal-btn-inner">
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
</span>
</button>
);
};
export default SeahubModalCloseIcon;

View File

@ -33,7 +33,6 @@ const propTypes = {
direntList: PropTypes.array.isRequired,
repoTags: PropTypes.array.isRequired,
filePermission: PropTypes.string,
onFileTagChanged: PropTypes.func.isRequired,
onItemMove: PropTypes.func.isRequired,
loadDirentList: PropTypes.func.isRequired,
};

View File

@ -1,23 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
import { gettext, enableFileTags } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import TextTranslation from '../../utils/text-translation';
import CustomizePopover from '../customize-popover';
import ListTagPopover from '../popover/list-tag-popover';
import ViewModes from '../../components/view-modes';
import SortMenu from '../../components/sort-menu';
import MetadataViewToolBar from '../../metadata/components/view-toolbar';
import TagsTableSearcher from '../../tag/views/all-tags/tags-table/tags-searcher';
import { PRIVATE_FILE_TYPE } from '../../constants';
import { ALL_TAGS_ID } from '../../tag/constants';
import AllTagsSortSetter from '../../tag/views/all-tags/tags-table/sort-setter';
import TagFilesSortSetter from '../../tag/views/tag-files/sort-setter';
const propTypes = {
repoID: PropTypes.string.isRequired,
userPerm: PropTypes.string,
currentPath: PropTypes.string.isRequired,
updateUsedRepoTags: PropTypes.func.isRequired,
onDeleteRepoTag: PropTypes.func.isRequired,
currentMode: PropTypes.string.isRequired,
switchViewMode: PropTypes.func.isRequired,
isCustomPermission: PropTypes.bool,
@ -31,75 +26,17 @@ const propTypes = {
class DirTool extends React.Component {
constructor(props) {
super(props);
this.state = {
isRepoTagDialogOpen: false,
isDropdownMenuOpen: false,
};
}
toggleDropdownMenu = () => {
this.setState({
isDropdownMenuOpen: !this.state.isDropdownMenuOpen
});
};
hidePopover = (e) => {
if (e) {
let dom = e.target;
while (dom) {
if (typeof dom.className === 'string' && dom.className.includes('tag-color-popover')) return;
dom = dom.parentNode;
}
}
this.setState({ isRepoTagDialogOpen: false });
};
toggleCancel = () => {
this.setState({ isRepoTagDialogOpen: false });
};
getMenu = () => {
const list = [];
const { userPerm, currentPath } = this.props;
if (userPerm !== 'rw' || Utils.isMarkdownFile(currentPath)) {
return list;
}
const { TAGS } = TextTranslation;
if (enableFileTags) {
list.push(TAGS);
}
return list;
};
onMenuItemClick = (item) => {
const { key } = item;
switch (key) {
case 'Tags':
this.setState({ isRepoTagDialogOpen: !this.state.isRepoTagDialogOpen });
break;
}
};
onMenuItemKeyDown = (e, item) => {
if (e.key == 'Enter' || e.key == 'Space') {
this.onMenuItemClick(item);
}
};
onSelectSortOption = (item) => {
const [sortBy, sortOrder] = item.value.split('-');
this.props.sortItems(sortBy, sortOrder);
};
render() {
const menuItems = this.getMenu();
const { isDropdownMenuOpen } = this.state;
const { repoID, currentMode, currentPath, sortBy, sortOrder, viewId, isCustomPermission, onToggleDetail, onCloseDetail } = this.props;
const { currentMode, currentPath, sortBy, sortOrder, viewId, isCustomPermission, onToggleDetail, onCloseDetail } = this.props;
const propertiesText = TextTranslation.PROPERTIES.value;
const isFileExtended = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/');
const isTagView = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/');
const isAllTagsView = currentPath.split('/').pop() === ALL_TAGS_ID;
if (isFileExtended) {
return (
@ -117,68 +54,22 @@ class DirTool extends React.Component {
if (isTagView) {
return (
<div className="dir-tool">
<TagsTableSearcher />
{isAllTagsView && <TagsTableSearcher />}
{isAllTagsView ? <AllTagsSortSetter /> : <TagFilesSortSetter />}
</div>
);
}
return (
<React.Fragment>
<div className="dir-tool d-flex">
<ViewModes currentViewMode={currentMode} switchViewMode={this.props.switchViewMode} />
<SortMenu sortBy={sortBy} sortOrder={sortOrder} onSelectSortOption={this.onSelectSortOption} />
{(!isCustomPermission) &&
<div className="cur-view-path-btn" onClick={onToggleDetail}>
<span className="sf3-font sf3-font-info" aria-label={propertiesText} title={propertiesText}></span>
</div>
}
{menuItems.length > 0 &&
<Dropdown isOpen={isDropdownMenuOpen} toggle={this.toggleDropdownMenu}>
<DropdownToggle
tag="i"
id="cur-folder-more-op-toggle"
className='cur-view-path-btn sf3-font-more sf3-font'
data-toggle="dropdown"
title={gettext('More operations')}
aria-label={gettext('More operations')}
aria-expanded={isDropdownMenuOpen}
>
</DropdownToggle>
<DropdownMenu>
{menuItems.map((menuItem, index) => {
if (menuItem === 'Divider') {
return <DropdownItem key={index} divider />;
} else {
return (
<DropdownItem
key={index}
onClick={this.onMenuItemClick.bind(this, menuItem)}
onKeyDown={this.onMenuItemKeyDown.bind(this, menuItem)}
>{menuItem.value}
</DropdownItem>
);
}
})}
</DropdownMenu>
</Dropdown>
}
</div>
{this.state.isRepoTagDialogOpen &&
<CustomizePopover
popoverClassName="list-tag-popover"
target="cur-folder-more-op-toggle"
hidePopover={this.hidePopover}
hidePopoverWithEsc={this.hidePopover}
boundariesElement={document.body}
placement={'bottom-end'}
>
<ListTagPopover
repoID={repoID}
onListTagCancel={this.toggleCancel}
/>
</CustomizePopover>
<div className="dir-tool d-flex">
<ViewModes currentViewMode={currentMode} switchViewMode={this.props.switchViewMode} />
<SortMenu sortBy={sortBy} sortOrder={sortOrder} onSelectSortOption={this.onSelectSortOption} />
{(!isCustomPermission) &&
<div className="cur-view-path-btn" onClick={onToggleDetail}>
<span className="sf3-font sf3-font-info" aria-label={propertiesText} title={propertiesText}></span>
</div>
}
</React.Fragment>
</div>
);
}

View File

@ -89,7 +89,7 @@ Picker.propTypes = {
showHourAndMinute: PropTypes.bool.isRequired,
disabledDate: PropTypes.func.isRequired,
value: PropTypes.object,
disabled: PropTypes.func.isRequired,
disabled: PropTypes.func,
inputWidth: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired
};

View File

@ -1,126 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Button, ModalBody, ModalFooter, Input, Label } from 'reactstrap';
import { gettext } from '../../utils/constants';
import { TAG_COLORS } from '../../constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import SeahubModalHeader from '@/components/common/seahub-modal-header';
const propTypes = {
repoID: PropTypes.string.isRequired,
onRepoTagCreated: PropTypes.func,
toggleCancel: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
class CreateTagDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
tagName: '',
tagColor: TAG_COLORS[0],
newTag: {},
errorMsg: '',
};
}
inputNewName = (e) => {
this.setState({
tagName: e.target.value,
});
if (this.state.errorMsg) {
this.setState({ errorMsg: '' });
}
};
selectTagcolor = (e) => {
this.setState({
tagColor: e.target.value,
});
};
createTag = () => {
let name = this.state.tagName;
let color = this.state.tagColor;
let repoID = this.props.repoID;
seafileAPI.createRepoTag(repoID, name, color).then((res) => {
let repoTagID = res.data.repo_tag.repo_tag_id;
if (this.props.onRepoTagCreated) this.props.onRepoTagCreated(repoTagID);
this.props.toggleCancel();
}).catch((error) => {
let errMessage;
if (error.response.status === 500) {
errMessage = gettext('Internal Server Error');
} else if (error.response.status === 400) {
errMessage = gettext('Tag "{name}" already exists.');
errMessage = errMessage.replace('{name}', Utils.HTMLescape(name));
}
this.setState({ errorMsg: errMessage });
});
};
handleKeyDown = (e) => {
if (e.key === 'Enter') {
this.createTag();
}
};
render() {
let canSave = this.state.tagName.trim() ? true : false;
return (
<Fragment>
<SeahubModalHeader toggle={this.props.onClose}>
<span className="tag-dialog-back sf3-font sf3-font-arrow rotate-180 d-inline-block" onClick={this.props.toggleCancel} aria-label={gettext('Back')}></span>
{gettext('New Tag')}
</SeahubModalHeader>
<ModalBody>
<div role="form" className="tag-create">
<div className="form-group">
<Label>{gettext('Name')}</Label>
<Input
name="tag-name"
onKeyDown={this.handleKeyDown}
autoFocus={true}
value={this.state.tagName}
onChange={this.inputNewName}
/>
<div className="mt-2"><span className="error">{this.state.errorMsg}</span></div>
</div>
<div className="form-group">
<Label>{gettext('Select a color')}</Label>
<div className="d-flex justify-content-between">
{TAG_COLORS.map((item, index) => {
return (
<div key={index} className="tag-color-option" onChange={this.selectTagcolor}>
<label className="colorinput">
{index === 0 ?
<input name="color" type="radio" value={item} className="colorinput-input" defaultChecked onClick={this.selectTagcolor}></input> :
<input name="color" type="radio" value={item} className="colorinput-input" onClick={this.selectTagcolor}></input>}
<span className="colorinput-color rounded-circle d-flex align-items-center justify-content-center" style={{ backgroundColor: item }}>
<i className="sf2-icon-tick color-selected"></i>
</span>
</label>
</div>
);
})
}
</div>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggleCancel}>{gettext('Cancel')}</Button>
{canSave ?
<Button color="primary" onClick={this.createTag}>{gettext('Save')}</Button> :
<Button color="primary" disabled>{gettext('Save')}</Button>
}
</ModalFooter>
</Fragment>
);
}
}
CreateTagDialog.propTypes = propTypes;
export default CreateTagDialog;

View File

@ -9,14 +9,11 @@ import SeahubModalHeader from '@/components/common/seahub-modal-header';
class DismissGroupDialog extends React.Component {
constructor(props) {
super(props);
}
dismissGroup = () => {
let that = this;
seafileAPI.deleteGroup(this.props.groupID).then((res) => {
that.props.onGroupChanged();
const { groupID } = this.props;
seafileAPI.deleteGroup(groupID).then((res) => {
this.props.onGroupDeleted();
toaster.success(gettext('Group deleted'));
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@ -25,13 +22,13 @@ class DismissGroupDialog extends React.Component {
render() {
return (
<Modal isOpen={this.props.showDismissGroupDialog} toggle={this.props.toggleDismissGroupDialog}>
<SeahubModalHeader>{gettext('Delete Group')}</SeahubModalHeader>
<Modal isOpen={true} toggle={this.props.toggleDialog}>
<SeahubModalHeader toggle={this.props.toggleDialog}>{gettext('Delete Group')}</SeahubModalHeader>
<ModalBody>
<span>{gettext('Really want to delete this group?')}</span>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggleDismissGroupDialog}>{gettext('Cancel')}</Button>
<Button color="secondary" onClick={this.props.toggleDialog}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.dismissGroup}>{gettext('Delete')}</Button>
</ModalFooter>
</Modal>
@ -40,11 +37,9 @@ class DismissGroupDialog extends React.Component {
}
const DismissGroupDialogPropTypes = {
showDismissGroupDialog: PropTypes.bool.isRequired,
toggleDismissGroupDialog: PropTypes.func.isRequired,
loadGroup: PropTypes.func.isRequired,
groupID: PropTypes.string,
onGroupChanged: PropTypes.func.isRequired,
groupID: PropTypes.number.isRequired,
toggleDialog: PropTypes.func.isRequired,
onGroupDeleted: PropTypes.func.isRequired
};
DismissGroupDialog.propTypes = DismissGroupDialogPropTypes;

View File

@ -1,220 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap';
import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import CreateTagDialog from './create-tag-dialog';
import toaster from '../toast';
import SeahubModalHeader from '@/components/common/seahub-modal-header';
require('../../css/repo-tag.css');
const TagItemPropTypes = {
repoID: PropTypes.string.isRequired,
repoTag: PropTypes.object.isRequired,
filePath: PropTypes.string.isRequired,
fileTagList: PropTypes.array.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
};
class TagItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isTagHighlighted: false
};
}
onMouseEnter = () => {
this.setState({
isTagHighlighted: true
});
};
onMouseLeave = () => {
this.setState({
isTagHighlighted: false
});
};
getRepoTagIdList = () => {
let repoTagIdList = [];
let fileTagList = this.props.fileTagList || [];
repoTagIdList = fileTagList.map((fileTag) => fileTag.repo_tag_id);
return repoTagIdList;
};
onEditFileTag = () => {
let { repoID, repoTag, filePath } = this.props;
let repoTagIdList = this.getRepoTagIdList();
if (repoTagIdList.indexOf(repoTag.id) === -1) {
let id = repoTag.id;
seafileAPI.addFileTag(repoID, filePath, id).then(() => {
repoTagIdList = this.getRepoTagIdList();
this.props.onFileTagChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else {
let fileTag = null;
let fileTagList = this.props.fileTagList;
for (let i = 0; i < fileTagList.length; i++) {
if (fileTagList[i].repo_tag_id === repoTag.id) {
fileTag = fileTagList[i];
break;
}
}
seafileAPI.deleteFileTag(repoID, fileTag.id).then(() => {
repoTagIdList = this.getRepoTagIdList();
this.props.onFileTagChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
};
render() {
const { isTagHighlighted } = this.state;
const { repoTag } = this.props;
const repoTagIdList = this.getRepoTagIdList();
const isTagSelected = repoTagIdList.indexOf(repoTag.id) != -1;
return (
<li
className={`tag-list-item cursor-pointer px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
onClick={this.onEditFileTag}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<div className="d-flex align-items-center">
<span className="tag-color w-4 h-4 rounded-circle" style={{ backgroundColor: repoTag.color }}></span>
<span className="tag-name mx-2">{repoTag.name}</span>
</div>
{isTagSelected && <i className="sf2-icon-tick tag-selected-icon"></i>}
</li>
);
}
}
TagItem.propTypes = TagItemPropTypes;
const TagListPropTypes = {
repoID: PropTypes.string.isRequired,
repoTags: PropTypes.array.isRequired,
filePath: PropTypes.string.isRequired,
fileTagList: PropTypes.array.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
toggleCancel: PropTypes.func.isRequired,
createNewTag: PropTypes.func.isRequired,
};
class TagList extends React.Component {
render() {
const { repoTags } = this.props;
return (
<Fragment>
<SeahubModalHeader toggle={this.props.toggleCancel}>{gettext('Select Tags')}</SeahubModalHeader>
<ModalBody className="px-0">
<ul className="tag-list tag-list-container">
{repoTags.map((repoTag) => {
return (
<TagItem
key={repoTag.id}
repoTag={repoTag}
repoID={this.props.repoID}
filePath={this.props.filePath}
fileTagList={this.props.fileTagList}
onFileTagChanged={this.props.onFileTagChanged}
/>
);
})}
</ul>
<a
href="#"
className="add-tag-link px-4 py-2 d-flex align-items-center"
onClick={this.props.createNewTag}
>
<span className="sf2-icon-plus mr-2"></span>
{gettext('Create a new tag')}
</a>
</ModalBody>
<ModalFooter>
<Button onClick={this.props.toggleCancel}>{gettext('Close')}</Button>
</ModalFooter>
</Fragment>
);
}
}
TagList.propTypes = TagListPropTypes;
const propTypes = {
repoID: PropTypes.string.isRequired,
repoTags: PropTypes.array.isRequired,
filePath: PropTypes.string.isRequired,
fileTagList: PropTypes.array.isRequired,
toggleCancel: PropTypes.func.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
};
class EditFileTagDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
isCreateRepoTagShow: false,
isListRepoTagShow: true,
};
}
createNewTag = () => {
this.setState({
isCreateRepoTagShow: !this.state.isCreateRepoTagShow,
isListRepoTagShow: !this.state.isListRepoTagShow,
});
};
onRepoTagCreated = (repoTagID) => {
let { repoID, filePath } = this.props;
seafileAPI.addFileTag(repoID, filePath, repoTagID).then(() => {
this.props.onFileTagChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
render() {
return (
<Modal isOpen={true} toggle={this.props.toggleCancel} autoFocus={false}>
{this.state.isListRepoTagShow &&
<TagList
repoID={this.props.repoID}
repoTags={this.props.repoTags}
filePath={this.props.filePath}
fileTagList={this.props.fileTagList}
onFileTagChanged={this.props.onFileTagChanged}
toggleCancel={this.props.toggleCancel}
createNewTag={this.createNewTag}
/>
}
{this.state.isCreateRepoTagShow &&
<CreateTagDialog
repoID={this.props.repoID}
onClose={this.props.toggleCancel}
toggleCancel={this.createNewTag}
onRepoTagCreated={this.onRepoTagCreated}
/>
}
</Modal>
);
}
}
EditFileTagDialog.propTypes = propTypes;
export default EditFileTagDialog;

View File

@ -11,7 +11,7 @@ import { Utils } from '../../utils/utils';
import '../../css/group-invite-members-dialog.css';
const propTypes = {
groupID: PropTypes.string.isRequired,
groupID: PropTypes.number.isRequired,
toggleInviteMembersDialog: PropTypes.func.isRequired,
};

View File

@ -9,7 +9,7 @@ import SeahubModalHeader from '@/components/common/seahub-modal-header';
import Loading from '../loading';
const propTypes = {
groupID: PropTypes.string.isRequired,
groupID: PropTypes.number.isRequired,
toggleDialog: PropTypes.func.isRequired
};

View File

@ -88,7 +88,7 @@
}
.lightbox-side-panel .file-details-collapse .file-details-collapse-header .file-details-collapse-header-operation:hover,
.lightbox-side-panel .dirent-detail-item .dirent-detail-item-value:hover {
.lightbox-side-panel .dirent-detail-item .dirent-detail-item-value.editable:hover {
background-color: #666;
}

View File

@ -38,11 +38,11 @@ const ImageDialog = ({ repoID, repoInfo, enableRotate: oldEnableRotate = true, i
const nextImg = imageItems[(imageIndex + 1) % imageItemsLength];
const prevImg = imageItems[(imageIndex + imageItemsLength - 1) % imageItemsLength];
// The backend server does not support rotating HEIC images
// The backend server does not support rotating HEIC, GIF, SVG images
let enableRotate = oldEnableRotate;
const urlParts = mainImg.src.split('?')[0].split('.');
const suffix = urlParts[urlParts.length - 1];
if (suffix === 'heic') {
const suffix = urlParts[urlParts.length - 1].toLowerCase();
if (suffix === 'heic' || suffix === 'svg' || suffix === 'gif') {
enableRotate = false;
}
@ -53,9 +53,6 @@ const ImageDialog = ({ repoID, repoInfo, enableRotate: oldEnableRotate = true, i
}
const renderSidePanel = () => {
const dirent = { id, name, type: 'file' };
const path = mainImg.parentDir;
return (
<div
className="lightbox-side-panel"
@ -65,9 +62,15 @@ const ImageDialog = ({ repoID, repoInfo, enableRotate: oldEnableRotate = true, i
<div className="side-panel-controller" onClick={onToggleSidePanel}>
<Icon className="expand-button" symbol={expanded ? 'right_arrow' : 'left_arrow'} />
</div>
{expanded && (<EmbeddedFileDetails repoID={repoID} repoInfo={repoInfo} path={path} dirent={dirent} />)}
{expanded &&
<EmbeddedFileDetails
repoID={repoID}
repoInfo={repoInfo}
path={mainImg.parentDir}
dirent={{ id, name, type: 'file' }}
/>
}
</div>
);
};

View File

@ -5,7 +5,7 @@ import { gettext, siteRoot, groupImportMembersExtraMsg } from '../../utils/const
import SeahubModalHeader from '@/components/common/seahub-modal-header';
const propTypes = {
toggleImportMembersDialog: PropTypes.func.isRequired,
toggleDialog: PropTypes.func.isRequired,
importMembersInBatch: PropTypes.func.isRequired,
};
@ -19,7 +19,7 @@ class ImportMembersDialog extends React.Component {
}
toggle = () => {
this.props.toggleImportMembersDialog();
this.props.toggleDialog();
};
openFileInput = () => {
@ -49,9 +49,8 @@ class ImportMembersDialog extends React.Component {
return (
<Modal isOpen={true} toggle={this.toggle}>
<SeahubModalHeader toggle={this.toggle}>{gettext('Import members from a .xlsx file')}</SeahubModalHeader>
<ModalBody>
<p>{groupImportMembersExtraMsg}</p>
{groupImportMembersExtraMsg && <p>{groupImportMembersExtraMsg}</p>}
<p><a className="text-secondary small" href={`${siteRoot}api/v2.1/group-members-import-example/`}>{gettext('Download an example file')}</a></p>
<button className="btn btn-outline-primary" onClick={this.openFileInput}>{gettext('Upload file')}</button>
<input className="d-none" type="file" onChange={this.uploadFile} ref={this.fileInputRef} />

View File

@ -9,13 +9,10 @@ import SeahubModalHeader from '@/components/common/seahub-modal-header';
class LeaveGroupDialog extends React.Component {
constructor(props) {
super(props);
}
leaveGroup = () => {
seafileAPI.quitGroup(this.props.groupID, username).then((res) => {
this.props.onGroupChanged();
const { groupID } = this.props;
seafileAPI.quitGroup(groupID, username).then((res) => {
this.props.onLeavingGroup();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@ -24,13 +21,13 @@ class LeaveGroupDialog extends React.Component {
render() {
return (
<Modal isOpen={true} toggle={this.props.toggleLeaveGroupDialog}>
<SeahubModalHeader toggle={this.props.toggleLeaveGroupDialog}>{gettext('Leave Group')}</SeahubModalHeader>
<Modal isOpen={true} toggle={this.props.toggleDialog}>
<SeahubModalHeader toggle={this.props.toggleDialog}>{gettext('Leave Group')}</SeahubModalHeader>
<ModalBody>
<p>{gettext('Really want to leave this group?')}</p>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggleLeaveGroupDialog}>{gettext('Cancel')}</Button>
<Button color="secondary" onClick={this.props.toggleDialog}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.leaveGroup}>{gettext('Leave')}</Button>
</ModalFooter>
</Modal>
@ -39,9 +36,9 @@ class LeaveGroupDialog extends React.Component {
}
const LeaveGroupDialogPropTypes = {
toggleLeaveGroupDialog: PropTypes.func.isRequired,
groupID: PropTypes.string,
onGroupChanged: PropTypes.func.isRequired,
groupID: PropTypes.number.isRequired,
onLeavingGroup: PropTypes.func.isRequired,
toggleDialog: PropTypes.func.isRequired
};
LeaveGroupDialog.propTypes = LeaveGroupDialogPropTypes;

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Modal, ModalBody, Form } from 'reactstrap';
import { gettext, siteRoot, mediaUrl } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import SeahubModalCloseIcon from '../../components/common/seahub-modal-close';
import '../../css/lib-decrypt.css';
@ -56,7 +57,7 @@ class LibDecryptDialog extends React.Component {
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalBody>
<button type="button" className="close" onClick={this.toggle}><span aria-hidden="true">×</span></button>
<SeahubModalCloseIcon className="position-absolute top-0 end-0 m-0" toggle={this.toggle} />
<Form className="lib-decrypt-form text-center">
<img src={`${mediaUrl}img/lock.png`} alt="" aria-hidden="true" />
<p className="intro">{gettext('This library is password protected')}</p>

View File

@ -2,6 +2,7 @@ import React, { Fragment, useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap';
import { gettext, enableRepoAutoDel } from '../../utils/constants';
import { TAB } from '../../constants/repo-setting-tabs';
import LibHistorySettingPanel from './lib-settings/lib-history-setting-panel';
import LibAutoDelSettingPanel from './lib-settings/lib-old-files-auto-del-setting-panel';
import {
@ -16,14 +17,7 @@ import { useMetadataStatus } from '../../hooks';
import '../../css/lib-settings.css';
const TAB = {
HISTORY_SETTING: 'history_setting',
AUTO_DEL_SETTING: 'auto_delete_setting',
EXTENDED_PROPERTIES_SETTING: 'extended_properties_setting',
FACE_RECOGNITION_SETTING: 'face_recognition_setting',
TAGS_SETTING: 'tags_setting',
OCR_SETTING: 'ocr_setting',
};
const { enableSeafileAI, enableSeafileOCR } = window.app.config;
const propTypes = {
toggleDialog: PropTypes.func.isRequired,
@ -31,7 +25,7 @@ const propTypes = {
currentRepoInfo: PropTypes.object.isRequired
};
const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab, showMigrateTip }) => {
const [activeTab, setActiveTab] = useState(tab || TAB.HISTORY_SETTING);
const toggleTab = useCallback((tab) => {
@ -110,20 +104,22 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
{gettext('Extended properties')}
</NavLink>
</NavItem>
<NavItem
role="tab"
aria-selected={activeTab === TAB.FACE_RECOGNITION_SETTING}
aria-controls="face-recognition-setting-panel"
>
<NavLink
className={activeTab === TAB.FACE_RECOGNITION_SETTING ? 'active' : ''}
onClick={toggleTab.bind(this, TAB.FACE_RECOGNITION_SETTING)}
tabIndex="0"
onKeyDown={onTabKeyDown}
{enableSeafileAI &&
<NavItem
role="tab"
aria-selected={activeTab === TAB.FACE_RECOGNITION_SETTING}
aria-controls="face-recognition-setting-panel"
>
{gettext('Face recognition')}
</NavLink>
</NavItem>
<NavLink
className={activeTab === TAB.FACE_RECOGNITION_SETTING ? 'active' : ''}
onClick={toggleTab.bind(this, TAB.FACE_RECOGNITION_SETTING)}
tabIndex="0"
onKeyDown={onTabKeyDown}
>
{gettext('Face recognition')}
</NavLink>
</NavItem>
}
<NavItem
role="tab"
aria-selected={activeTab === TAB.TAGS_SETTING}
@ -138,20 +134,22 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
{gettext('Tags')}
</NavLink>
</NavItem>
<NavItem
role="tab"
aria-selected={activeTab === TAB.OCR_SETTING}
aria-controls="ocr-setting-panel"
>
<NavLink
className={activeTab === TAB.OCR_SETTING ? 'active' : ''}
onClick={toggleTab.bind(this, TAB.OCR_SETTING)}
tabIndex="0"
onKeyDown={onTabKeyDown}
{enableSeafileAI && enableSeafileOCR &&
<NavItem
role="tab"
aria-selected={activeTab === TAB.OCR_SETTING}
aria-controls="ocr-setting-panel"
>
{gettext('OCR')}
</NavLink>
</NavItem>
<NavLink
className={activeTab === TAB.OCR_SETTING ? 'active' : ''}
onClick={toggleTab.bind(this, TAB.OCR_SETTING)}
tabIndex="0"
onKeyDown={onTabKeyDown}
>
{gettext('OCR')}
</NavLink>
</NavItem>
}
</>
}
</Nav>
@ -203,6 +201,7 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
submit={updateEnableTags}
toggleDialog={toggleDialog}
enableMetadata={enableMetadata}
showMigrateTip={showMigrateTip}
/>
</TabPane>
)}
@ -229,5 +228,3 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
LibSettingsDialog.propTypes = propTypes;
export default LibSettingsDialog;
export { TAB };

View File

@ -112,6 +112,8 @@ class LibSubFolderSetUserPermissionDialog extends React.Component {
} else {
this.permissions = ['r', 'rw', 'cloud-edit', 'preview', 'invisible'];
}
this.userSelect = React.createRef();
}
handleUserSelectChange = (option) => {

View File

@ -8,7 +8,7 @@ import SeahubModalHeader from '@/components/common/seahub-modal-header';
import '../../css/manage-members-dialog.css';
const propTypes = {
groupID: PropTypes.string,
groupID: PropTypes.number.isRequired,
isOwner: PropTypes.bool.isRequired,
toggleManageMembersDialog: PropTypes.func,
toggleDepartmentDetailDialog: PropTypes.func,

View File

@ -9,7 +9,7 @@ import toaster from '../toast';
const propTypes = {
toggle: PropTypes.func.isRequired,
groupID: PropTypes.number.isRequired,
group: PropTypes.object.isRequired,
onSetQuota: PropTypes.func.isRequired,
};
@ -29,7 +29,7 @@ class SetGroupQuotaDialog extends React.Component {
if ((quota.length && myReg.test(quota)) || quota == -2) {
this.setState({ errMessage: '' });
let newQuota = this.state.quota == -2 ? this.state.quota : this.state.quota * 1000000;
orgAdminAPI.orgAdminSetGroupQuota(orgID, this.props.groupID, newQuota).then((res) => {
orgAdminAPI.orgAdminSetGroupQuota(orgID, this.props.group.id, newQuota).then((res) => {
this.props.toggle();
this.props.onSetQuota(res.data);
}).catch(error => {
@ -55,10 +55,15 @@ class SetGroupQuotaDialog extends React.Component {
};
render() {
const group = this.props.group;
const oldQuota = Utils.bytesToSize(group.quota);
const message = gettext('The current quota for {group_name} is {quota}').replace('{group_name}', group.name).replace('{quota}', oldQuota);
return (
<Modal isOpen={true} toggle={this.props.toggle} autoFocus={false}>
<SeahubModalHeader toggle={this.props.toggle}>{gettext('Set Quota')}</SeahubModalHeader>
<ModalBody>
<p>{message}</p>
<p>{gettext('Please enter a new quota')}</p>
<InputGroup>
<Input
onKeyDown={this.handleKeyDown}

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import { Modal, ModalBody, ModalFooter, Input, Button } from 'reactstrap';
import { Modal, ModalBody, ModalFooter, Input, Label, Button } from 'reactstrap';
import SeahubModalHeader from '@/components/common/seahub-modal-header';
import toaster from '../toast';
@ -12,7 +12,7 @@ class RenameGroupDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
newGroupName: this.props.currentGroupName,
newGroupName: this.props.groupName,
isSubmitBtnActive: false,
};
}
@ -30,48 +30,42 @@ class RenameGroupDialog extends React.Component {
});
};
renameGroup = () => {
let name = this.state.newGroupName.trim();
if (name) {
let that = this;
seafileAPI.renameGroup(this.props.groupID, name).then((res) => {
that.props.loadGroup(this.props.groupID);
that.props.onGroupChanged(res.data.id);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
this.setState({
newGroupName: '',
handleSubmit = () => {
const { groupID } = this.props;
const { newGroupName } = this.state;
seafileAPI.renameGroup(groupID, newGroupName.trim()).then((res) => {
const { name } = res.data;
this.props.onGroupNameChanged(name);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
this.props.toggleRenameGroupDialog();
this.props.toggleDialog();
};
handleKeyDown = (event) => {
if (event.keyCode === 13) {
this.renameGroup();
this.handleSubmit();
}
};
render() {
return (
<Modal isOpen={this.props.showRenameGroupDialog} toggle={this.props.toggleRenameGroupDialog}>
<SeahubModalHeader>{gettext('Rename Group')}</SeahubModalHeader>
<Modal isOpen={true} toggle={this.props.toggleDialog}>
<SeahubModalHeader toggle={this.props.toggleDialog}>{gettext('Rename Group')}</SeahubModalHeader>
<ModalBody>
<label htmlFor="newGroupName">{gettext('Rename group to')}</label>
<Label for="group-name">{gettext('Rename group to')}</Label>
<Input
type="text"
id="newGroupName"
name="new-group-name"
id="group-name"
value={this.state.newGroupName}
onChange={this.handleGroupNameChange}
onKeyDown={this.handleKeyDown}
/>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggleRenameGroupDialog}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.renameGroup} disabled={!this.state.isSubmitBtnActive}>{gettext('Submit')}</Button>
<Button color="secondary" onClick={this.props.toggleDialog}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit} disabled={!this.state.isSubmitBtnActive}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
@ -79,12 +73,10 @@ class RenameGroupDialog extends React.Component {
}
const RenameGroupDialogPropTypes = {
showRenameGroupDialog: PropTypes.bool.isRequired,
toggleRenameGroupDialog: PropTypes.func.isRequired,
loadGroup: PropTypes.func.isRequired,
groupID: PropTypes.string,
onGroupChanged: PropTypes.func.isRequired,
currentGroupName: PropTypes.string.isRequired,
toggleDialog: PropTypes.func.isRequired,
groupID: PropTypes.number,
onGroupNameChanged: PropTypes.func.isRequired,
groupName: PropTypes.string.isRequired,
};
RenameGroupDialog.propTypes = RenameGroupDialogPropTypes;

View File

@ -9,7 +9,7 @@ import SeahubModalHeader from '@/components/common/seahub-modal-header';
const propTypes = {
toggle: PropTypes.func.isRequired,
groupID: PropTypes.number.isRequired,
group: PropTypes.object.isRequired,
onSetQuota: PropTypes.func.isRequired,
};
@ -29,7 +29,7 @@ class SetGroupQuotaDialog extends React.Component {
if ((quota.length && numberReg.test(quota)) || quota == -2) {
this.setState({ errMessage: '' });
let newQuota = this.state.quota == -2 ? this.state.quota : this.state.quota * 1000000;
systemAdminAPI.sysAdminUpdateDepartmentQuota(this.props.groupID, newQuota).then((res) => {
systemAdminAPI.sysAdminUpdateDepartmentQuota(this.props.group.id, newQuota).then((res) => {
this.props.toggle();
this.props.onSetQuota(res.data);
}).catch(error => {
@ -55,10 +55,15 @@ class SetGroupQuotaDialog extends React.Component {
};
render() {
const group = this.props.group;
const oldQuota = Utils.bytesToSize(group.quota);
const message = gettext('The current quota for {group_name} is {quota}').replace('{group_name}', group.name).replace('{quota}', oldQuota);
return (
<Modal isOpen={true} toggle={this.props.toggle} autoFocus={false}>
<SeahubModalHeader toggle={this.props.toggle}>{gettext('Set Quota')}</SeahubModalHeader>
<ModalBody>
<p>{message}</p>
<p>{gettext('Please enter a new quota')}</p>
<InputGroup>
<Input
onKeyDown={this.handleKeyDown}

View File

@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalBody, ModalFooter, Form, FormGroup, Input, Label } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import SeahubModalHeader from '@/components/common/seahub-modal-header';
const propTypes = {
toggleDialog: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired
};
class SysAdminUserDeactivateDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
keepSharing: true
};
}
handleOptionChange = (e) => {
this.setState({ keepSharing: e.target.value === 'true' });
};
submit = () => {
this.props.onSubmit(this.state.keepSharing);
this.props.toggleDialog();
};
render() {
return (
<Modal isOpen={true} toggle={this.props.toggleDialog}>
<SeahubModalHeader toggle={this.props.toggleDialog}>
{gettext('Set user inactive')}
</SeahubModalHeader>
<ModalBody>
<Form>
<FormGroup tag="fieldset">
<p>{gettext('Do you want to keep the sharing relationships?')}</p>
<FormGroup check>
<Label check>
<Input
type="radio"
name="keepSharing"
value="true"
checked={this.state.keepSharing === true}
onChange={this.handleOptionChange}
className="mr-2"
/>
{gettext('Keep sharing')}
</Label>
</FormGroup>
<FormGroup check>
<Label check>
<Input
type="radio"
name="keepSharing"
value="false"
checked={this.state.keepSharing === false}
onChange={this.handleOptionChange}
className="mr-2"
/>
{gettext('Do not keep sharing')}
</Label>
</FormGroup>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggleDialog}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.submit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminUserDeactivateDialog.propTypes = propTypes;
export default SysAdminUserDeactivateDialog;

View File

@ -1,108 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Popover, PopoverBody } from 'reactstrap';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import { TAG_COLORS } from '../../constants';
import toaster from '../toast';
import '../../css/repo-tag.css';
const tagColorPropTypes = {
tag: PropTypes.object.isRequired,
repoID: PropTypes.string.isRequired
};
class TagColor extends React.Component {
constructor(props) {
super(props);
this.state = {
tagColor: this.props.tag.color,
isPopoverOpen: false
};
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.tag.color !== this.props.tag.color) {
this.setState({
tagColor: nextProps.tag.color,
});
}
}
togglePopover = () => {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen
});
};
selectTagColor = (e) => {
const newColor = e.target.value;
const { repoID, tag } = this.props;
const { id, name } = tag;
seafileAPI.updateRepoTag(repoID, id, name, newColor).then(() => {
this.setState({
tagColor: newColor,
isPopoverOpen: !this.state.isPopoverOpen
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
render() {
const { isPopoverOpen, tagColor } = this.state;
const { tag } = this.props;
const { id, color } = tag;
let colorList = [...TAG_COLORS];
// for color from previous color options
if (colorList.indexOf(color) == -1) {
colorList.unshift(color);
}
return (
<div>
<span
id={`tag-${id}-color`}
className="tag-color cursor-pointer rounded-circle d-flex align-items-center justify-content-center"
style={{ backgroundColor: tagColor }}
onClick={this.togglePopover}
>
<i className="sf3-font sf3-font-down text-white"></i>
</span>
<Popover
target={`tag-${id}-color`}
isOpen={isPopoverOpen}
placement="bottom"
toggle={this.togglePopover}
className="tag-color-popover mw-100"
>
<PopoverBody className="p-2">
<div className="d-flex justify-content-between">
{colorList.map((item, index) => {
return (
<div key={index} className="tag-color-option mx-1">
<label className="colorinput">
<input name="color" type="radio" value={item} className="colorinput-input" defaultChecked={item == tagColor} onClick={this.selectTagColor} />
<span className="colorinput-color rounded-circle d-flex align-items-center justify-content-center" style={{ backgroundColor: item }}>
<i className="sf2-icon-tick color-selected"></i>
</span>
</label>
</div>
);
})
}
</div>
</PopoverBody>
</Popover>
</div>
);
}
}
TagColor.propTypes = tagColorPropTypes;
export default TagColor;

View File

@ -1,99 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
import '../../css/repo-tag.css';
const tagNamePropTypes = {
tag: PropTypes.object.isRequired,
repoID: PropTypes.string.isRequired
};
class TagName extends React.Component {
constructor(props) {
super(props);
this.state = {
tagName: this.props.tag.name,
isEditing: false
};
this.input = React.createRef();
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.tag.name !== this.props.tag.name) {
this.setState({
tagName: nextProps.tag.name,
});
}
}
toggleMode = () => {
this.setState({
isEditing: !this.state.isEditing
}, () => {
if (this.state.isEditing) {
this.input.current.focus();
}
});
};
updateTagName = (e) => {
const newName = e.target.value;
const { repoID, tag } = this.props;
const { id, color } = tag;
seafileAPI.updateRepoTag(repoID, id, newName, color).then(() => {
this.setState({
tagName: newName
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
onInputKeyDown = (e) => {
if (e.key == 'Enter') {
this.toggleMode();
this.updateTagName(e);
}
else if (e.key == 'Escape') {
e.nativeEvent.stopImmediatePropagation();
this.toggleMode();
}
};
onInputBlur = (e) => {
this.toggleMode();
this.updateTagName(e);
};
render() {
const { isEditing, tagName } = this.state;
return (
<div className="mx-2 flex-fill d-flex">
{isEditing ?
<input
type="text"
ref={this.input}
defaultValue={tagName}
onBlur={this.onInputBlur}
onKeyDown={this.onInputKeyDown}
className="flex-fill form-control-sm form-control"
/> :
<span
onClick={this.toggleMode}
className="cursor-pointer flex-fill"
>{tagName}
</span>
}
</div>
);
}
}
TagName.propTypes = tagNamePropTypes;
export default TagName;

View File

@ -11,9 +11,9 @@ import toaster from '../toast';
import '../../css/transfer-group-dialog.css';
const propTypes = {
groupID: PropTypes.string,
toggleTransferGroupDialog: PropTypes.func.isRequired,
onGroupChanged: PropTypes.func.isRequired
groupID: PropTypes.number.isRequired,
onGroupTransfered: PropTypes.func.isRequired,
toggleDialog: PropTypes.func.isRequired
};
class TransferGroupDialog extends React.Component {
@ -21,19 +21,14 @@ class TransferGroupDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedOption: null,
errMessage: '',
selectedOption: null
};
this.options = [];
this.userSelect = React.createRef();
}
handleSelectChange = (option) => {
this.setState({
selectedOption: option,
errMessage: '',
selectedOption: option
});
this.options = [];
};
transferGroup = () => {
@ -42,19 +37,21 @@ class TransferGroupDialog extends React.Component {
if (selectedOption && selectedOption[0]) {
email = selectedOption[0].email;
}
if (email) {
seafileAPI.transferGroup(this.props.groupID, email).then((res) => {
this.props.toggleTransferGroupDialog();
toaster.success(gettext('Group has been transfered'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
this.setState({ errMessage: errMessage });
});
if (!email) {
return false;
}
seafileAPI.transferGroup(this.props.groupID, email).then((res) => {
toaster.success(gettext('Group has been transfered'));
this.props.onGroupTransfered(res.data);
this.props.toggleDialog();
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
toggle = () => {
this.props.toggleTransferGroupDialog();
this.props.toggleDialog();
};
render() {
@ -69,7 +66,6 @@ class TransferGroupDialog extends React.Component {
placeholder={gettext('Please enter 1 or more character')}
onSelectChange={this.handleSelectChange}
/>
<div className="error">{this.state.errMessage}</div>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Close')}</Button>

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
@ -6,16 +6,34 @@ import TreeSection from '../../tree-section';
import TrashDialog from '../../dialog/trash-dialog';
import LibSettingsDialog from '../../dialog/lib-settings';
import RepoHistoryDialog from '../../dialog/repo-history';
import { eventBus } from '../../common/event-bus';
import { EVENT_BUS_TYPE } from '../../common/event-bus-type';
import { TAB } from '../../../constants/repo-setting-tabs';
import './index.css';
const DirOthers = ({ userPerm, repoID, currentRepoInfo }) => {
const showSettings = currentRepoInfo.is_admin; // repo owner, department admin, shared with 'Admin' permission
let [isSettingsDialogOpen, setSettingsDialogOpen] = useState(false);
let [activeTab, setActiveTab] = useState(TAB.HISTORY_SETTING);
let [showMigrateTip, setShowMigrateTip] = useState(false);
const toggleSettingsDialog = () => {
setSettingsDialogOpen(!isSettingsDialogOpen);
};
useEffect(() => {
const unsubscribeUnselectFiles = eventBus.subscribe(EVENT_BUS_TYPE.OPEN_LIBRARY_SETTINGS_TAGS, () => {
setSettingsDialogOpen(true);
setActiveTab(TAB.TAGS_SETTING);
setShowMigrateTip(true);
});
return () => {
unsubscribeUnselectFiles();
};
});
const [showTrashDialog, setShowTrashDialog] = useState(false);
const toggleTrashDialog = () => {
setShowTrashDialog(!showTrashDialog);
@ -59,6 +77,8 @@ const DirOthers = ({ userPerm, repoID, currentRepoInfo }) => {
repoID={repoID}
currentRepoInfo={currentRepoInfo}
toggleDialog={toggleSettingsDialog}
tab={activeTab}
showMigrateTip={showMigrateTip}
/>
)}
{isRepoHistoryDialogOpen && (

View File

@ -1,12 +1,17 @@
import React, { useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../../utils/constants';
import TreeSection from '../../tree-section';
import { useMetadataStatus } from '../../../hooks';
import { TagsTreeView } from '../../../tag';
import { useTags } from '../../../tag/hooks';
import EditTagDialog from '../../../tag/components/dialog/edit-tag-dialog';
const DirTags = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
const [isShowEditTagDialog, setIsShowEditTagDialog] = useState(false);
const { enableMetadata, enableTags } = useMetadataStatus();
const { isLoading, tagsData, addTag } = useTags();
const enableMetadataManagement = useMemo(() => {
if (currentRepoInfo.encrypted) return false;
@ -14,15 +19,51 @@ const DirTags = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [window.app.pageOptions.enableMetadataManagement, currentRepoInfo]);
const { enableMetadata, enableTags } = useMetadataStatus();
const { isLoading } = useTags();
const tags = useMemo(() => {
if (!tagsData) return [];
return tagsData.rows;
}, [tagsData]);
const createTag = useCallback((tag, callback) => {
addTag(tag, callback);
}, [addTag]);
const openAddTag = useCallback(() => {
setIsShowEditTagDialog(true);
}, []);
const closeAddTag = useCallback(() => {
setIsShowEditTagDialog(false);
}, []);
const renderTreeSectionHeaderOperations = (menuProps) => {
const canAdd = userPerm === 'rw' || userPerm === 'admin';
let operations = [];
if (enableTags && canAdd) {
operations.push(
<span key="tree-section-create-operation" role="button" className="tree-section-header-operation tree-section-create-operation" onClick={openAddTag}>
<i className="sf3-font sf3-font-new"></i>
</span>
);
}
return operations;
};
if (!enableMetadataManagement) return null;
if (!enableMetadata || !enableTags) return null;
return (
<TreeSection repoID={repoID} title={gettext('Tags')} stateStorageKey="tags">
<TreeSection
repoID={repoID}
title={gettext('Tags')}
stateStorageKey="tags"
renderHeaderOperations={renderTreeSectionHeaderOperations}
>
{!isLoading && (<TagsTreeView userPerm={userPerm} repoID={repoID} currentPath={currentPath} />)}
{isShowEditTagDialog && (
<EditTagDialog tags={tags} title={gettext('New tag')} onToggle={closeAddTag} onSubmit={createTag} />
)}
</TreeSection>
);
};

View File

@ -2,11 +2,12 @@ import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import TreeSection from '../../tree-section';
import ExtensionPrompts from '../extension-prompts';
import LibSettingsDialog, { TAB } from '../../dialog/lib-settings';
import LibSettingsDialog from '../../dialog/lib-settings';
import ViewsMoreOperations from './views-more-operations';
import { MetadataTreeView, useMetadata } from '../../../metadata';
import { useMetadataStatus } from '../../../hooks';
import { gettext } from '../../../utils/constants';
import { TAB } from '../../../constants/repo-setting-tabs';
import './index.css';

View File

@ -39,10 +39,10 @@
min-height: 34px;
}
.dirent-detail-item .dirent-detail-item-value:hover {
.dirent-detail-item .dirent-detail-item-value.editable:hover {
background-color: #F5F5F5;
border-radius: 3px;
cursor: default;
cursor: pointer;
}
.dirent-detail-item .dirent-detail-item-value.editable:hover {

View File

@ -8,7 +8,9 @@ import './index.css';
const DetailItem = ({ readonly = true, field, className, children }) => {
const icon = useMemo(() => {
if (field.type === 'size') return COLUMNS_ICON_CONFIG[CellType.NUMBER];
if (field.type === 'size') {
return COLUMNS_ICON_CONFIG[CellType.NUMBER];
}
return COLUMNS_ICON_CONFIG[field.type];
}, [field]);

View File

@ -3,11 +3,8 @@ import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { v4 as uuidv4 } from 'uuid';
import Icon from '../icon';
import { gettext, enableFileTags } from '../../utils/constants';
import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import EditFileTagPopover from '../popover/edit-filetag-popover';
import FileTagList from '../file-tag-list';
import ExtraMetadataAttributesDialog from '../dialog/extra-metadata-attributes-dialog';
const propTypes = {
@ -18,7 +15,6 @@ const propTypes = {
direntType: PropTypes.string.isRequired,
direntDetail: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
fileTagList: PropTypes.array.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
};
@ -29,7 +25,6 @@ class DetailListView extends React.Component {
constructor(props) {
super(props);
this.state = {
isEditFileTagShow: false,
isShowMetadataExtraProperties: false,
};
this.tagListTitleID = `detail-list-view-tags-${uuidv4()}`;
@ -55,12 +50,6 @@ class DetailListView extends React.Component {
return Utils.joinPath(path, dirent.name);
};
onEditFileTagToggle = () => {
this.setState({
isEditFileTagShow: !this.state.isEditFileTagShow
});
};
onFileTagChanged = () => {
let direntPath = this.getDirentPath();
this.props.onFileTagChanged(this.props.dirent, direntPath);
@ -70,8 +59,8 @@ class DetailListView extends React.Component {
this.setState({ isShowMetadataExtraProperties: !this.state.isShowMetadataExtraProperties });
};
renderTags = () => {
const { direntType, direntDetail, fileTagList = [] } = this.props;
renderInfos = () => {
const { direntType, direntDetail } = this.props;
const position = this.getFileParent();
if (direntType === 'dir') {
return (
@ -106,15 +95,6 @@ class DetailListView extends React.Component {
<tr><th>{gettext('Size')}</th><td>{Utils.bytesToSize(direntDetail.size)}</td></tr>
<tr><th>{gettext('Location')}</th><td>{position}</td></tr>
<tr><th>{gettext('Last Update')}</th><td>{dayjs(direntDetail.last_modified).fromNow()}</td></tr>
<tr className="file-tag-container">
<th>{gettext('Tags')}</th>
<td>
<FileTagList fileTagList={fileTagList} />
{enableFileTags &&
<span onClick={this.onEditFileTagToggle} id={this.tagListTitleID}><Icon symbol='tag' /></span>
}
</td>
</tr>
{direntDetail.permission === 'rw' && window.app.pageOptions.enableMetadataManagement && (
<tr className="file-extra-attributes">
<th colSpan={2}>
@ -130,24 +110,11 @@ class DetailListView extends React.Component {
};
render() {
const { direntType, direntDetail, fileTagList = [] } = this.props;
const { direntType, direntDetail } = this.props;
const direntPath = this.getDirentPath();
return (
<Fragment>
{this.renderTags()}
{this.state.isEditFileTagShow &&
<EditFileTagPopover
repoID={this.props.repoID}
repoTags={this.props.repoTags}
filePath={direntPath}
fileTagList={fileTagList}
toggleCancel={this.onEditFileTagToggle}
onFileTagChanged={this.onFileTagChanged}
target={this.tagListTitleID}
isEditFileTagShow={this.state.isEditFileTagShow}
/>
}
{this.renderInfos()}
{this.state.isShowMetadataExtraProperties && (
<ExtraMetadataAttributesDialog
repoID={this.props.repoID}

View File

@ -25,7 +25,7 @@
cursor: pointer;
}
.detail-header .detail-control .detail-control-close {
.detail-header .detail-control .detail-control-icon {
font-size: 16px;
fill: #666;
}

View File

@ -15,7 +15,7 @@ const Header = ({ title, icon, iconSize = 32, onClose, children, component = {}
{children}
{onClose && (
<div className="detail-control" onClick={onClose}>
{closeIcon ? closeIcon : <Icon symbol="close" className="detail-control-close" />}
{closeIcon ? closeIcon : <Icon symbol="close" className="detail-control-icon" />}
</div>
)}
</div>

View File

@ -6,6 +6,8 @@ import { CellType } from '../../../metadata/constants';
import { gettext } from '../../../utils/constants';
import { MetadataDetails } from '../../../metadata';
import { useMetadataStatus } from '../../../hooks';
import { Utils } from '../../../utils/utils';
import { SYSTEM_FOLDERS } from '../../../constants';
const DirDetails = ({ direntDetail }) => {
const { enableMetadata, enableMetadataManagement } = useMetadataStatus();
@ -13,14 +15,36 @@ const DirDetails = ({ direntDetail }) => {
return { type: CellType.MTIME, name: gettext('Last modified time') };
}, []);
const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []);
const filesField = useMemo(() => ({ type: CellType.NUMBER, name: gettext('Files') }), []);
let file_count = direntDetail.file_count || 0;
let size = Utils.bytesToSize(direntDetail.size);
let special_folder = false;
if (direntDetail.path !== undefined) {
const path = direntDetail.path;
special_folder = SYSTEM_FOLDERS.some(prefix => path === prefix || path.startsWith(prefix + '/'));
}
return (
<>
{enableMetadataManagement && enableMetadata && (
<>
<DetailItem field={filesField} value={file_count} className="sf-metadata-property-detail-formatter">
{special_folder ?
<Formatter field={CellType.TEXT} value={'--'} /> :
<Formatter field={filesField} value={file_count} />}
</DetailItem>
<DetailItem field={sizeField} value={size} className="sf-metadata-property-detail-formatter">
{special_folder ?
<Formatter field={CellType.TEXT} value={'--'} /> :
<Formatter field={sizeField} value={size} />}
</DetailItem>
<MetadataDetails />
</>
)}
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
<Formatter field={lastModifiedTimeField} value={direntDetail.mtime} />
</DetailItem>
{enableMetadataManagement && enableMetadata && (
<MetadataDetails />
)}
</>
);
};

View File

@ -92,12 +92,17 @@ const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, isShowRepo
</DetailItem>
{isShowRepoTags && window.app.pageOptions.enableFileTags && !enableMetadata && (
<DetailItem field={tagsField} className="sf-metadata-property-detail-formatter">
<FileTag repoID={repoID} dirent={dirent} path={path} repoTags={repoTags} fileTagList={fileTagList} onFileTagChanged={onFileTagChanged} />
<FileTag
repoID={repoID}
dirent={dirent}
path={path}
repoTags={repoTags}
fileTagList={fileTagList}
onFileTagChanged={onFileTagChanged}
/>
</DetailItem>
)}
{enableMetadata && (
<MetadataDetails />
)}
{enableMetadata && <MetadataDetails />}
</>
);

View File

@ -9,10 +9,13 @@ import DirDetails from './dir-details';
import FileDetails from './file-details';
import ObjectUtils from '../../../utils/object';
import { MetadataDetailsProvider } from '../../../metadata/hooks';
import { Settings, AI } from '../../../metadata/components/metadata-details';
import AIIcon from '../../../metadata/components/metadata-details/ai-icon';
import SettingsIcon from '../../../metadata/components/metadata-details/settings-icon';
import './index.css';
const { enableSeafileAI } = window.app.config;
class DirentDetails extends React.Component {
constructor(props) {
@ -94,8 +97,8 @@ class DirentDetails extends React.Component {
>
<Detail>
<Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={this.props.onClose} >
<AI />
<Settings />
{enableSeafileAI && <AIIcon />}
<SettingsIcon />
</Header>
<Body>
{this.renderImage()}

View File

@ -8,10 +8,13 @@ import { Header, Body } from '../detail';
import FileDetails from '../dirent-details/file-details';
import { MetadataContext } from '../../../metadata';
import { MetadataDetailsProvider } from '../../../metadata/hooks';
import { AI, Settings } from '../../../metadata/components/metadata-details';
import AIIcon from '../../../metadata/components/metadata-details/ai-icon';
import SettingsIcon from '../../../metadata/components/metadata-details/settings-icon';
import './index.css';
const { enableSeafileAI } = window.app.config;
const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width = 300, className, component = {} }) => {
const { headerComponent } = component;
const [direntDetail, setDirentDetail] = useState('');
@ -21,11 +24,6 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width =
return urlParams.has('view');
}, []);
const isTag = useMemo(() => {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.has('tag');
}, []);
useEffect(() => {
const fullPath = path.split('/').pop() === dirent?.name ? path : Utils.joinPath(path, dirent?.name || '');
seafileAPI.getFileInfo(repoID, fullPath).then(res => {
@ -37,7 +35,7 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width =
}, [repoID, path, dirent]);
useEffect(() => {
if (isView || isTag) return;
if (isView) return;
let isExistContext = true;
if (!window.sfMetadataContext) {
@ -75,8 +73,8 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width =
<Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={onClose} component={headerComponent}>
{onClose && (
<>
<AI />
<Settings />
{enableSeafileAI && <AIIcon />}
<SettingsIcon />
</>
)}
</Header>

View File

@ -1,8 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import MD5 from 'MD5';
import { UncontrolledTooltip } from 'reactstrap';
import { gettext, siteRoot, mediaUrl, enableVideoThumbnail, enablePDFThumbnail } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import { imageThumbnailCenter, videoThumbnailCenter } from '../../utils/thumbnail-center';
@ -74,6 +72,23 @@ class DirentGridItem extends React.Component {
}
}
componentDidUpdate(prevProps) {
if (prevProps.dirent !== this.props.dirent) {
this.setState({ dirent: this.props.dirent });
}
const { repoID, path } = this.props;
const { dirent } = this.state;
if (this.checkGenerateThumbnail(dirent) && !this.isGeneratingThumbnail) {
this.isGeneratingThumbnail = true;
this.thumbnailCenter.createThumbnail({
repoID,
path: [path, dirent.name].join('/'),
callback: this.updateDirentThumbnail,
});
}
}
componentWillUnmount() {
if (this.clickTimeout) {
clearTimeout(this.clickTimeout);
@ -297,13 +312,7 @@ class DirentGridItem extends React.Component {
render() {
let { dirent, isGridDropTipShow } = this.state;
let { is_freezed, is_locked, lock_owner_name, file_tags, isSelected } = dirent;
let toolTipID = '';
let tagTitle = '';
if (file_tags && file_tags.length > 0) {
toolTipID = MD5(dirent.name).slice(0, 7);
tagTitle = file_tags.map(item => item.name).join(' ');
}
let { is_freezed, is_locked, lock_owner_name, isSelected } = dirent;
const showName = this.getRenderedText(dirent);
return (
<>
@ -341,21 +350,6 @@ class DirentGridItem extends React.Component {
}
</div>
<div className="grid-file-name" onDragStart={this.onGridItemDragStart} draggable={this.canDrag} >
{(dirent.type !== 'dir' && file_tags && file_tags.length > 0) && (
<>
<div id={`tag-list-title-${toolTipID}`} className="dirent-item tag-list tag-list-stacked d-inline-block align-middle">
{file_tags.map((fileTag, index) => {
let length = file_tags.length;
return (
<span className="file-tag" key={fileTag.id} style={{ zIndex: length - index, backgroundColor: fileTag.color }}></span>
);
})}
</div>
<UncontrolledTooltip target={`tag-list-title-${toolTipID}`} placement="bottom">
{tagTitle}
</UncontrolledTooltip>
</>
)}
{(!dirent.isDir() && !this.canPreview) ?
<a
className="sf-link grid-file-name-link"

View File

@ -1,6 +1,6 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { siteRoot, username, enableSeadoc, thumbnailDefaultSize, thumbnailSizeForOriginal, gettext, fileServerRoot, enableWhiteboard, useGoFileserver } from '../../utils/constants';
import { siteRoot, username, enableSeadoc, thumbnailDefaultSize, thumbnailSizeForOriginal, gettext, fileServerRoot, enableWhiteboard, useGoFileserver, enableExcalidraw } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
import URLDecorator from '../../utils/url-decorator';
@ -15,7 +15,6 @@ import MoveDirentDialog from '../dialog/move-dirent-dialog';
import CopyDirentDialog from '../dialog/copy-dirent-dialog';
import ShareDialog from '../dialog/share-dialog';
import ZipDownloadDialog from '../dialog/zip-download-dialog';
import EditFileTagDialog from '../dialog/edit-filetag-dialog';
import Rename from '../../components/dialog/rename-dirent';
import CreateFile from '../dialog/create-file-dialog';
import CreateFolder from '../dialog/create-folder-dialog';
@ -53,7 +52,6 @@ const propTypes = {
updateDirent: PropTypes.func.isRequired,
onGridItemClick: PropTypes.func,
repoTags: PropTypes.array.isRequired,
onFileTagChanged: PropTypes.func,
onAddFolder: PropTypes.func.isRequired,
showDirentDetail: PropTypes.func.isRequired,
onItemRename: PropTypes.func.isRequired,
@ -80,7 +78,6 @@ class DirentGridView extends React.Component {
isShareDialogShow: false,
isMoveDialogShow: false,
isCopyDialogShow: false,
isEditFileTagShow: false,
isZipDialogOpen: false,
isRenameDialogShow: false,
isCreateFolderDialogShow: false,
@ -355,6 +352,12 @@ class DirentGridView extends React.Component {
case 'Copy':
this.onItemCopyToggle();
break;
case 'Star':
this.onToggleStarItem();
break;
case 'Unstar':
this.onToggleStarItem();
break;
case 'Unfreeze Document':
this.onUnlockItem(currentObject);
break;
@ -376,9 +379,6 @@ class DirentGridView extends React.Component {
case 'Convert to sdoc':
this.onItemConvert(currentObject, event, 'sdoc');
break;
case 'Tags':
this.onEditFileTagToggle();
break;
case 'Permission':
this.onPermissionItem();
break;
@ -412,6 +412,9 @@ class DirentGridView extends React.Component {
case 'New Whiteboard File':
this.onCreateFileToggle('.draw');
break;
case 'New Excalidraw File':
this.onCreateFileToggle('.exdraw');
break;
case 'New SeaDoc File':
this.onCreateFileToggle('.sdoc');
break;
@ -450,18 +453,6 @@ class DirentGridView extends React.Component {
hideMenu();
};
onEditFileTagToggle = () => {
this.setState({
isEditFileTagShow: !this.state.isEditFileTagShow
});
};
onFileTagChanged = () => {
let dirent = this.state.activeDirent ? this.state.activeDirent : '';
let direntPath = Utils.joinPath(this.props.path, dirent.name);
this.props.onFileTagChanged(dirent, direntPath);
};
getDirentPath = (dirent) => {
let path = this.props.path;
return path === '/' ? path + dirent.name : path + '/' + dirent.name;
@ -522,6 +513,35 @@ class DirentGridView extends React.Component {
this.setState({ isCopyDialogShow: !this.state.isCopyDialogShow });
};
onToggleStarItem = () => {
const { activeDirent: dirent } = this.state;
const { repoID } = this.props;
const filePath = this.getDirentPath(dirent);
const itemName = dirent.name;
if (dirent.starred) {
seafileAPI.unstarItem(repoID, filePath).then(() => {
this.props.updateDirent(dirent, 'starred', false);
const msg = gettext('Successfully unstarred {name_placeholder}.')
.replace('{name_placeholder}', itemName);
toaster.success(msg);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else {
seafileAPI.starItem(repoID, filePath).then(() => {
this.props.updateDirent(dirent, 'starred', true);
const msg = gettext('Successfully starred {name_placeholder}.')
.replace('{name_placeholder}', itemName);
toaster.success(msg);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
};
onPermissionItem = () => {
this.setState({ isPermissionDialogOpen: !this.state.isPermissionDialogOpen });
};
@ -734,13 +754,15 @@ class DirentGridView extends React.Component {
if (!['admin', 'rw'].includes(this.props.userPerm)) return;
const {
NEW_FOLDER, NEW_FILE,
NEW_FOLDER,
NEW_FILE,
NEW_MARKDOWN_FILE,
NEW_EXCEL_FILE,
NEW_POWERPOINT_FILE,
NEW_WORD_FILE,
NEW_SEADOC_FILE,
NEW_TLDRAW_FILE
NEW_TLDRAW_FILE,
NEW_EXCALIDRAW_FILE
} = TextTranslation;
let direntsContainerMenuList = [
@ -763,6 +785,10 @@ class DirentGridView extends React.Component {
direntsContainerMenuList.push(NEW_TLDRAW_FILE);
}
if (enableExcalidraw) {
direntsContainerMenuList.push(NEW_EXCALIDRAW_FILE);
}
if (selectedDirentList.length === 0) {
if (!hasCustomPermission('create')) return;
this.handleContextClick(event, DIRENT_GRID_CONTAINER_MENU_ID, direntsContainerMenuList);
@ -995,16 +1021,6 @@ class DirentGridView extends React.Component {
onAddFolder={this.props.onAddFolder}
/>
}
{this.state.isEditFileTagShow &&
<EditFileTagDialog
repoID={this.props.repoID}
fileTagList={dirent.file_tags}
filePath={direntPath}
toggleCancel={this.onEditFileTagToggle}
repoTags={this.props.repoTags}
onFileTagChanged={this.onFileTagChanged}
/>
}
{this.state.isShareDialogShow &&
<ModalPortal>
<ShareDialog

View File

@ -1,8 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import MediaQuery from 'react-responsive';
import { v4 as uuidv4 } from 'uuid';
import dayjs from 'dayjs';
import { DropdownItem } from 'reactstrap';
import { gettext, siteRoot, mediaUrl, username, useGoFileserver, fileServerRoot, enableVideoThumbnail, enablePDFThumbnail } from '../../utils/constants';
@ -17,13 +15,10 @@ import MoveDirentDialog from '../dialog/move-dirent-dialog';
import CopyDirentDialog from '../dialog/copy-dirent-dialog';
import ShareDialog from '../dialog/share-dialog';
import ZipDownloadDialog from '../dialog/zip-download-dialog';
import EditFileTagDialog from '../dialog/edit-filetag-dialog';
import EditFileTagPopover from '../popover/edit-filetag-popover';
import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-dialog';
import FileAccessLog from '../dialog/file-access-log';
import toaster from '../toast';
import MobileItemMenu from '../../components/mobile-item-menu';
import FileTag from './file-tag';
import '../../css/dirent-list-item.css';
@ -95,10 +90,8 @@ class DirentListItem extends React.Component {
isShowTagTooltip: false,
isDragTipShow: false,
isDropTipshow: false,
isEditFileTagShow: false,
isPermissionDialogOpen: false
};
this.tagListTitleID = `tag-list-title-${uuidv4()}`;
this.isGeneratingThumbnail = false;
this.thumbnailCenter = null;
this.dragIconRef = null;
@ -241,21 +234,28 @@ class DirentListItem extends React.Component {
this.props.onItemSelected(this.state.dirent, event);
};
onItemStarred = (e) => {
let dirent = this.state.dirent;
let repoID = this.props.repoID;
let filePath = this.getDirentPath(dirent);
onItemStarred = () => {
const { dirent } = this.state;
const { repoID } = this.props;
const filePath = this.getDirentPath(dirent);
const itemName = dirent.name;
if (dirent.starred) {
seafileAPI.unstarItem(repoID, filePath).then(() => {
this.props.updateDirent(this.state.dirent, 'starred', false);
this.props.updateDirent(dirent, 'starred', false);
const msg = gettext('Successfully unstarred {name_placeholder}.')
.replace('{name_placeholder}', itemName);
toaster.success(msg);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else {
seafileAPI.starItem(repoID, filePath).then(() => {
this.props.updateDirent(this.state.dirent, 'starred', true);
this.props.updateDirent(dirent, 'starred', true);
const msg = gettext('Successfully starred {name_placeholder}.')
.replace('{name_placeholder}', itemName);
toaster.success(msg);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@ -350,9 +350,6 @@ class DirentListItem extends React.Component {
case 'Copy':
this.onItemCopyToggle();
break;
case 'Tags':
this.onEditFileTagToggle();
break;
case 'Permission':
this.onPermissionItem();
break;
@ -410,12 +407,6 @@ class DirentListItem extends React.Component {
this.props.onItemConvert(this.state.dirent, dstType);
};
onEditFileTagToggle = () => {
this.setState({
isEditFileTagShow: !this.state.isEditFileTagShow
});
};
onFileTagChanged = () => {
let direntPath = this.getDirentPath(this.state.dirent);
this.props.onFileTagChanged(this.state.dirent, direntPath);
@ -891,18 +882,6 @@ class DirentListItem extends React.Component {
)}
</td>
<td className="tag-list-title">
{(dirent.type !== 'dir' && dirent.file_tags && dirent.file_tags.length > 0) && (
<div id={this.tagListTitleID} className="dirent-item tag-list tag-list-stacked">
{dirent.file_tags.map((fileTag, index) => {
return (
<FileTag fileTag={fileTag} length={dirent.file_tags.length} key={index} index={index}/>
);
})}
</div>
)}
{(dirent.type !== 'dir' && (!dirent.file_tags || dirent.file_tags.length == 0)) &&
<div id={this.tagListTitleID} className="dirent-item tag-list tag-list-stacked"></div>
}
</td>
<td className="operation">{this.renderItemOperation()}</td>
<td className="file-size">{dirent.size || ''}</td>
@ -993,32 +972,6 @@ class DirentListItem extends React.Component {
/>
</ModalPortal>
}
<MediaQuery query="(min-width: 768px)">
{this.state.isEditFileTagShow &&
<EditFileTagPopover
repoID={this.props.repoID}
repoTags={this.props.repoTags}
fileTagList={dirent.file_tags}
filePath={direntPath}
toggleCancel={this.onEditFileTagToggle}
onFileTagChanged={this.onFileTagChanged}
target={this.tagListTitleID}
isEditFileTagShow={this.state.isEditFileTagShow}
/>
}
</MediaQuery>
<MediaQuery query="(max-width: 767.8px)">
{this.state.isEditFileTagShow &&
<EditFileTagDialog
repoID={this.props.repoID}
repoTags={this.props.repoTags}
fileTagList={dirent.file_tags}
filePath={direntPath}
toggleCancel={this.onEditFileTagToggle}
onFileTagChanged={this.onFileTagChanged}
/>
}
</MediaQuery>
{this.state.isZipDialogOpen &&
<ModalPortal>
<ZipDownloadDialog

View File

@ -1,7 +1,7 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, thumbnailDefaultSize, fileServerRoot, enableWhiteboard, useGoFileserver } from '../../utils/constants';
import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, thumbnailDefaultSize, fileServerRoot, enableWhiteboard, useGoFileserver, enableExcalidraw } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import TextTranslation from '../../utils/text-translation';
import URLDecorator from '../../utils/url-decorator';
@ -431,7 +431,8 @@ class DirentListView extends React.Component {
NEW_POWERPOINT_FILE,
NEW_WORD_FILE,
NEW_SEADOC_FILE,
NEW_TLDRAW_FILE
NEW_TLDRAW_FILE,
NEW_EXCALIDRAW_FILE,
} = TextTranslation;
const direntsContainerMenuList = [
@ -452,6 +453,10 @@ class DirentListView extends React.Component {
direntsContainerMenuList.push(NEW_TLDRAW_FILE);
}
if (enableExcalidraw) {
direntsContainerMenuList.push(NEW_EXCALIDRAW_FILE);
}
if (this.props.selectedDirentList.length === 0) {
let id = 'dirent-container-menu';
@ -529,6 +534,9 @@ class DirentListView extends React.Component {
case 'New Whiteboard File':
this.onCreateFileToggle('.draw');
break;
case 'New Excalidraw File':
this.onCreateFileToggle('.exdraw');
break;
case 'New SeaDoc File':
this.onCreateFileToggle('.sdoc');
break;

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { enableSeadoc, gettext, enableWhiteboard } from '../../utils/constants';
import { enableSeadoc, gettext, enableWhiteboard, enableExcalidraw } from '../../utils/constants';
import Loading from '../loading';
import ModalPortal from '../modal-portal';
import CreateFile from '../../components/dialog/create-file-dialog';
@ -74,7 +74,8 @@ class DirentNoneView extends React.Component {
NEW_POWERPOINT_FILE,
NEW_WORD_FILE,
NEW_SEADOC_FILE,
NEW_TLDRAW_FILE
NEW_TLDRAW_FILE,
NEW_EXCALIDRAW_FILE
} = TextTranslation;
const direntsContainerMenuList = [
NEW_FOLDER, NEW_FILE, 'Divider',
@ -92,6 +93,9 @@ class DirentNoneView extends React.Component {
if (enableWhiteboard) {
direntsContainerMenuList.push(NEW_TLDRAW_FILE);
}
if (enableExcalidraw) {
direntsContainerMenuList.push(NEW_EXCALIDRAW_FILE);
}
let id = 'dirent-container-menu';
if (isCustomPermission) {
const { create: canCreate } = customPermission.permission;

View File

@ -34,6 +34,7 @@ class ItemDropdownMenu extends React.Component {
currentItem: ''
};
this.dropdownRef = React.createRef();
this.mainMenuDirection = 'down';
this.subMenuDirection = 'right';
}
@ -47,6 +48,24 @@ class ItemDropdownMenu extends React.Component {
});
setTimeout(() => {
if (this.dropdownRef.current) {
const rect = this.dropdownRef.current.getBoundingClientRect();
const windowHeight = window.innerHeight;
const spaceBelow = windowHeight - rect.bottom;
const spaceAbove = rect.top;
const menuHeightThreshold = 400;
let mainMenuDirection;
if (spaceBelow < menuHeightThreshold && spaceAbove < menuHeightThreshold) {
const spaceRight = window.innerWidth - rect.right;
mainMenuDirection = spaceRight >= 200 ? 'right' : 'left';
} else if (spaceBelow < menuHeightThreshold) {
mainMenuDirection = 'up';
} else {
mainMenuDirection = 'down';
}
this.mainMenuDirection = mainMenuDirection;
this.subMenuDirection = (window.innerWidth - this.dropdownRef.current.getBoundingClientRect().right < 400) ? 'left' : 'right';
}
}, 1);
@ -196,7 +215,12 @@ class ItemDropdownMenu extends React.Component {
}
return (
<Dropdown isOpen={this.state.isItemMenuShow} toggle={this.onDropdownToggleClick} className="vam">
<Dropdown
isOpen={this.state.isItemMenuShow}
toggle={this.onDropdownToggleClick}
className="vam"
direction={this.mainMenuDirection}
>
<DropdownToggle
tag={tagName || 'i'}
role="button"

View File

@ -11,7 +11,6 @@ const {
thumbnailSizeForOriginal,
previousImage, nextImage, rawPath,
lastModificationTime,
xmindImageSrc // for xmind file
} = window.app.pageOptions;
let previousImageUrl;
@ -62,9 +61,6 @@ class FileContent extends React.Component {
thumbnailURL = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${Utils.encodePath(filePath)}?mtime=${lastModificationTime}`;
}
// for xmind file
const xmindSrc = xmindImageSrc ? `${siteRoot}${xmindImageSrc}` : '';
const { scale, angle } = this.props;
let style = {};
if (scale && angle != undefined) {
@ -84,7 +80,7 @@ class FileContent extends React.Component {
{nextImage && (
<a href={nextImageUrl} id="img-next" title={gettext('you can also press →')}><span className="sf3-font sf3-font-down rotate-270 d-inline-block"></span></a>
)}
<img src={xmindSrc || thumbnailURL || rawPath} alt={fileName} id="image-view" onError={this.handleLoadFailure} style={ style } />
<img src={thumbnailURL || rawPath} alt={fileName} id="image-view" onError={this.handleLoadFailure} style={ style } />
</div>
);
}

View File

@ -630,16 +630,21 @@ class FileUploader extends React.Component {
};
replaceRepetitionFile = () => {
let resumableFile = this.resumable.files[this.resumable.files.length - 1];
let { repoID, path } = this.props;
seafileAPI.getUpdateLink(repoID, path).then(res => {
this.resumable.opts.target = res.data;
let resumableFile = this.resumable.files[this.resumable.files.length - 1];
resumableFile.formData['replace'] = 1;
resumableFile.formData['target_file'] = resumableFile.formData.parent_dir + resumableFile.fileName;
this.setState({ isUploadRemindDialogShow: false });
this.setUploadFileList(this.resumable.files);
this.resumable.upload();
this.setState({
isUploadRemindDialogShow: false,
isUploadProgressDialogShow: true,
uploadFileList: [...this.state.uploadFileList, resumableFile]
}, () => {
this.resumable.upload();
});
Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', true);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);

View File

@ -0,0 +1,213 @@
import React from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
import CommentList from './comment-widget/comment-list';
import ReplyList from './comment-widget/reply-list';
import '../../css/comments-list.css';
const { username, repoID, filePath, fileUuid } = window.app.pageOptions;
const CommentPanelPropTypes = {
toggleCommentPanel: PropTypes.func.isRequired,
participants: PropTypes.array,
onParticipantsChange: PropTypes.func,
};
class CommentPanel extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
commentsList: [],
showResolvedComment: true,
participants: null,
relatedUsers: null,
currentComment: null,
};
this.toBeAddedParticipant = [];
}
listComments = () => {
seafileAPI.listComments(repoID, fileUuid).then((res) => {
this.setState({
commentsList: res.data.comments,
isLoading: false,
});
if (this.state.currentComment) {
let newCurrentComment = res.data.comments.find(comment => comment.id === this.state.currentComment.id);
if (newCurrentComment) {
this.setState({
currentComment: newCurrentComment
});
}
}
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
listRepoRelatedUsers = () => {
seafileAPI.listRepoRelatedUsers(repoID).then((res) => {
let users = res.data.user_list.map((item) => {
return { id: item.email, display: item.name, avatar_url: item.avatar_url, contact_email: item.contact_email };
});
this.setState({ relatedUsers: users });
});
};
handleCommentChange = (event) => {
this.setState({ comment: event.target.value });
};
addComment = (comment) => {
seafileAPI.postComment(repoID, fileUuid, comment).then(() => {
this.listComments();
}).catch(err => {
toaster.danger(Utils.getErrorMsg(err));
});
};
addReply = (reply) => {
const replyData = {
author: username,
reply,
type: 'reply',
updated_at: dayjs().format('YYYY-MM-DD HH:mm:ss')
};
seafileAPI.insertReply(repoID, fileUuid, this.state.currentComment.id, replyData).then(() => {
this.listComments();
}).catch(err => {
toaster.danger(Utils.getErrorMsg(err));
});
};
resolveComment = (comment, resolveState = 'true') => {
seafileAPI.updateComment(repoID, fileUuid, comment.id, resolveState, null, null).then(() => {
this.listComments();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
deleteComment = (comment) => {
seafileAPI.deleteComment(repoID, fileUuid, comment.id).then(() => {
this.clearCurrentComment();
this.listComments();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
deleteReply = (commentId, replyId) => {
seafileAPI.deleteReply(repoID, fileUuid, commentId, replyId).then(() => {
this.listComments();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
updateReply = (commentId, replyId, reply) => {
const replyData = {
author: username,
reply,
type: 'reply',
updated_at: dayjs().format('YYYY-MM-DD HH:mm:ss')
};
seafileAPI.updateReply(repoID, fileUuid, commentId, replyId, replyData).then(() => {
this.listComments();
}).catch(err => {
toaster.danger(Utils.getErrorMsg(err));
});
};
editComment = (comment, newComment) => {
seafileAPI.updateComment(repoID, fileUuid, comment.id, null, null, newComment).then((res) => {
this.listComments();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
onParticipantsChange = () => {
if (this.props.onParticipantsChange) {
this.props.onParticipantsChange();
} else {
this.getParticipants();
}
};
getParticipants = () => {
if (this.props.participants) {
this.setState({ participants: this.props.participants });
} else {
seafileAPI.listFileParticipants(repoID, filePath).then((res) => {
this.setState({ participants: res.data.participant_list });
});
}
};
componentDidMount() {
this.listComments();
this.getParticipants();
this.listRepoRelatedUsers();
}
onClickComment = (currentComment) => {
this.setState({ currentComment });
};
clearCurrentComment = () => {
this.setState({ currentComment: null });
};
render() {
const { commentsList } = this.state;
return (
<div className="seafile-comment">
{
this.state.currentComment ?
<ReplyList
currentComment={this.state.currentComment}
clearCurrentComment={this.clearCurrentComment}
toggleCommentList={this.props.toggleCommentPanel}
commentsList={commentsList}
relatedUsers={this.state.relatedUsers}
participants={this.state.participants}
deleteComment={this.deleteComment}
resolveComment={this.resolveComment}
editComment={this.editComment}
addReply={this.addReply}
deleteReply={this.deleteReply}
updateReply={this.updateReply}
onParticipantsChange={this.onParticipantsChange}
/>
:
<CommentList
onClickComment={this.onClickComment}
commentsList={commentsList}
relatedUsers={this.state.relatedUsers}
participants={this.state.participants}
addComment={this.addComment}
toggleCommentList={this.props.toggleCommentPanel}
onParticipantsChange={this.onParticipantsChange}
isLoading={this.state.isLoading}
/>
}
</div>
);
}
}
CommentPanel.propTypes = CommentPanelPropTypes;
export default CommentPanel;

View File

@ -0,0 +1,28 @@
.comments-panel-body__header {
display: flex;
flex-direction: column;
padding: 0 16px;
}
.comments-panel-body__header .comments-types-count {
height: 38px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
}
.comments-panel-body__header .comment-type {
color: #212529;
font-size: 12px;
}
.comments-panel-body__header .comment-type {
color: #212529;
font-size: 12px;
}
.comments-panel-body__header .comment-count-tip {
color: #999;
font-size: 12px;
}

View File

@ -0,0 +1,59 @@
import React, { useState } from 'react';
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import './comment-body-header.css';
const t = gettext;
const CommentBodyHeader = ({ commentList = [], commentType, setCommentType }) => {
const [isDropdownOpen, setDropdownOpen] = useState(false);
let commentTip = null;
if (commentList.length === 1) {
commentTip = gettext('Total {comments_count} comment');
commentTip = commentTip.replace('{comments_count}', commentList.length);
}
if (commentList.length > 1) {
commentTip = gettext('Total {comments_count} comments');
commentTip = commentTip.replace('{comments_count}', commentList.length);
}
const getText = (type) => {
switch (type) {
case 'All comments':
return gettext('All comments');
case 'Resolved comments':
return gettext('Resolved comments');
case 'Unresolved comments':
return gettext('Unresolved comments');
default:
return gettext('All comments');
}
};
return (
<div className='comments-panel-body__header'>
<div className="comments-types-count">
<div id="comment-types" className='comment-type'>
<Dropdown isOpen={isDropdownOpen} toggle={() => setDropdownOpen(!isDropdownOpen)}>
<DropdownToggle tag={'div'} caret className='d-flex align-items-center justify-content-center'>
<div id={'comment-type-controller'}>{getText(commentType)}</div>
</DropdownToggle>
<DropdownMenu className='sdoc-dropdown-menu sdoc-comment-filter-dropdown' container="comment-types">
<DropdownItem className='sdoc-dropdown-menu-item' tag={'div'} onClick={(e) => setCommentType(e, 'All comments')}>
{t('All comments')}
</DropdownItem>
<DropdownItem className='sdoc-dropdown-menu-item' tag={'div'} onClick={(e) => setCommentType(e, 'Resolved comments')}>{t('Resolved comments')}</DropdownItem>
<DropdownItem className='sdoc-dropdown-menu-item' tag={'div'} onClick={(e) => setCommentType(e, 'Unresolved comments')}>{t('Unresolved comments')}</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
<div className='comment-count-tip'>{commentTip}</div>
</div>
</div>
);
};
export default CommentBodyHeader;

View File

@ -0,0 +1,9 @@
.comment-delete-popover .comment-delete-popover-container {
padding: 16px;
}
.comment-delete-popover .comment-delete-popover-container .delete-control {
display: flex;
justify-content: flex-end;
width: 100%;
}

View File

@ -0,0 +1,71 @@
import React, { useCallback, useRef, useEffect } from 'react';
import isHotkey from 'is-hotkey';
import { Button, UncontrolledPopover } from 'reactstrap';
import { getEventClassName } from '@/utils/dom';
import { gettext } from '../../../utils/constants';
import './comment-delete-popover.css';
const CommentDeletePopover = ({ type, setIsShowDeletePopover, deleteConfirm, targetId, parentDom = document.body }) => {
const popoverRef = useRef(null);
const hide = useCallback((event) => {
if (popoverRef.current && !getEventClassName(event).includes('popover') && !popoverRef.current.contains(event.target)) {
setIsShowDeletePopover(false);
event.preventDefault();
event.stopPropagation();
return false;
}
}, [setIsShowDeletePopover]);
const onHotKey = useCallback((event) => {
if (isHotkey('esc', event)) {
event.preventDefault();
setIsShowDeletePopover(false);
}
}, [setIsShowDeletePopover]);
useEffect(() => {
document.addEventListener('click', hide, true);
document.addEventListener('keydown', onHotKey);
return () => {
document.removeEventListener('click', hide, true);
document.removeEventListener('keydown', onHotKey);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onDeleteCancel = useCallback((event) => {
event.stopPropagation();
setIsShowDeletePopover(false);
}, [setIsShowDeletePopover]);
const handleConfirm = useCallback((event) => {
event.stopPropagation();
deleteConfirm();
}, [deleteConfirm]);
return (
<UncontrolledPopover
container={parentDom}
target={targetId}
onClick={event => event.stopPropagation()}
placement="left"
className='comment-delete-popover'
isOpen={true}
>
<div className='comment-delete-popover-container' ref={popoverRef}>
<div className='delete-tip'>
{type === 'comment' ? gettext('Are you sure to delete this comment?') : gettext('Are you sure to delete this reply?')}
</div>
<div className='delete-control mt-5'>
<Button color='secondary' size='sm' className='mr-2' onClick={onDeleteCancel}>{gettext('Cancel')}</Button>
<Button color='primary' size='sm' onClick={handleConfirm}>{gettext('Confirm')}</Button>
</div>
</div>
</UncontrolledPopover>
);
};
export default CommentDeletePopover;

View File

@ -0,0 +1,99 @@
import React from 'react';
import PropTypes from 'prop-types';
import { processor } from '@seafile/seafile-editor';
const commentItemPropTypes = {
time: PropTypes.string,
item: PropTypes.object,
showResolvedComment: PropTypes.bool,
onClickComment: PropTypes.func,
};
class CommentItemReadOnly extends React.Component {
constructor(props) {
super(props);
this.state = {
html: '',
newComment: this.props.item.comment,
};
}
convertComment = (mdFile) => {
processor.process(mdFile).then((result) => {
let html = String(result);
this.setState({ html: html });
});
};
handleCommentChange = (event) => {
this.setState({
newComment: event.target.value,
});
};
componentWillMount() {
this.convertComment(this.props.item.comment);
}
componentWillReceiveProps(nextProps) {
this.convertComment(nextProps.item.comment);
}
onCommentContentClick = (e) => {
// click participant link, page shouldn't jump
if (e.target.nodeName !== 'A') return;
const preNode = e.target.previousSibling;
if (preNode && preNode.nodeType === 3 && preNode.nodeValue.slice(-1) === '@') {
e.preventDefault();
}
};
render() {
const item = this.props.item;
const replies = item.replies || [];
const lastReply = replies[replies.length - 1];
return (
<li className={'seafile-comment-item'} id={item.id} onClick={() => this.props.onClickComment(item)}>
<div className="seafile-comment-info">
<img className="avatar mt-1" src={item.avatar_url} alt=""/>
<div className="comment-author-info">
<div className="comment-author-name ellipsis">{item.user_name}</div>
<div className="comment-author-time">
{this.props.time}
{item.resolved &&
<span className="comment-success-resolved sdocfont sdoc-mark-as-resolved"></span>
}
</div>
</div>
</div>
<div
className="seafile-comment-content"
dangerouslySetInnerHTML={{ __html: this.state.html }}
onClick={e => this.onCommentContentClick(e)}
>
</div>
{replies.length > 0 &&
<div className="comment-footer">
<span className="comments-count">
<i className="sdocfont sdoc-comments"></i>
<span className="comments-count-number">{replies.length}</span>
</span>
<div className="comment-author">
<span className="comment-author__avatar">
<img alt="" src={lastReply.avatar_url}/>
</span>
<div className="comment-author__latest-reply">
<p>{lastReply.reply}</p>
</div>
</div>
</div>
}
</li>
);
}
}
CommentItemReadOnly.propTypes = commentItemPropTypes;
export default CommentItemReadOnly;

View File

@ -0,0 +1,210 @@
import React from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import { processor } from '@seafile/seafile-editor';
import { Button, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import CommentDeletePopover from './comment-delete-popover';
const commentItemPropTypes = {
time: PropTypes.string,
item: PropTypes.object,
deleteComment: PropTypes.func,
showResolvedComment: PropTypes.bool,
editComment: PropTypes.func,
};
const { username } = window.app.pageOptions;
class CommentItem extends React.Component {
constructor(props) {
super(props);
this.state = {
dropdownOpen: false,
html: '',
newComment: this.props.item.comment,
editable: false,
isShowDeletePopover: false,
};
}
toggleDropDownMenu = () => {
this.setState({
dropdownOpen: !this.state.dropdownOpen,
});
};
convertComment = (mdFile) => {
processor.process(mdFile).then((result) => {
let html = String(result);
this.setState({ html: html });
});
};
toggleEditComment = () => {
this.setState({
editable: !this.state.editable
});
};
updateComment = (event) => {
const newComment = this.state.newComment.trim();
if (this.props.item.comment !== newComment) {
this.props.editComment(this.props.item, newComment);
}
this.toggleEditComment();
};
handleCommentChange = (event) => {
this.setState({
newComment: event.target.value,
});
};
onCommentContentClick = (e) => {
// click participant link, page shouldn't jump
if (e.target.nodeName !== 'A') return;
const preNode = e.target.previousSibling;
if (preNode && preNode.nodeType === 3 && preNode.nodeValue.slice(-1) === '@') {
e.preventDefault();
}
};
componentWillMount() {
this.convertComment(this.props.item.comment);
}
componentWillReceiveProps(nextProps) {
this.convertComment(nextProps.item.comment);
}
onCommentClick = (e) => {
// click participant link, page shouldn't jump
if (e.target.nodeName !== 'A') return;
const preNode = e.target.previousSibling;
if (preNode && preNode.nodeType === 3 && preNode.nodeValue.slice(-1) === '@') {
e.preventDefault();
}
};
toggleShowDeletePopover = () => {
this.setState({
isShowDeletePopover: !this.state.isShowDeletePopover
});
};
render() {
const item = this.props.item;
let oldTime = (new Date(item.created_at)).getTime();
let time = dayjs(oldTime).format('YYYY-MM-DD HH:mm');
const commentOpToolsId = `commentOpTools_${item?.id}`;
if (this.state.editable) {
return (
<li className="seafile-comment-item" id={item.id}>
<div className="seafile-comment-info">
<img className="avatar mt-1" src={item.avatar_url} alt=""/>
<div className="comment-author-info">
<div className="comment-author-name ellipsis">{item.user_name}</div>
<div className="comment-author-time">{time}</div>
</div>
</div>
<div className="seafile-edit-comment">
<textarea className="edit-comment-input" value={this.state.newComment} onChange={this.handleCommentChange} clos="100" rows="3" warp="virtual"></textarea>
<Button className="comment-btn" color="primary" size="sm" onClick={this.updateComment} id={item.id}>{gettext('Update')}</Button>{' '}
<Button className="comment-btn" color="secondary" size="sm" onClick={this.toggleEditComment}>{gettext('Cancel')}</Button>
</div>
</li>
);
}
return (
<li className={'seafile-comment-item'} id={item.id}>
<div className="seafile-comment-info">
<img className="avatar mt-1" src={item.avatar_url} alt=""/>
<div className="comment-author-info">
<div className="comment-author-name ellipsis">{item.user_name}</div>
<div className="comment-author-time">
{time}
{item.resolved &&
<span className="comment-success-resolved sdocfont sdoc-mark-as-resolved"></span>
}
</div>
</div>
{(item.user_email === username) &&
<Dropdown
isOpen={this.state.dropdownOpen}
size="sm"
className="seafile-comment-dropdown"
toggle={this.toggleDropDownMenu}
id={commentOpToolsId}
>
<DropdownToggle
tag="i"
role="button"
tabIndex="0"
className="seafile-comment-dropdown-btn sf-dropdown-toggle sf3-font-more sf3-font"
title={gettext('More operations')}
aria-label={gettext('More operations')}
data-toggle="dropdown"
aria-expanded={this.state.dropdownOpen}
aria-haspopup={true}
/>
<DropdownMenu>
<DropdownItem
onClick={this.toggleShowDeletePopover}
className="delete-comment"
id={item.id}
>
{gettext('Delete')}
</DropdownItem>
<DropdownItem
onClick={this.toggleEditComment}
className="edit-comment"
id={item.id}
>
{gettext('Edit')}
</DropdownItem>
{!item.resolved &&
<DropdownItem
onClick={() => this.props.resolveComment(this.props.item, 'true')}
className="resolve-comment"
id={item.id}
>
{gettext('Mark as resolved')}
</DropdownItem>
}
{item.resolved &&
<DropdownItem
onClick={() => this.props.resolveComment(this.props.item, 'false')}
className="resolve-comment"
id={item.id}
>
{gettext('Resubmit')}
</DropdownItem>
}
</DropdownMenu>
</Dropdown>
}
</div>
<div
className="seafile-comment-content"
dangerouslySetInnerHTML={{ __html: this.state.html }}
onClick={e => this.onCommentContentClick(e)}
>
</div>
{this.state.isShowDeletePopover && (
<CommentDeletePopover
type="comment"
targetId={commentOpToolsId}
deleteConfirm={() => this.props.deleteComment(this.props.item)}
setIsShowDeletePopover={this.toggleShowDeletePopover}
/>
)}
</li>
);
}
}
CommentItem.propTypes = commentItemPropTypes;
export default CommentItem;

View File

@ -0,0 +1,251 @@
import React from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import classname from 'classnames';
import deepCopy from 'deep-copy';
import { gettext } from '../../../utils/constants';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../toast';
import Loading from '../../loading';
import { MentionsInput, Mention } from 'react-mentions';
import { defaultStyle } from '../../../css/react-mentions-default-style';
import CommentItemReadOnly from './comment-item-readonly';
import CommentBodyHeader from './comment-body-header';
const { username, repoID, filePath } = window.app.pageOptions;
const CommentListPropTypes = {
toggleCommentList: PropTypes.func.isRequired,
participants: PropTypes.array,
onParticipantsChange: PropTypes.func,
};
class CommentList extends React.Component {
constructor(props) {
super(props);
const initStyle = defaultStyle;
initStyle['&multiLine']['input'].minHeight = 40;
initStyle['&multiLine']['input'].height = 40;
initStyle['&multiLine']['input'].borderRadius = '5px';
initStyle['&multiLine']['input'].borderBottom = '1px solid rgb(230, 230, 221)';
initStyle['&multiLine']['input'].lineHeight = '24px';
this.state = {
comment: '',
isInputFocus: false,
defaultStyle: initStyle,
commentType: 'All comments',
};
this.toBeAddedParticipant = [];
this.commentListScrollRef = React.createRef();
}
componentDidUpdate(prevProps) {
if (prevProps.commentsList.length < this.props.commentsList.length) {
let container = this.commentListScrollRef.current;
if (container) {
container.scrollTop = container.scrollHeight + 100;
}
}
}
onKeyDown = (e) => {
if (e.key == 'Enter') {
e.preventDefault();
this.onSubmit();
}
};
handleCommentChange = (event) => {
this.setState({ comment: event.target.value });
};
onSubmit = () => {
if (!this.state.comment.trim()) {
return;
}
this.addParticipant(username);
if (this.toBeAddedParticipant.length === 0) {
this.props.addComment(this.state.comment.trim());
this.setState({ comment: '' });
} else {
seafileAPI.addFileParticipants(repoID, filePath, this.toBeAddedParticipant).then((res) => {
this.onParticipantsChange(repoID, filePath);
this.toBeAddedParticipant = [];
this.props.addComment(this.state.comment.trim());
this.setState({ comment: '' });
}).catch((err) => {
toaster.danger(Utils.getErrorMsg(err));
});
}
};
onParticipantsChange = () => {
if (this.props.onParticipantsChange) {
this.props.onParticipantsChange();
} else {
this.getParticipants();
}
};
checkParticipant = (email) => {
return this.props.participants.map((participant) => {return participant.email;}).includes(email);
};
addParticipant = (email) => {
if (this.checkParticipant(email)) return;
this.toBeAddedParticipant.push(email);
};
renderUserSuggestion = (entry, search, highlightedDisplay, index, focused) => {
return (
<div className={`comment-participant-item user ${focused ? 'active' : ''}`}>
<div className="comment-participant-container">
<img className="comment-participant-avatar" alt={highlightedDisplay} src={entry.avatar_url}/>
<div className="comment-participant-name">{highlightedDisplay}</div>
</div>
</div>
);
};
onInputFocus = () => {
if (this.inpurBlurTimer) {
clearTimeout(this.inpurBlurTimer);
this.inpurBlurTimer = null;
}
if (this.state.isInputFocus === false) {
let defaultStyle = this.state.defaultStyle;
defaultStyle['&multiLine']['input'].maxHeight = 90;
defaultStyle['&multiLine']['input'].minHeight = 90;
defaultStyle['&multiLine']['input'].height = 90;
defaultStyle['&multiLine']['input'].borderBottom = 'none';
defaultStyle['&multiLine']['input'].borderRadius = '5px 5px 0 0';
defaultStyle['&multiLine']['input'].overflowY = 'auto';
defaultStyle['&multiLine']['input'].lineHeight = 'default';
this.setState({
isInputFocus: true,
defaultStyle: deepCopy(defaultStyle),
});
}
};
onInputBlur = () => {
if (this.state.isInputFocus === true) {
this.inpurBlurTimer = setTimeout(() => {
let defaultStyle = this.state.defaultStyle;
defaultStyle['&multiLine']['input'].minHeight = 40;
defaultStyle['&multiLine']['input'].height = 40;
defaultStyle['&multiLine']['input'].borderBottom = '1px solid rgb(230, 230, 221)';
defaultStyle['&multiLine']['input'].borderRadius = '5px';
defaultStyle['&multiLine']['input'].lineHeight = '24px';
this.setState({
isInputFocus: false,
defaultStyle: deepCopy(defaultStyle),
});
}, 100);
}
};
setCommentType = (e, commentType) => {
this.setState({ commentType });
};
getFilteredComments = () => {
const { commentsList } = this.props;
if (this.state.commentType === 'All comments') {
return commentsList;
} else if (this.state.commentType === 'Resolved comments') {
return commentsList.filter((comment) => comment.resolved);
} else if (this.state.commentType === 'Unresolved comments') {
return commentsList.filter((comment) => !comment.resolved);
}
return commentsList;
};
render() {
const { commentsList, isLoading } = this.props;
const filteredComments = this.getFilteredComments();
return (
<div className="seafile-comment-page h-100">
<div className="seafile-comment-title">
<div className="comments-panel-header-left">
{gettext('Comments')}
</div>
<div className="comments-panel-header-right">
<span className="sdoc-icon-btn" onClick={this.props.toggleCommentList}>
<i className="sdocfont sdoc-sm-close"></i>
</span>
</div>
</div>
<div
className="flex-fill o-auto"
style={{ height: this.state.isInputFocus ? 'calc(100% - 170px)' : 'calc(100% - 124px)' }}
ref={this.commentListScrollRef}
>
<CommentBodyHeader
commentList={commentsList}
commentType={this.state.commentType}
setCommentType={this.setCommentType}
/>
{isLoading && <Loading/>}
{!isLoading && filteredComments.length > 0 &&
<ul className="seafile-comment-list">
{filteredComments.map((item) => {
let oldTime = (new Date(item.created_at)).getTime();
let time = dayjs(oldTime).format('YYYY-MM-DD HH:mm');
return (
<CommentItemReadOnly
key={item.id}
item={item}
time={time}
onClickComment={this.props.onClickComment}
/>
);
})}
</ul>
}
{!isLoading && filteredComments.length === 0 &&
<p className="text-center my-4">{gettext('No comment yet.')}</p>
}
</div>
<div
className={classname('seafile-comment-footer flex-shrink-0')}
style={{ height: this.state.isInputFocus ? '120px' : '72px' }}
>
<MentionsInput
value={this.state.comment}
onChange={this.handleCommentChange}
onKeyDown={this.onKeyDown}
placeholder={gettext('Enter comment, Shift + Enter for new line, Enter to send')}
style={this.state.defaultStyle}
onFocus={this.onInputFocus}
onBlur={this.onInputBlur}
>
<Mention
trigger="@"
displayTransform={(username, display) => `@${display}`}
data={this.props.relatedUsers}
renderSuggestion={this.renderUserSuggestion}
onAdd={(id, display) => {this.addParticipant(id);}}
appendSpaceOnAdd={true}
/>
</MentionsInput>
{this.state.isInputFocus &&
<div className="comment-submit-container">
<div onClick={this.onSubmit}>
<i className="sdocfont sdoc-save sdoc-comment-btn"></i>
</div>
</div>
}
</div>
</div>
);
}
}
CommentList.propTypes = CommentListPropTypes;
export default CommentList;

View File

@ -0,0 +1,175 @@
import React from 'react';
import PropTypes from 'prop-types';
import { processor } from '@seafile/seafile-editor';
import { Button, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import CommentDeletePopover from './comment-delete-popover';
const { username } = window.app.pageOptions;
const commentItemPropTypes = {
time: PropTypes.string,
item: PropTypes.object,
deleteReply: PropTypes.func,
showResolvedComment: PropTypes.bool,
editComment: PropTypes.func,
};
class ReplyItem extends React.Component {
constructor(props) {
super(props);
this.state = {
dropdownOpen: false,
html: '',
newReply: this.props.item.reply,
editable: false,
isShowDeletePopover: false,
};
}
componentWillMount() {
this.convertComment(this.props.item.reply);
}
componentWillReceiveProps(nextProps) {
this.convertComment(nextProps.item.reply);
}
toggleDropDownMenu = () => {
this.setState({
dropdownOpen: !this.state.dropdownOpen,
});
};
convertComment = (mdFile) => {
processor.process(mdFile).then((result) => {
let html = String(result);
this.setState({ html: html });
});
};
toggleEditComment = () => {
this.setState({
editable: !this.state.editable
});
};
updateComment = () => {
const newReply = this.state.newReply.trim();
if (this.props.item.reply !== newReply) {
this.props.updateReply(newReply);
}
this.toggleEditComment();
};
handleCommentChange = (event) => {
this.setState({
newReply: event.target.value,
});
};
onCommentContentClick = (e) => {
// click participant link, page shouldn't jump
if (e.target.nodeName !== 'A') return;
const preNode = e.target.previousSibling;
if (preNode && preNode.nodeType === 3 && preNode.nodeValue.slice(-1) === '@') {
e.preventDefault();
}
};
toggleShowDeletePopover = () => {
this.setState({
isShowDeletePopover: !this.state.isShowDeletePopover
});
};
render() {
const item = this.props.item;
const replyOpToolsId = `commentOpTools_${item?.id}`;
if (this.state.editable) {
return (
<li className="seafile-comment-item" id={item.id}>
<div className="seafile-comment-info mt-1">
<img className="avatar" src={item.avatar_url} alt=""/>
<div className="comment-author-info">
<div className="comment-author-name ellipsis">{item.user_name}</div>
<div className="comment-author-time">{this.props.time}</div>
</div>
</div>
<div className="seafile-edit-comment">
<textarea className="edit-comment-input" value={this.state.newReply} onChange={this.handleCommentChange} clos="100" rows="3" warp="virtual"></textarea>
<Button className="comment-btn" color="primary" size="sm" onClick={this.updateComment} id={item.id}>{gettext('Update')}</Button>{' '}
<Button className="comment-btn" color="secondary" size="sm" onClick={this.toggleEditComment}>{gettext('Cancel')}</Button>
</div>
</li>
);
}
return (
<li className={'seafile-comment-item'} id={item.id}>
<div className="seafile-comment-info mt-1">
<img className="avatar" src={item.avatar_url} alt=""/>
<div className="comment-author-info">
<div className="comment-author-name ellipsis">{item.user_name}</div>
<div className="comment-author-time">{this.props.time}</div>
</div>
{(item.user_email === username) &&
<Dropdown
isOpen={this.state.dropdownOpen}
size="sm"
className="seafile-comment-dropdown"
toggle={this.toggleDropDownMenu}
id={replyOpToolsId}
>
<DropdownToggle
tag="i"
role="button"
tabIndex="0"
className="seafile-comment-dropdown-btn sf-dropdown-toggle sf3-font-more sf3-font"
title={gettext('More operations')}
aria-label={gettext('More operations')}
data-toggle="dropdown"
aria-expanded={this.state.dropdownOpen}
aria-haspopup={true}
/>
<DropdownMenu>
<DropdownItem
onClick={this.toggleShowDeletePopover}
className="delete-comment"
id={item.id}
>
{gettext('Delete')}
</DropdownItem>
<DropdownItem
onClick={this.toggleEditComment}
className="edit-comment"
id={item.id}
>
{gettext('Edit')}
</DropdownItem>
</DropdownMenu>
</Dropdown>
}
</div>
<div
className="seafile-comment-content"
dangerouslySetInnerHTML={{ __html: this.state.html }}
onClick={e => this.onCommentContentClick(e)}
>
</div>
{this.state.isShowDeletePopover && (
<CommentDeletePopover
type="reply"
deleteConfirm={this.props.deleteReply}
setIsShowDeletePopover={this.toggleShowDeletePopover}
targetId={replyOpToolsId}
/>
)}
</li>
);
}
}
ReplyItem.propTypes = commentItemPropTypes;
export default ReplyItem;

View File

@ -0,0 +1,234 @@
import React from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import classname from 'classnames';
import deepCopy from 'deep-copy';
import { gettext } from '../../../utils/constants';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../toast';
import { MentionsInput, Mention } from 'react-mentions';
import { defaultStyle } from '../../../css/react-mentions-default-style';
import CommentItem from './comment-item';
import ReplyItem from './reply-item';
const { username, repoID, filePath } = window.app.pageOptions;
const ReplyListPropTypes = {
toggleCommentList: PropTypes.func.isRequired,
participants: PropTypes.array,
onParticipantsChange: PropTypes.func,
currentComment: PropTypes.object,
clearCurrentComment: PropTypes.func,
commentsList: PropTypes.array,
};
class ReplyList extends React.Component {
constructor(props) {
super(props);
const initStyle = defaultStyle;
initStyle['&multiLine']['input'].minHeight = 40;
initStyle['&multiLine']['input'].height = 40;
initStyle['&multiLine']['input'].borderRadius = '5px';
initStyle['&multiLine']['input'].borderBottom = '1px solid rgb(230, 230, 221)';
initStyle['&multiLine']['input'].lineHeight = '24px';
this.state = {
comment: '',
isInputFocus: false,
defaultStyle: initStyle,
};
this.toBeAddedParticipant = [];
this.commentListScrollRef = React.createRef();
}
componentDidUpdate(prevProps) {
if (prevProps.currentComment.replies.length < this.props.currentComment.replies.length) {
let container = this.commentListScrollRef.current;
if (container) {
container.scrollTop = container.scrollHeight + 100;
}
}
}
onKeyDown = (e) => {
if (e.key == 'Enter') {
e.preventDefault();
this.onSubmit();
}
};
handleCommentChange = (event) => {
this.setState({ comment: event.target.value });
};
onSubmit = () => {
if (!this.state.comment.trim()) return;
this.addParticipant(username);
if (this.toBeAddedParticipant.length === 0) {
this.props.addReply(this.state.comment.trim());
this.setState({ comment: '' });
} else {
seafileAPI.addFileParticipants(repoID, filePath, this.toBeAddedParticipant).then((res) => {
this.onParticipantsChange(repoID, filePath);
this.toBeAddedParticipant = [];
this.props.addReply(this.state.comment.trim());
this.setState({ comment: '' });
}).catch((err) => {
toaster.danger(Utils.getErrorMsg(err));
});
}
};
onParticipantsChange = () => {
if (this.props.onParticipantsChange) {
this.props.onParticipantsChange();
} else {
this.getParticipants();
}
};
checkParticipant = (email) => {
return this.props.participants.map((participant) => {return participant.email;}).includes(email);
};
addParticipant = (email) => {
if (this.checkParticipant(email)) return;
this.toBeAddedParticipant.push(email);
};
renderUserSuggestion = (entry, search, highlightedDisplay, index, focused) => {
return (
<div className={`comment-participant-item user ${focused ? 'active' : ''}`}>
<div className="comment-participant-container">
<img className="comment-participant-avatar" alt={highlightedDisplay} src={entry.avatar_url}/>
<div className="comment-participant-name">{highlightedDisplay}</div>
</div>
</div>
);
};
onInputFocus = () => {
if (this.inpurBlurTimer) {
clearTimeout(this.inpurBlurTimer);
this.inpurBlurTimer = null;
}
if (this.state.isInputFocus === false) {
let defaultStyle = this.state.defaultStyle;
defaultStyle['&multiLine']['input'].maxHeight = 90;
defaultStyle['&multiLine']['input'].minHeight = 90;
defaultStyle['&multiLine']['input'].height = 90;
defaultStyle['&multiLine']['input'].borderBottom = 'none';
defaultStyle['&multiLine']['input'].borderRadius = '5px 5px 0 0';
defaultStyle['&multiLine']['input'].overflowY = 'auto';
defaultStyle['&multiLine']['input'].lineHeight = 'default';
this.setState({
isInputFocus: true,
defaultStyle: deepCopy(defaultStyle),
});
}
};
onInputBlur = () => {
if (this.state.isInputFocus === true) {
this.inpurBlurTimer = setTimeout(() => {
let defaultStyle = this.state.defaultStyle;
defaultStyle['&multiLine']['input'].minHeight = 40;
defaultStyle['&multiLine']['input'].height = 40;
defaultStyle['&multiLine']['input'].borderBottom = '1px solid rgb(230, 230, 221)';
defaultStyle['&multiLine']['input'].borderRadius = '5px';
defaultStyle['&multiLine']['input'].lineHeight = '24px';
this.setState({
isInputFocus: false,
defaultStyle: deepCopy(defaultStyle),
});
}, 100);
}
};
render() {
const { currentComment } = this.props;
const { replies } = currentComment;
return (
<div className="seafile-reply-page h-100">
<div className="seafile-comment-title">
<div className="comments-panel-header-left">
<div className="goback sdoc-icon-btn ml-0 mr-1" onClick={this.props.clearCurrentComment}>
<i className="sdocfont sdoc-previous-page" style={{ transform: 'scale(1.2)' }}></i>
</div>
<span className="title">{gettext('Comment details')}</span>
</div>
<div className="comments-panel-header-right">
<div className="sdoc-icon-btn" onClick={this.props.toggleCommentList}>
<i className="sdocfont sdoc-sm-close"></i>
</div>
</div>
</div>
<div
className="flex-fill o-auto"
style={{ height: this.state.isInputFocus ? 'calc(100% - 170px)' : 'calc(100% - 124px)' }}
ref={this.commentListScrollRef}
>
<ul className="seafile-comment-list">
<CommentItem
key={currentComment.id}
item={currentComment}
deleteComment={this.props.deleteComment}
resolveComment={this.props.resolveComment}
editComment={this.props.editComment}
/>
{replies.map((item) => {
let oldTime = (new Date(item.created_at)).getTime();
let time = dayjs(oldTime).format('YYYY-MM-DD HH:mm');
return (
<ReplyItem
key={item.id}
item={item}
time={time}
deleteReply={() => this.props.deleteReply(currentComment.id, item.id)}
updateReply={(replyContent) => this.props.updateReply(currentComment.id, item.id, replyContent)}
/>
);
})}
</ul>
</div>
<div
className={classname('seafile-comment-footer flex-shrink-0')}
style={{ height: this.state.isInputFocus ? '120px' : '72px' }}
>
<MentionsInput
value={this.state.comment}
onChange={this.handleCommentChange}
placeholder={gettext('Enter reply, Shift + Enter for new line, Enter to send')}
onKeyDown={this.onKeyDown}
style={this.state.defaultStyle}
onFocus={this.onInputFocus}
onBlur={this.onInputBlur}
>
<Mention
trigger="@"
displayTransform={(username, display) => `@${display}`}
data={this.state.relatedUsers}
renderSuggestion={this.renderUserSuggestion}
onAdd={(id, display) => {this.addParticipant(id);}}
appendSpaceOnAdd={true}
/>
</MentionsInput>
{this.state.isInputFocus &&
<div className="comment-submit-container">
<div onClick={this.onSubmit}>
<i className="sdocfont sdoc-save sdoc-comment-btn"></i>
</div>
</div>
}
</div>
</div>
);
}
}
ReplyList.propTypes = ReplyListPropTypes;
export default ReplyList;

View File

@ -18,6 +18,7 @@ const propTypes = {
isSaving: PropTypes.bool,
needSave: PropTypes.bool,
toggleLockFile: PropTypes.func.isRequired,
toggleCommentPanel: PropTypes.func.isRequired,
toggleDetailsPanel: PropTypes.func.isRequired,
setImageScale: PropTypes.func,
rotateImage: PropTypes.func
@ -157,15 +158,6 @@ class FileToolbar extends React.Component {
onClick={this.props.toggleLockFile}
/>
)}
{showShareBtn && (
<IconButton
id="share-file"
icon='share'
text={gettext('Share')}
onClick={this.toggleShareDialog}
/>
)}
{(canEditFile && fileType != 'SDoc' && !err) &&
(this.props.isSaving ?
<div type='button' aria-label={gettext('Saving...')} className={'file-toolbar-btn'}>
@ -198,12 +190,19 @@ class FileToolbar extends React.Component {
text={gettext('Details')}
onClick={this.props.toggleDetailsPanel}
/>
{filePerm == 'rw' && (
<div
className='file-toolbar-btn'
onClick={this.props.toggleCommentPanel}
aria-label={gettext('Comment')}
>
<i className="sdocfont sdoc-comments"></i>
</div>
{showShareBtn && (
<IconButton
id="open-via-client"
icon="client"
text={gettext('Open via Client')}
href={`seafile://openfile?repo_id=${encodeURIComponent(repoID)}&path=${encodeURIComponent(filePath)}`}
id="share-file"
icon='share'
text={gettext('Share')}
onClick={this.toggleShareDialog}
/>
)}
<Dropdown isOpen={moreDropdownOpen} toggle={this.toggleMoreOpMenu}>
@ -216,6 +215,11 @@ class FileToolbar extends React.Component {
<Icon symbol="more-vertical" />
</DropdownToggle>
<DropdownMenu>
{/* {(
<DropdownItem onClick={this.props.toggleCommentPanel}>
{gettext('Comment')}
</DropdownItem>
)} */}
{filePerm == 'rw' && (
<a href={`${siteRoot}repo/file_revisions/${repoID}/?p=${encodeURIComponent(filePath)}&referer=${encodeURIComponent(location.href)}`} className="dropdown-item">
{gettext('History')}
@ -224,6 +228,11 @@ class FileToolbar extends React.Component {
<a href={`${siteRoot}library/${repoID}/${Utils.encodePath(repoName + parentDir)}`} className="dropdown-item">
{gettext('Open parent folder')}
</a>
{filePerm == 'rw' && (
<a href={`seafile://openfile?repo_id=${encodeURIComponent(repoID)}&path=${encodeURIComponent(filePath)}`} className="dropdown-item">
{gettext('Open via client')}
</a>
)}
</DropdownMenu>
</Dropdown>
</div>
@ -275,6 +284,11 @@ class FileToolbar extends React.Component {
</a>
</DropdownItem>
)}
{(
<DropdownItem onClick={this.props.toggleCommentPanel}>
{gettext('Comment')}
</DropdownItem>
)}
<DropdownItem onClick={this.props.toggleDetailsPanel}>{gettext('Details')}</DropdownItem>
</DropdownMenu>
</Dropdown>

View File

@ -10,6 +10,7 @@ import toaster from '../toast';
import IconButton from '../icon-button';
import FileInfo from './file-info';
import FileToolbar from './file-toolbar';
import CommentPanel from './comment-panel';
import OnlyofficeFileToolbar from './onlyoffice-file-toolbar';
import EmbeddedFileDetails from '../dirent-detail/embedded-file-details';
import { MetadataStatusProvider } from '../../hooks';
@ -43,6 +44,7 @@ class FileView extends React.Component {
isStarred: isStarred,
isLocked: isLocked,
lockedByMe: lockedByMe,
isCommentPanelOpen: false,
isHeaderShown: (storedIsHeaderShown === null) || (storedIsHeaderShown == 'true'),
isDetailsPanelOpen: false
};
@ -53,8 +55,18 @@ class FileView extends React.Component {
document.getElementById('favicon').href = fileIcon;
}
toggleCommentPanel = () => {
this.setState({
isCommentPanelOpen: !this.state.isCommentPanelOpen,
isDetailsPanelOpen: false,
});
};
toggleDetailsPanel = () => {
this.setState({ isDetailsPanelOpen: !this.state.isDetailsPanelOpen });
this.setState({
isDetailsPanelOpen: !this.state.isDetailsPanelOpen,
isCommentPanelOpen: false,
});
};
toggleStar = () => {
@ -142,6 +154,7 @@ class FileView extends React.Component {
isSaving={this.props.isSaving}
needSave={this.props.needSave}
toggleLockFile={this.toggleLockFile}
toggleCommentPanel={this.toggleCommentPanel}
toggleDetailsPanel={this.toggleDetailsPanel}
setImageScale={this.props.setImageScale}
rotateImage={this.props.rotateImage}
@ -158,6 +171,13 @@ class FileView extends React.Component {
/>
}
{this.props.content}
{this.state.isCommentPanelOpen &&
<CommentPanel
toggleCommentPanel={this.toggleCommentPanel}
participants={this.props.participants}
onParticipantsChange={this.props.onParticipantsChange}
/>
}
{isDetailsPanelOpen && (
<MetadataStatusProvider repoID={repoID} repoInfo={repoInfo}>
<CollaboratorsProvider repoID={repoID}>

View File

@ -10,7 +10,7 @@ import OpIcon from './op-icon';
const propTypes = {
groupMembers: PropTypes.array.isRequired,
groupID: PropTypes.string,
groupID: PropTypes.number.isRequired,
isOwner: PropTypes.bool.isRequired,
isItemFreezed: PropTypes.bool.isRequired,
toggleItemFreezed: PropTypes.func.isRequired,
@ -61,7 +61,7 @@ const MemberPropTypes = {
changeMember: PropTypes.func.isRequired,
deleteMember: PropTypes.func.isRequired,
toggleItemFreezed: PropTypes.func.isRequired,
groupID: PropTypes.string,
groupID: PropTypes.number.isRequired,
isOwner: PropTypes.bool.isRequired,
isItemFreezed: PropTypes.bool.isRequired
};

View File

@ -1,27 +1,313 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import AISearchIcon from '../assets/icons/AI-search.svg';
import AddTableIcon from '../assets/icons/add-table.svg';
import AiIcon from '../assets/icons/ai.svg';
import ArrowIcon from '../assets/icons/arrow.svg';
import BellIcon from '../assets/icons/bell.svg';
import CheckCircleIcon from '../assets/icons/check-circle.svg';
import CheckMarkIcon from '../assets/icons/check-mark.svg';
import CheckSquareSolidIcon from '../assets/icons/check-square-solid.svg';
import CheckboxIcon from '../assets/icons/checkbox.svg';
import ClientIcon from '../assets/icons/client.svg';
import CloseIcon from '../assets/icons/close.svg';
import CollaboratorIcon from '../assets/icons/collaborator.svg';
import CopyIcon from '../assets/icons/copy.svg';
import CreationTimeIcon from '../assets/icons/creation-time.svg';
import CreatorIcon from '../assets/icons/creator.svg';
import CurrencyIcon from '../assets/icons/currency.svg';
import DateIcon from '../assets/icons/date.svg';
import DeleteIcon from '../assets/icons/delete.svg';
import DescriptionIcon from '../assets/icons/description.svg';
import DoubleArrowDownIcon from '../assets/icons/double-arrow-down.svg';
import DoubleArrowUpIcon from '../assets/icons/double-arrow-up.svg';
import DownloadIcon from '../assets/icons/download.svg';
import DragIcon from '../assets/icons/drag.svg';
import DropDownIcon from '../assets/icons/drop-down.svg';
import EditIcon from '../assets/icons/edit.svg';
import ExclamationCircleIcon from '../assets/icons/exclamation-circle.svg';
import ExclamationTriangleIcon from '../assets/icons/exclamation-triangle.svg';
import ExpandIcon from '../assets/icons/expand.svg';
import EyeSlashIcon from '../assets/icons/eye-slash.svg';
import FaceRecognitionViewIcon from '../assets/icons/face-recognition-view.svg';
import FileIcon from '../assets/icons/file.svg';
import FilesIcon from '../assets/icons/files.svg';
import FilterCircledIcon from '../assets/icons/filter-circled.svg';
import FilterIcon from '../assets/icons/filter.svg';
import FlagIcon from '../assets/icons/flag.svg';
import FolderIcon from '../assets/icons/folder.svg';
import FoldersIcon from '../assets/icons/folders.svg';
import ForkNumberIcon from '../assets/icons/fork-number.svg';
import GroupIcon from '../assets/icons/group.svg';
import HelpfulSelectedIcon from '../assets/icons/helpful-selected.svg';
import HelpfulIcon from '../assets/icons/helpful.svg';
import HelplessSelectedIcon from '../assets/icons/helpless-selected.svg';
import HelplessIcon from '../assets/icons/helpless.svg';
import HideIcon from '../assets/icons/hide.svg';
import ImageIcon from '../assets/icons/image.svg';
import InfoIcon from '../assets/icons/info.svg';
import KanbanIcon from '../assets/icons/kanban.svg';
import LeftArrowIcon from '../assets/icons/left_arrow.svg';
import LikeIcon from '../assets/icons/like.svg';
import LinkIcon from '../assets/icons/link.svg';
import LinkageIcon from '../assets/icons/linkage.svg';
import LocationIcon from '../assets/icons/location.svg';
import LockIcon from '../assets/icons/lock.svg';
import LongTextIcon from '../assets/icons/long-text.svg';
import MainViewIcon from '../assets/icons/main-view.svg';
import MapIcon from '../assets/icons/map.svg';
import MarkdownIcon from '../assets/icons/markdown.svg';
import MinusSignIcon from '../assets/icons/minus_sign.svg';
import MonitorIcon from '../assets/icons/monitor.svg';
import MoreLevelIcon from '../assets/icons/more-level.svg';
import MoreVerticalIcon from '../assets/icons/more-vertical.svg';
import MoveToIcon from '../assets/icons/move-to.svg';
import MultipleSelectIcon from '../assets/icons/multiple-select.svg';
import NumberIcon from '../assets/icons/number.svg';
import OpenFileIcon from '../assets/icons/open-file.svg';
import OpenFolderIcon from '../assets/icons/open-folder.svg';
import PartiallySelectedIcon from '../assets/icons/partially-selected.svg';
import PlusSignIcon from '../assets/icons/plus_sign.svg';
import PraiseIcon from '../assets/icons/praise.svg';
import PrintIcon from '../assets/icons/print.svg';
import RateIcon from '../assets/icons/rate.svg';
import RemoveFromFolderIcon from '../assets/icons/remove-from-folder.svg';
import RenameIcon from '../assets/icons/rename.svg';
import RightArrowIcon from '../assets/icons/right_arrow.svg';
import RotateIcon from '../assets/icons/rotate.svg';
import RowHeightDefaultIcon from '../assets/icons/row-height-default.svg';
import RowHeightDoubleIcon from '../assets/icons/row-height-double.svg';
import RowHeightQuadrupleIcon from '../assets/icons/row-height-quadruple.svg';
import RowHeightTripleIcon from '../assets/icons/row-height-triple.svg';
import SaveIcon from '../assets/icons/save.svg';
import SearchIcon from '../assets/icons/search.svg';
import SendIcon from '../assets/icons/send.svg';
import SetUpIcon from '../assets/icons/set-up.svg';
import ShareIcon from '../assets/icons/share.svg';
import SingleSelectIcon from '../assets/icons/single-select.svg';
import SortAscendingIcon from '../assets/icons/sort-ascending.svg';
import SortDescendingIcon from '../assets/icons/sort-descending.svg';
import SortIcon from '../assets/icons/sort.svg';
import SpinnerIcon from '../assets/icons/spinner.svg';
import TableIcon from '../assets/icons/table.svg';
import TagIcon from '../assets/icons/tag.svg';
import TextIcon from '../assets/icons/text.svg';
import TimeIcon from '../assets/icons/time.svg';
import UnlockIcon from '../assets/icons/unlock.svg';
import UrlIcon from '../assets/icons/url.svg';
import WikiPreviewIcon from '../assets/icons/wiki-preview.svg';
import WikiSettingsIcon from '../assets/icons/wiki-settings.svg';
import X01Icon from '../assets/icons/x-01.svg';
import '../css/icon.css';
const importAll = (requireContext) => {
requireContext.keys().forEach(requireContext);
};
try {
importAll(require.context('../assets/icons', true, /\.svg$/));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
const Icon = (props) => {
const { className, symbol, style } = props;
const iconClass = classnames('seafile-multicolor-icon', className, `seafile-multicolor-icon-${symbol}`);
return (
<svg className={iconClass} style={style}>
<use xlinkHref={`#${symbol}`} />
</svg>
);
const commonProps = { className: iconClass, style: style, ariaHidden: 'true' };
switch (symbol) {
case 'ai-search':
return <AISearchIcon {...commonProps} />;
case 'add-table':
return <AddTableIcon {...commonProps} />;
case 'ai':
return <AiIcon {...commonProps} />;
case 'arrow':
return <ArrowIcon {...commonProps} />;
case 'bell':
return <BellIcon {...commonProps} />;
case 'check-circle':
return <CheckCircleIcon {...commonProps} />;
case 'check-mark':
return <CheckMarkIcon {...commonProps} />;
case 'check-square-solid':
return <CheckSquareSolidIcon {...commonProps} />;
case 'checkbox':
return <CheckboxIcon {...commonProps} />;
case 'client':
return <ClientIcon {...commonProps} />;
case 'close':
return <CloseIcon {...commonProps} />;
case 'collaborator':
return <CollaboratorIcon {...commonProps} />;
case 'copy':
return <CopyIcon {...commonProps} />;
case 'creation-time':
return <CreationTimeIcon {...commonProps} />;
case 'creator':
return <CreatorIcon {...commonProps} />;
case 'currency':
return <CurrencyIcon {...commonProps} />;
case 'date':
return <DateIcon {...commonProps} />;
case 'delete':
return <DeleteIcon {...commonProps} />;
case 'description':
return <DescriptionIcon {...commonProps} />;
case 'double-arrow-down':
return <DoubleArrowDownIcon {...commonProps} />;
case 'double-arrow-up':
return <DoubleArrowUpIcon {...commonProps} />;
case 'download':
return <DownloadIcon {...commonProps} />;
case 'drag':
return <DragIcon {...commonProps} />;
case 'drop-down':
return <DropDownIcon {...commonProps} />;
case 'edit':
return <EditIcon {...commonProps} />;
case 'exclamation-circle':
return <ExclamationCircleIcon {...commonProps} />;
case 'exclamation-triangle':
return <ExclamationTriangleIcon {...commonProps} />;
case 'expand':
return <ExpandIcon {...commonProps} />;
case 'eye-slash':
return <EyeSlashIcon {...commonProps} />;
case 'face-recognition-view':
return <FaceRecognitionViewIcon {...commonProps} />;
case 'file':
return <FileIcon {...commonProps} />;
case 'files':
return <FilesIcon {...commonProps} />;
case 'filter-circled':
return <FilterCircledIcon {...commonProps} />;
case 'filter':
return <FilterIcon {...commonProps} />;
case 'flag':
return <FlagIcon {...commonProps} />;
case 'folder':
return <FolderIcon {...commonProps} />;
case 'folders':
return <FoldersIcon {...commonProps} />;
case 'fork-number':
return <ForkNumberIcon {...commonProps} />;
case 'group':
return <GroupIcon {...commonProps} />;
case 'helpful-selected':
return <HelpfulSelectedIcon {...commonProps} />;
case 'helpful':
return <HelpfulIcon {...commonProps} />;
case 'helpless-selected':
return <HelplessSelectedIcon {...commonProps} />;
case 'helpless':
return <HelplessIcon {...commonProps} />;
case 'hide':
return <HideIcon {...commonProps} />;
case 'image':
return <ImageIcon {...commonProps} />;
case 'info':
return <InfoIcon {...commonProps} />;
case 'kanban':
return <KanbanIcon {...commonProps} />;
case 'left-arrow':
return <LeftArrowIcon {...commonProps} />;
case 'like':
return <LikeIcon {...commonProps} />;
case 'link':
return <LinkIcon {...commonProps} />;
case 'linkage':
return <LinkageIcon {...commonProps} />;
case 'location':
return <LocationIcon {...commonProps} />;
case 'lock':
return <LockIcon {...commonProps} />;
case 'long-text':
return <LongTextIcon {...commonProps} />;
case 'main-view':
return <MainViewIcon {...commonProps} />;
case 'map':
return <MapIcon {...commonProps} />;
case 'markdown':
return <MarkdownIcon {...commonProps} />;
case 'minus_sign':
return <MinusSignIcon {...commonProps} />;
case 'monitor':
return <MonitorIcon {...commonProps} />;
case 'more-level':
return <MoreLevelIcon {...commonProps} />;
case 'more-vertical':
return <MoreVerticalIcon {...commonProps} />;
case 'move-to':
return <MoveToIcon {...commonProps} />;
case 'multiple-select':
return <MultipleSelectIcon {...commonProps} />;
case 'number':
return <NumberIcon {...commonProps} />;
case 'open-file':
return <OpenFileIcon {...commonProps} />;
case 'open-folder':
return <OpenFolderIcon {...commonProps} />;
case 'partially-selected':
return <PartiallySelectedIcon {...commonProps} />;
case 'plus_sign':
return <PlusSignIcon {...commonProps} />;
case 'praise':
return <PraiseIcon {...commonProps} />;
case 'print':
return <PrintIcon {...commonProps} />;
case 'rate':
return <RateIcon {...commonProps} />;
case 'remove-from-folder':
return <RemoveFromFolderIcon {...commonProps} />;
case 'rename':
return <RenameIcon {...commonProps} />;
case 'right_arrow':
return <RightArrowIcon {...commonProps} />;
case 'rotate':
return <RotateIcon {...commonProps} />;
case 'row-height-default':
return <RowHeightDefaultIcon {...commonProps} />;
case 'row-height-double':
return <RowHeightDoubleIcon {...commonProps} />;
case 'row-height-quadruple':
return <RowHeightQuadrupleIcon {...commonProps} />;
case 'row-height-triple':
return <RowHeightTripleIcon {...commonProps} />;
case 'save':
return <SaveIcon {...commonProps} />;
case 'search':
return <SearchIcon {...commonProps} />;
case 'send':
return <SendIcon {...commonProps} />;
case 'set-up':
return <SetUpIcon {...commonProps} />;
case 'share':
return <ShareIcon {...commonProps} />;
case 'single-select':
return <SingleSelectIcon {...commonProps} />;
case 'sort-ascending':
return <SortAscendingIcon {...commonProps} />;
case 'sort-descending':
return <SortDescendingIcon {...commonProps} />;
case 'sort':
return <SortIcon {...commonProps} />;
case 'spinner':
return <SpinnerIcon {...commonProps} />;
case 'table':
return <TableIcon {...commonProps} />;
case 'tag':
return <TagIcon {...commonProps} />;
case 'text':
return <TextIcon {...commonProps} />;
case 'time':
return <TimeIcon {...commonProps} />;
case 'unlock':
return <UnlockIcon {...commonProps} />;
case 'url':
return <UrlIcon {...commonProps} />;
case 'wiki-preview':
return <WikiPreviewIcon {...commonProps} />;
case 'wiki-settings':
return <WikiSettingsIcon {...commonProps} />;
case 'x-01':
return <X01Icon {...commonProps} />;
default:
return null;
}
};
Icon.propTypes = {

View File

@ -12,7 +12,7 @@ import GroupMembers from './group-members';
const propTypes = {
toggleManageMembersDialog: PropTypes.func,
toggleDepartmentDetailDialog: PropTypes.func,
groupID: PropTypes.string,
groupID: PropTypes.number.isRequired,
isOwner: PropTypes.bool.isRequired
};

View File

@ -1,31 +0,0 @@
.list-tag-popover .popover {
width: 500px;
max-width: 500px;
}
.list-tag-popover .add-tag-link {
cursor: pointer;
}
.list-tag-popover .tag-list-footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
padding: 1rem;
border-top: 1px solid #dedede;
}
.list-tag-popover .tag-list-footer .item-text {
color: #ff9800;
cursor: pointer;
}
.list-tag-popover .tag-list-footer a:hover {
text-decoration: none;
}
.list-tag-popover .tag-color {
width: 20px;
height: 20px;
}

View File

@ -1,160 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { v4 as uuidv4 } from 'uuid';
import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
import RepoTag from '../../models/repo-tag';
import TagListItem from './tag-list-item';
import VirtualTagListItem from './virtual-tag-list-item';
import TagListFooter from './tag-list-footer';
import { TAG_COLORS } from '../../constants/';
import '../../css/repo-tag.css';
import './list-tag-popover.css';
export default class ListTagPopover extends React.Component {
static propTypes = {
repoID: PropTypes.string.isRequired,
onListTagCancel: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
repotagList: []
};
}
componentDidMount() {
this.loadTags();
}
componentWillUnmount() {
this.setState = () => {};
}
loadTags = () => {
seafileAPI.listRepoTags(this.props.repoID).then(res => {
let repotagList = [];
res.data.repo_tags.forEach(item => {
let repo_tag = new RepoTag(item);
repotagList.push(repo_tag);
});
this.setState({ repotagList });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
updateTags = (newRepotagList) => {
this.setState({
repotagList: [...this.state.repotagList, ...newRepotagList],
});
};
onDeleteTag = (tag) => {
const { repoID } = this.props;
const { id: targetTagID } = tag;
seafileAPI.deleteRepoTag(repoID, targetTagID).then((res) => {
this.setState({
repotagList: this.state.repotagList.filter(tag => tag.id != targetTagID)
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
createVirtualTag = (e) => {
e.preventDefault();
let { repotagList } = this.state;
let virtual_repo_tag = {
name: '',
color: TAG_COLORS[Math.floor(Math.random() * TAG_COLORS.length)], // generate random tag color for virtual tag
id: `virtual-tag-${uuidv4()}`,
is_virtual: true,
};
repotagList.push(virtual_repo_tag);
this.setState({ repotagList });
};
deleteVirtualTag = (virtualTag) => {
let { repotagList } = this.state;
let index = repotagList.findIndex(item => item.id === virtualTag.id);
repotagList.splice(index, 1);
this.setState({ repotagList });
};
updateVirtualTag = (virtualTag, data) => {
const repoID = this.props.repoID;
const { repotagList } = this.state;
const index = repotagList.findIndex(item => item.id === virtualTag.id);
if (index < 0) return null;
// If virtual tag color is updated and virtual tag name is empty, it will be saved to local state, don't save it to the server
if (data.color) {
virtualTag.color = data.color;
repotagList[index] = virtualTag;
this.setState({ repotagList });
return;
}
// If virtual tag name is updated and name is not empty, virtual tag color use default, save it to the server
if (data.name && data.name.length > 0) {
let color = virtualTag.color;
let name = data.name;
seafileAPI.createRepoTag(repoID, name, color).then((res) => {
// After saving sag to the server, replace the virtual tag with newly created tag
repotagList[index] = new RepoTag(res.data.repo_tag);
this.setState({ repotagList });
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
};
render() {
return (
<Fragment>
<ul className="tag-list tag-list-container my-2">
{this.state.repotagList.map((repoTag, index) => {
if (repoTag.is_virtual) {
return (
<VirtualTagListItem
key={index}
item={repoTag}
repoID={this.props.repoID}
deleteVirtualTag={this.deleteVirtualTag}
updateVirtualTag={this.updateVirtualTag}
/>
);
} else {
return (
<TagListItem
key={index}
item={repoTag}
repoID={this.props.repoID}
onDeleteTag={this.onDeleteTag}
/>
);
}
})}
</ul>
<div className="add-tag-link px-4 py-2 d-flex align-items-center" onClick={this.createVirtualTag}>
<span className="sf2-icon-plus mr-2"></span>{gettext('Create a new tag')}
</div>
<TagListFooter
toggle={this.props.onListTagCancel}
repotagList={this.state.repotagList}
updateTags={this.updateTags}
repoID={this.props.repoID}
/>
</Fragment>
);
}
}

View File

@ -1,137 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Tooltip } from 'reactstrap';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import RepoTag from '../../models/repo-tag';
import toaster from '../toast';
export default class TagListFooter extends Component {
static propTypes = {
repoID: PropTypes.string.isRequired,
toggle: PropTypes.func.isRequired,
repotagList: PropTypes.array.isRequired,
updateTags: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
showTooltip: false,
};
}
toggleTooltip = () => {
this.setState({ showTooltip: !this.state.showTooltip });
};
onClickImport = () => {
this.importOptionsInput.click();
};
importTagsInputChange = () => {
if (!this.importOptionsInput.files || !this.importOptionsInput.files.length) {
toaster.warning(gettext('Please select a file'));
return;
}
const fileReader = new FileReader();
fileReader.onload = this.onImportTags.bind(this);
fileReader.onerror = this.onImportTagsError.bind(this);
fileReader.readAsText(this.importOptionsInput.files[0]);
};
getValidTags = (tags) => {
let validTags = [];
let tagNameMap = {};
this.props.repotagList.forEach(tag => tagNameMap[tag.name] = true);
for (let i = 0; i < tags.length; i++) {
if (!tags[i] || typeof tags[i] !== 'object' || !tags[i].name || !tags[i].color) {
continue;
}
if (!tagNameMap[tags[i].name]) {
validTags.push(
{
name: tags[i].name,
color: tags[i].color,
}
);
tagNameMap[tags[i].name] = true;
}
}
return validTags;
};
onImportTags = (event) => {
let tags = [];
try {
tags = JSON.parse(event.target.result); // handle JSON file format is error
} catch (error) {
toaster.danger(gettext('The imported tags are invalid'));
return;
}
if (!Array.isArray(tags) || tags.length === 0) {
toaster.danger(gettext('The imported tags are invalid'));
return;
}
let validTags = this.getValidTags(tags);
if (validTags.length === 0) {
toaster.warning(gettext('The imported tag already exists'));
return;
}
seafileAPI.createRepoTags(this.props.repoID, validTags).then((res) => {
toaster.success(gettext('Tags imported'));
let repotagList = [];
res.data.repo_tags.forEach(item => {
let repo_tag = new RepoTag(item);
repotagList.push(repo_tag);
});
this.props.updateTags(repotagList);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
this.importOptionsInput.value = null;
};
onImportTagsError = () => {
toaster.success(gettext('Failed to import tags. Please reupload.'));
};
getDownloadUrl = () => {
const tags = this.props.repotagList.map(item => {
return { name: item.name, color: item.color };
});
return `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(tags))}`;
};
render() {
return (
<div className="tag-list-footer">
<span className="sf3-font sf3-font-tips mr-2" style={{ color: '#999' }} id="import-export-tags-tip"></span>
<Tooltip
toggle={this.toggleTooltip}
delay={{ show: 0, hide: 0 }}
target='import-export-tags-tip'
placement='bottom'
isOpen={this.state.showTooltip}
>
{gettext('Use the import/export function to transfer tags quickly to another library. (The export is in JSON format.)')}
</Tooltip>
<input
type="file"
ref={ref => this.importOptionsInput = ref}
accept='.json'
className="d-none"
onChange={this.importTagsInputChange}
/>
<span className="item-text" onClick={this.onClickImport}>{gettext('Import tags')}</span>
<span className="mx-2">|</span>
<a href={this.getDownloadUrl()} download='tags.json' onClick={this.props.toggle}>
<span className="item-text">{gettext('Export tags')}</span>
</a>
</div>
);
}
}

View File

@ -1,66 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import TagColor from '../dialog/tag-color';
import TagName from '../dialog/tag-name';
import '../../css/repo-tag.css';
import './list-tag-popover.css';
const tagListItemPropTypes = {
item: PropTypes.object.isRequired,
repoID: PropTypes.string.isRequired,
onDeleteTag: PropTypes.func.isRequired
};
class TagListItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isTagHighlighted: false
};
}
onMouseOver = () => {
this.setState({
isTagHighlighted: true
});
};
onMouseOut = () => {
this.setState({
isTagHighlighted: false
});
};
deleteTag = () => {
this.props.onDeleteTag(this.props.item);
};
render() {
const { isTagHighlighted } = this.state;
const { item, repoID } = this.props;
return (
<li
className={`tag-list-item px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
>
<TagColor repoID={repoID} tag={item} />
<TagName repoID={repoID} tag={item} />
<button
className={`tag-delete-icon sf3-font-delete1 sf3-font border-0 px-0 bg-transparent cursor-pointer ${isTagHighlighted ? '' : 'invisible'}`}
onClick={this.deleteTag}
aria-label={gettext('Delete')}
title={gettext('Delete')}
>
</button>
</li>
);
}
}
TagListItem.propTypes = tagListItemPropTypes;
export default TagListItem;

View File

@ -1,96 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Popover, PopoverBody } from 'reactstrap';
import { TAG_COLORS } from '../../constants';
import '../../css/repo-tag.css';
export default class VirtualTagColor extends React.Component {
static propTypes = {
updateVirtualTag: PropTypes.func.isRequired,
tag: PropTypes.object.isRequired,
repoID: PropTypes.string.isRequired
};
constructor(props) {
super(props);
this.state = {
tagColor: this.props.tag.color,
isPopoverOpen: false
};
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.tag.color !== this.props.tag.color) {
this.setState({
tagColor: nextProps.tag.color,
});
}
}
togglePopover = () => {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen
});
};
selectTagColor = (e) => {
const newColor = e.target.value;
this.props.updateVirtualTag(this.props.tag, { color: newColor });
this.setState({
tagColor: newColor,
isPopoverOpen: !this.state.isPopoverOpen,
});
};
render() {
const { isPopoverOpen, tagColor } = this.state;
const { tag } = this.props;
const { id, color } = tag;
let colorList = [...TAG_COLORS];
// for color from previous color options
if (colorList.indexOf(color) == -1) {
colorList.unshift(color);
}
return (
<div>
<span
id={`tag-${id}-color`}
className="tag-color cursor-pointer rounded-circle d-flex align-items-center justify-content-center"
style={{ backgroundColor: tagColor }}
onClick={this.togglePopover}
>
<i className="sf3-font sf3-font-down text-white"></i>
</span>
<Popover
target={`tag-${id}-color`}
isOpen={isPopoverOpen}
placement="bottom"
toggle={this.togglePopover}
className="tag-color-popover mw-100"
>
<PopoverBody className="p-2">
<div className="d-flex justify-content-between">
{colorList.map((item, index) => {
return (
<div key={index} className="tag-color-option mx-1">
<label className="colorinput">
<input name="color" type="radio" value={item} className="colorinput-input" defaultChecked={item == tagColor} onClick={this.selectTagColor} />
<span className="colorinput-color rounded-circle d-flex align-items-center justify-content-center" style={{ backgroundColor: item }}>
<i className="sf2-icon-tick color-selected"></i>
</span>
</label>
</div>
);
})
}
</div>
</PopoverBody>
</Popover>
</div>
);
}
}

View File

@ -1,59 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import VirtualTagColor from './virtual-tag-color';
import VirtualTagName from './virtual-tag-name';
import '../../css/repo-tag.css';
import './list-tag-popover.css';
export default class VirtualTagListItem extends React.Component {
static propTypes = {
item: PropTypes.object.isRequired,
repoID: PropTypes.string.isRequired,
deleteVirtualTag: PropTypes.func.isRequired,
updateVirtualTag: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
isTagHighlighted: false
};
}
onMouseOver = () => {
this.setState({ isTagHighlighted: true });
};
onMouseOut = () => {
this.setState({ isTagHighlighted: false });
};
deleteVirtualTag = () => {
this.props.deleteVirtualTag(this.props.item);
};
render() {
const { isTagHighlighted } = this.state;
const { item, repoID } = this.props;
return (
<li
className={`tag-list-item px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
>
<VirtualTagColor repoID={repoID} tag={item} updateVirtualTag={this.props.updateVirtualTag} />
<VirtualTagName repoID={repoID} tag={item} updateVirtualTag={this.props.updateVirtualTag} />
<button
className={`tag-delete-icon sf3-font-delete1 sf3-font border-0 px-0 bg-transparent cursor-pointer ${isTagHighlighted ? '' : 'invisible'}`}
onClick={this.deleteVirtualTag}
aria-label={gettext('Delete')}
title={gettext('Delete')}
>
</button>
</li>
);
}
}

View File

@ -1,90 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import '../../css/repo-tag.css';
export default class VirtualTagName extends React.Component {
static propTypes = {
updateVirtualTag: PropTypes.func.isRequired,
tag: PropTypes.object.isRequired,
repoID: PropTypes.string.isRequired
};
constructor(props) {
super(props);
this.state = {
tagName: this.props.tag.name,
isEditing: true,
};
this.input = React.createRef();
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.tag.name !== this.props.tag.name) {
this.setState({
tagName: nextProps.tag.name,
});
}
}
componentDidMount() {
setTimeout(() => {
this.input.current.focus();
}, 1);
}
toggleMode = () => {
this.setState({
isEditing: !this.state.isEditing
});
};
updateTagName = (e) => {
const newName = e.target.value;
this.props.updateVirtualTag(this.props.tag, { name: newName });
this.setState({
tagName: newName
});
};
onInputKeyDown = (e) => {
if (e.key == 'Enter') {
this.toggleMode();
this.updateTagName(e);
}
else if (e.key == 'Escape') {
e.nativeEvent.stopImmediatePropagation();
this.toggleMode();
}
};
onInputBlur = (e) => {
this.toggleMode();
this.updateTagName(e);
};
render() {
const { isEditing, tagName } = this.state;
return (
<div className="mx-2 flex-fill d-flex">
{isEditing ?
<input
type="text"
ref={this.input}
defaultValue={tagName}
onBlur={this.onInputBlur}
onKeyDown={this.onInputKeyDown}
className="flex-fill form-control-sm form-control"
/> :
<span
onClick={this.toggleMode}
className="cursor-pointer flex-fill"
style={{ width: 100, height: 20 }}
>{tagName}
</span>
}
</div>
);
}
}

View File

@ -0,0 +1,36 @@
import React from 'react';
import { Button } from 'reactstrap';
import { gettext } from '../utils/constants';
import { useMetadataStatus } from '../hooks';
import { eventBus } from '../components/common/event-bus';
import { EVENT_BUS_TYPE } from '../components/common/event-bus-type';
const RepoInfoBarMigrate = () => {
const { enableMetadata } = useMetadataStatus();
const openMigrate = () => {
eventBus.dispatch(EVENT_BUS_TYPE.OPEN_TREE_PANEL, () => eventBus.dispatch(EVENT_BUS_TYPE.OPEN_LIBRARY_SETTINGS_TAGS));
};
return (
<div className="repo-info-bar-migrate mt-2">
{enableMetadata ?
(
<>
{gettext('Tips: There are tags of old version. Please migrate tags to new version.')}
<Button color="link" size="sm" tag="a" onClick={openMigrate}>{gettext('Migrate')}</Button>
</>
) :
(
<>{gettext('Tips: These are tags of old version. The feature is deprecated and can no longer be used.')}</>
)
}
</div>
);
};
RepoInfoBarMigrate.propTypes = {
};
export default RepoInfoBarMigrate;

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ModalPortal from './modal-portal';
import ListTaggedFilesDialog from './dialog/list-taggedfiles-dialog';
import RepoInfoBarMigrate from './repo-info-bar-migrate';
import '../css/repo-info-bar.css';
@ -39,7 +40,7 @@ class RepoInfoBar extends React.Component {
};
render() {
let { repoID, usedRepoTags, className } = this.props;
let { repoID, usedRepoTags, className, shareLinkToken } = this.props;
return (
<div className={`repo-info-bar ${className ? className : ''}`}>
@ -67,11 +68,12 @@ class RepoInfoBar extends React.Component {
toggleCancel={this.onListTaggedFiles}
updateUsedRepoTags={this.props.updateUsedRepoTags}
onFileTagChanged={this.props.onFileTagChanged}
shareLinkToken={this.props.shareLinkToken}
shareLinkToken={shareLinkToken}
enableFileDownload={this.props.enableFileDownload}
/>
</ModalPortal>
)}
{!shareLinkToken && <RepoInfoBarMigrate />}
</div>
);
}

View File

@ -15,7 +15,7 @@
.wiki-page-container .article .ml-2:hover {
text-decoration: underline;
color: #eb8205;
color: #EC8000;
}
#wiki-page-last-modified {

View File

@ -0,0 +1,152 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import isHotkey from 'is-hotkey';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import UserItem from './user-item';
import { seafileAPI } from '../../../utils/seafile-api';
import ModalPortal from '../../modal-portal';
import toaster from '../../toast';
import { SEARCH_FILTERS_KEY } from '../../../constants';
const FilterByCreator = ({ creatorList, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const [options, setOptions] = useState([]);
const [selectedOptions, setSelectedOptions] = useState(creatorList || []);
const [searchValue, setSearchValue] = useState('');
const [inputFocus, setInputFocus] = useState(false);
const toggle = useCallback((e) => {
setIsOpen(!isOpen);
}, [isOpen]);
const displayOptions = useMemo(() => {
if (!searchValue) return null;
return options.filter((option) => {
return option.name.toLowerCase().includes(searchValue.toLowerCase());
});
}, [options, searchValue]);
const onChangeOption = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const name = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
let updated = [...selectedOptions];
if (!updated.some((item) => item.name === name)) {
const newOption = options.find((option) => option.name === name);
updated = [...updated, newOption];
} else {
updated = updated.filter((option) => option.name !== name);
}
setSelectedOptions(updated);
onChange(SEARCH_FILTERS_KEY.CREATOR_LIST, updated);
if (displayOptions.length === 1) {
setSearchValue('');
}
}, [selectedOptions, displayOptions, options, onChange]);
const handleCancel = useCallback((e, name) => {
const updated = selectedOptions.filter((option) => option.name !== name);
setSelectedOptions(updated);
onChange(SEARCH_FILTERS_KEY.CREATOR_LIST, updated);
}, [selectedOptions, onChange]);
const handleInputChange = useCallback((e) => {
const v = e.target.value;
setSearchValue(v);
if (!selectedOptions) {
setOptions([]);
}
}, [selectedOptions]);
const handleInputKeyDown = useCallback((e) => {
if (isHotkey('enter')(e)) {
e.preventDefault();
e.stopPropagation();
setSearchValue('');
toggle();
}
}, [toggle]);
useEffect(() => {
if (!searchValue) return;
const getUsers = async () => {
try {
const res = await seafileAPI.searchUsers(searchValue);
const userList = res.data.users
.filter(user => user.name.toLowerCase().includes(searchValue.toLowerCase()));
setOptions(userList);
} catch (err) {
toaster.danger(Utils.getErrorMsg(err));
}
};
getUsers();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue]);
return (
<div className="search-filter filter-by-creator-container">
<Dropdown isOpen={isOpen} toggle={toggle}>
<DropdownToggle tag="div" className={classNames('search-filter-toggle', {
'active': isOpen && selectedOptions.length > 0,
'highlighted': selectedOptions.length > 0,
})}>
<div className="filter-label" title={gettext('Creator')}>{gettext('Creator')}</div>
<i className="sf3-font sf3-font-down sf3-font pl-1" />
</DropdownToggle>
<ModalPortal>
<DropdownMenu className="search-filter-menu filter-by-creator-menu">
<div className={classNames('input-container', { 'focus': inputFocus })}>
{selectedOptions.map((option) => (
<UserItem
key={option.name}
user={option}
isCancellable={true}
onCancel={handleCancel}
/>
))}
<div className="search-input-wrapper">
<input
type="text"
placeholder={selectedOptions.length ? '' : gettext('Search user')}
value={searchValue}
autoFocus
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
onFocus={() => setInputFocus(true)}
onBlur={() => setInputFocus(false)}
/>
</div>
</div>
{displayOptions && displayOptions.map((option) => (
<DropdownItem
key={option.name}
tag="div"
tabIndex="-1"
data-toggle={option.name}
onMouseDown={(e) => e.preventDefault()}
onClick={onChangeOption}
toggle={false}
>
{isOpen && <UserItem user={option} />}
{selectedOptions.includes(option.name) && <i className="dropdown-item-tick sf2-icon-tick"></i>}
</DropdownItem>
))}
</DropdownMenu>
</ModalPortal>
</Dropdown>
</div>
);
};
FilterByCreator.propTypes = {
creatorList: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
};
export default FilterByCreator;

View File

@ -0,0 +1,272 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import dayjs from 'dayjs';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import Picker from '../../date-and-time-picker';
import ModalPortal from '../../modal-portal';
import { SEARCH_FILTERS_KEY, SEARCH_FILTER_BY_DATE_OPTION_KEY, SEARCH_FILTER_BY_DATE_TYPE_KEY } from '../../../constants';
import classNames from 'classnames';
const DATE_INPUT_WIDTH = 118;
const FilterByDate = ({ date, onChange }) => {
const [value, setValue] = useState(date.value);
const [isOpen, setIsOpen] = useState(false);
const [isTypeOpen, setIsTypeOpen] = useState(false);
const [isCustomDate, setIsCustomDate] = useState(date.value === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM);
const [time, setTime] = useState({
from: date.from,
to: date.to,
});
const [type, setType] = useState(date.type);
const typeLabel = useMemo(() => {
switch (type) {
case SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME:
return gettext('Create time');
case SEARCH_FILTER_BY_DATE_TYPE_KEY.LAST_MODIFIED_TIME:
return gettext('Last modified time');
default:
return gettext('Create time');
}
}, [type]);
const label = useMemo(() => {
if (!value || value.length === 0) return gettext('Date');
return typeLabel;
}, [typeLabel, value]);
const typeOptions = useMemo(() => {
return [
{
key: SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME,
label: gettext('Create time'),
}, {
key: SEARCH_FILTER_BY_DATE_TYPE_KEY.LAST_MODIFIED_TIME,
label: gettext('Last modified time'),
}
];
}, []);
const options = useMemo(() => {
return [
{
key: SEARCH_FILTER_BY_DATE_OPTION_KEY.TODAY,
label: gettext('Today'),
}, {
key: SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_7_DAYS,
label: gettext('Last 7 days'),
}, {
key: SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_30_DAYS,
label: gettext('Last 30 days'),
},
'Divider',
{
key: SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM,
label: gettext('Custom time'),
},
];
}, []);
const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]);
const toggleType = useCallback(() => setIsTypeOpen(!isTypeOpen), [isTypeOpen]);
const onChangeType = useCallback((e) => {
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
if (option === type) return;
setType(option);
}, [type]);
const onClearDate = useCallback(() => {
setValue('');
setIsCustomDate(false);
setTime({
from: null,
to: null,
});
setIsOpen(false);
}, []);
const onOptionClick = useCallback((e) => {
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
if (option === value) return;
const today = dayjs().endOf('day');
const isCustomOption = option === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM;
setIsCustomDate(isCustomOption);
setValue(option);
setIsOpen(isCustomOption);
switch (option) {
case SEARCH_FILTER_BY_DATE_OPTION_KEY.TODAY: {
setTime({
from: dayjs().startOf('day').unix(),
to: today.unix()
});
break;
}
case SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_7_DAYS: {
setTime({
from: dayjs().subtract(6, 'day').startOf('day').unix(),
to: today.unix()
});
break;
}
case SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_30_DAYS: {
setTime({
from: dayjs().subtract(30, 'day').startOf('day').unix(),
to: today.unix()
});
break;
}
case SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM: {
setTime({
from: null,
to: null,
});
break;
}
}
}, [value]);
const disabledStartDate = useCallback((startDate) => {
if (!startDate) return false;
const today = dayjs();
const endValue = time.to;
if (!endValue) {
return startDate.isAfter(today);
}
return endValue.isBefore(startDate) || startDate.isAfter(today);
}, [time]);
const disabledEndDate = useCallback((endDate) => {
if (!endDate) return false;
const today = dayjs();
const startValue = time.from;
if (!startValue) {
return endDate.isAfter(today);
}
return endDate.isBefore(startValue) || endDate.isAfter(today);
}, [time]);
useEffect(() => {
if (!isOpen) {
if (type !== date.type || time.from !== date.from || time.to !== date.to) {
onChange(SEARCH_FILTERS_KEY.DATE, {
type,
value,
from: time.from,
to: time.to,
});
}
}
}, [isOpen, date, time, type, value, onChange]);
return (
<div className="search-filter filter-by-date-container">
<Dropdown isOpen={isOpen} toggle={toggle}>
<DropdownToggle tag="div" className={classNames('search-filter-toggle', {
'active': isOpen && value,
'highlighted': value,
})} onClick={toggle}>
<div className="filter-label" style={{ maxWidth: 300 }} title={label}>{label}</div>
<i
className="sf3-font sf3-font-down pl-1"
onClick={(e) => {
e.stopPropagation();
toggle();
}}
/>
</DropdownToggle>
<ModalPortal>
<DropdownMenu className="search-filter-menu filter-by-date-menu">
<div className="filter-by-date-menu-toolbar">
<Dropdown isOpen={isTypeOpen} toggle={toggleType}>
<DropdownToggle tag="div" className="search-filter-toggle filter-by-date-type-toggle">
<div className="filter-label">{typeLabel}</div>
<i
className="sf3-font sf3-font-down pl-1"
onClick={(e) => {
e.stopPropagation();
toggleType();
}}
/>
</DropdownToggle>
<DropdownMenu>
{typeOptions.map((option) => {
const isSelected = option.key === type;
return (
<DropdownItem key={option.key} data-toggle={option.key} onClick={onChangeType}>
{option.label}
{isSelected && <i className="dropdown-item-tick sf2-icon-tick"></i>}
</DropdownItem>
);
})}
</DropdownMenu>
</Dropdown>
<div className="delete-btn" onClick={onClearDate}>
<i className="op-icon sf3-font-delete1 sf3-font"></i>
</div>
</div>
{options.map((option, i) => {
const isSelected = option.key === value;
if (option === 'Divider') return <div key={i} className="seafile-divider dropdown-divider"></div>;
return (
<DropdownItem
key={option.key}
tag="div"
tabIndex="-1"
data-toggle={option.key}
onMouseDown={(e) => e.preventDefault()}
onClick={onOptionClick}
toggle={false}
>
{option.label}
{isSelected && <i className="dropdown-item-tick sf2-icon-tick"></i>}
</DropdownItem>
);
})}
{isCustomDate && (
<div className="filter-by-date-custom-date-container">
<div className="custom-date-container">
<div className="custom-date-label">{gettext('Start date')}</div>
<Picker
showHourAndMinute={false}
disabledDate={disabledStartDate}
value={time.from}
onChange={(value) => setTime({ ...time, from: value })}
inputWidth={DATE_INPUT_WIDTH}
/>
</div>
<div className="custom-date-container">
<div className="custom-date-label">{gettext('End date')}</div>
<Picker
showHourAndMinute={false}
disabledDate={disabledEndDate}
value={time.to}
onChange={(value) => setTime({ ...time, to: value })}
inputWidth={DATE_INPUT_WIDTH}
/>
</div>
</div>
)}
</DropdownMenu>
</ModalPortal>
</Dropdown>
</div>
);
};
FilterByDate.propTypes = {
date: PropTypes.shape({
type: PropTypes.string,
value: PropTypes.string,
start: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
end: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
}),
onChange: PropTypes.func.isRequired,
};
export default FilterByDate;

View File

@ -0,0 +1,81 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { gettext } from '../../../utils/constants';
import ModalPortal from '../../modal-portal';
import { SEARCH_FILTERS_KEY } from '../../../constants';
const FilterBySuffix = ({ suffixes, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState(suffixes);
const inputRef = useRef(null);
const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]);
const handleInput = useCallback((e) => {
setInputValue(e.target.value);
}, []);
const handleKeyDown = useCallback((e) => {
e.stopPropagation();
if (e.key === 'Enter') {
setIsOpen(false);
}
}, []);
const handleClearInput = useCallback(() => {
setInputValue('');
setIsOpen(false);
}, []);
useEffect(() => {
if (!isOpen && inputValue !== suffixes) {
onChange(SEARCH_FILTERS_KEY.SUFFIXES, inputValue.replace(/\./g, ''));
}
}, [isOpen, inputValue, suffixes, onChange]);
return (
<div className="search-filter filter-by-suffix-container">
<Dropdown isOpen={isOpen} toggle={toggle}>
<DropdownToggle tag="div" className={classNames('search-filter-toggle', {
'active': isOpen && inputValue.length > 0,
'highlighted': inputValue.length > 0,
})} onClick={toggle}>
<div className="filter-label" title={gettext('File suffix')}>{gettext('File suffix')}</div>
<i className="sf3-font sf3-font-down pl-1" />
</DropdownToggle>
<ModalPortal>
<DropdownMenu className="search-filter-menu filter-by-suffix-menu p-4">
<input
ref={inputRef}
type="text"
className="form-control"
placeholder={gettext('Seperate multiple suffixes by ","(like sdoc, pdf)')}
value={inputValue}
autoFocus
onChange={handleInput}
onKeyDown={handleKeyDown}
/>
{inputValue.length > 0 && (
<button
type="button"
className="clear-icon-right sf3-font sf3-font-x-01"
onClick={handleClearInput}
aria-label={gettext('Clear')}
>
</button>
)}
</DropdownMenu>
</ModalPortal>
</Dropdown>
</div>
);
};
FilterBySuffix.propTypes = {
suffixes: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
export default FilterBySuffix;

View File

@ -0,0 +1,65 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import PropTypes from 'prop-types';
import ModalPortal from '../../../components/modal-portal';
import { Utils } from '../../../utils/utils';
import { gettext } from '../../../utils/constants';
import { SEARCH_FILTERS_KEY } from '../../../constants';
const FilterByText = ({ searchFilenameOnly, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState(searchFilenameOnly ? SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY : SEARCH_FILTERS_KEY.SEARCH_FILENAME_AND_CONTENT);
const options = useMemo(() => {
return [
{
key: SEARCH_FILTERS_KEY.SEARCH_FILENAME_AND_CONTENT,
label: gettext('File name and content'),
}, {
key: SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY,
label: gettext('File name only'),
}
];
}, []);
const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]);
const onOptionClick = useCallback((e) => {
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
setValue(option);
const isSearchFilenameOnly = option === SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY;
onChange(SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY, isSearchFilenameOnly);
}, [onChange]);
const label = options.find((option) => option.key === value).label;
return (
<div className="search-filter filter-by-text-container">
<Dropdown isOpen={isOpen} toggle={toggle}>
<DropdownToggle tag="div" className="search-filter-toggle">
<div className="filter-label" title={label}>{label}</div>
<i className="sf3-font sf3-font-down sf3-font pl-1" />
</DropdownToggle>
<ModalPortal>
<DropdownMenu className="search-filter-menu filter-by-text-menu">
{options.map((option) => {
const isSelected = option.key === value;
return (
<DropdownItem key={option.key} data-toggle={option.key} onClick={onOptionClick}>
{option.label}
{isSelected && <i className="dropdown-item-tick sf2-icon-tick"></i>}
</DropdownItem>
);
})}
</DropdownMenu>
</ModalPortal>
</Dropdown>
</div>
);
};
FilterByText.propTypes = {
searchFilenameOnly: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
};
export default FilterByText;

View File

@ -0,0 +1,233 @@
.search-filters-container {
min-height: 32px;
position: relative;
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 12px;
padding: 0 16px 10px;
overflow: auto hidden;
scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0);
border-bottom: 1px solid #eee;
}
.search-filters-container .search-filter {
width: fit-content;
display: flex;
align-items: center;
margin-right: 8px;
}
.search-filters-container .search-filter .search-filter-toggle,
.search-filter-menu .search-filter-toggle {
display: flex;
align-items: center;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
}
.search-filters-container .search-filter .search-filter-toggle:hover,
.search-filter-menu .search-filter-toggle:hover {
background-color: #efefef;
}
.search-filter-toggle .sf3-font-down {
color: #666;
}
.search-filters-container .search-filter .filter-label {
width: fit-content;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-filters-container .search-filter.filter-by-suffix-container .filter-label,
.search-filters-container .search-filter.filter-by-creator-container .filter-label {
max-width: 120px;
}
.search-filters-container .dropdown-menu {
max-height: 400px;
}
.search-filter-menu.filter-by-text-menu,
.search-filter-menu.filter-by-date-menu,
.search-filter-menu.filter-by-creator-menu {
width: 280px;
}
.search-filter-menu.filter-by-suffix-menu {
width: 400px;
position: relative;
}
.search-filter-menu.filter-by-suffix-menu .clear-icon-right {
width: 20px;
height: 20px;
position: absolute;
top: 16px;
right: 16px;
display: flex;
justify-content: center;
align-items: center;
color: #666;
margin: 9px;
border: 0;
border-radius: 3px;
background-color: transparent;
padding: 0;
}
.search-filter-menu.filter-by-suffix-menu .clear-icon-right:hover {
background-color: #efefef;
}
.search-filters-container .search-filters-dropdown-item {
width: 100%;
}
.search-filter-menu {
z-index: 1050;
}
.search-filter-menu .dropdown-item {
position: relative;
display: flex;
align-items: center;
}
.search-filter-menu .dropdown-item .dropdown-item-tick {
width: 1rem;
left: auto;
right: 16px;
}
.search-filter-menu .input-container {
position: relative;
border: 1px solid #eaeaea;
border-radius: 4px;
padding: 4px 6px;
margin: 8px 16px;
min-height: 28px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.search-filter-menu .input-container.focus {
background-color: #fff;
border-color: #1991eb;
box-shadow: 0 0 0 2px rgba(70, 127, 207, .25);
color: #495057;
outline: 0;
}
.search-filter-menu .input-container .search-input-wrapper {
display: flex;
align-items: center;
border: none;
padding: 0px;
width: auto;
background: transparent;
font-size: inherit;
line-height: 20px;
flex: 1 1 60px;
min-width: 60px;
border-radius: 4px;
}
.search-filter-menu .input-container input {
border: none;
outline: none;
width: 100%;
display: block;
resize: none;
padding: 0px;
height: 20px;
}
.search-filter-menu .user-item {
height: 20px;
position: relative;
display: inline-flex;
align-items: center;
justify-content: flex-start;
}
.search-filter-menu .user-item .user-avatar {
width: 16px;
height: 16px;
margin-right: 4px;
border-radius: 50%;
}
.search-filter-menu .input-container .user-item {
background-color: #eaeaea;
border-radius: 10px;
padding: 0 4px 0 2px;
margin-right: 2px;
font-size: 13px;
}
.filter-by-date-menu .filter-by-date-menu-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 12px;
}
.filter-by-date-menu .filter-by-date-menu-toolbar .filter-by-date-type-toggle {
display: flex;
align-items: center;
}
.filter-by-date-menu .filter-by-date-menu-toolbar .filter-by-date-type-toggle .filter-label {
color: #7d7d7d;
}
.filter-by-date-menu .filter-by-date-menu-toolbar .delete-btn {
display: flex;
align-items: center;
justify-content: center;
margin-right: 4px;
cursor: pointer;
}
.filter-by-date-menu .filter-by-date-custom-date-container {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 16px;
gap: 8px;
}
.filter-by-date-menu .filter-by-date-custom-date-container .custom-date-container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
font-size: 14px;
}
.filter-by-date-menu .filter-by-date-custom-date-container .custom-date-container .custom-date-label {
margin-bottom: 6px;
}
.filter-by-date-menu .filter-by-date-custom-date-container .custom-date-container .form-control {
height: 28px;
font-size: 14px;
}
.search-filters-container .search-filter-toggle.active:hover,
.search-filters-container .search-filter-toggle.highlighted:hover {
background-color: rgba(255, 152, 0, 0.2);
}
.search-filters-container .search-filter-toggle.highlighted,
.search-filter-toggle.highlighted .sf3-font-down {
color: #ff9800;
}

View File

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import FilterByText from './filter-by-text';
import FilterByCreator from './filter-by-creator';
import FilterByDate from './filter-by-date';
import FilterBySuffix from './filter-by-suffix';
import './index.css';
const SearchFilters = ({ filters, onChange }) => {
return (
<div className="search-filters-container">
<FilterBySuffix suffixes={filters.suffixes} onChange={onChange} />
<FilterByText searchFilenameOnly={filters.search_filename_only} onChange={onChange} />
<FilterByCreator creatorList={filters.creator_list} onChange={onChange} />
<FilterByDate date={filters.date} onChange={onChange} />
</div>
);
};
SearchFilters.propTypes = {
filters: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
};
export default SearchFilters;

View File

@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import { mediaUrl } from '../../../utils/constants';
import IconBtn from '../../icon-btn';
const UserItem = ({ user, isCancellable, onCancel }) => {
return (
<div className="user-item">
<img src={user.avatar_url || `${mediaUrl}avatars/default.png`} alt={user.name} className="user-avatar" />
<span className="user-name">{user.name}</span>
{isCancellable && <IconBtn className="user-remove" onClick={(e) => onCancel(e, user.name)} symbol="x-01" />}
</div>
);
};
UserItem.propTypes = {
user: PropTypes.object.isRequired,
isCancellable: PropTypes.bool,
onCancel: PropTypes.func,
};
export default UserItem;

View File

@ -0,0 +1,58 @@
.search-tags-container {
width: 100%;
height: fit-content;
display: flex;
flex-direction: column;
justify-content: flex-start;
font-size: 0.8125rem;
}
.search-tags-container .tags-title {
width: 100%;
height: 20px;
display: flex;
flex-direction: row;
justify-content: flex-start;
padding: 0 16px;
margin: 10px 0 4px;
font-size: 0.875rem;
color: #999;
}
.search-tags-container .tags-content {
width: 100%;
height: fit-content;
max-height: 180px;
overflow: auto;
scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0);
padding: 0 16px;
margin-bottom: 10px;
}
.search-tags-container .tags-content .tag-item {
width: 100%;
height: 40px;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0 16px;
border-radius: 3px;
}
.search-tags-container .tags-content .tag-item:hover {
background-color: #f0f0f0;
cursor: pointer;
}
.search-tags-container .tags-content .tag-item .tag-color {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.search-tags-container .search-tags-divider {
height: 0;
border-top: 1px solid #eee;
margin: 0 16px;
}

View File

@ -0,0 +1,70 @@
import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../../utils/constants';
import { getTagColor, getTagId, getTagName } from '../../../tag/utils/cell';
import { PRIVATE_FILE_TYPE } from '../../../constants';
import { EVENT_BUS_TYPE } from '../../common/event-bus-type';
import './index.css';
const SearchTags = ({ repoID, tagsData, keyword, onSelectTag }) => {
const [displayTags, setDisplayTags] = useState([]);
const handleClick = useCallback((e, tagId) => {
e.preventDefault();
e.stopPropagation();
const node = {
children: [],
path: '/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/' + tagId,
isExpanded: false,
isLoaded: true,
isPreload: true,
object: {
file_tags: [],
id: tagId,
type: PRIVATE_FILE_TYPE.TAGS_PROPERTIES,
isDir: () => false,
},
parentNode: {},
key: repoID,
tag_id: tagId,
};
onSelectTag(node);
window.sfTagsDataContext?.eventBus?.dispatch(EVENT_BUS_TYPE.UPDATE_SELECTED_TAG, tagId);
}, [repoID, onSelectTag]);
useEffect(() => {
if (!tagsData || tagsData.length === 0 || !keyword) return;
const tags = tagsData?.filter((tag) => getTagName(tag).toLowerCase().includes(keyword.toLowerCase()));
setDisplayTags(tags);
}, [tagsData, keyword]);
if (!tagsData || tagsData.length === 0 || !keyword || displayTags.length === 0) return null;
return (
<div className="search-tags-container">
<div className="tags-title">{gettext('Tags')}</div>
<div className="tags-content">
{displayTags.map((tag) => {
const tagId = getTagId(tag);
const tagName = getTagName(tag);
const tagColor = getTagColor(tag);
return (
<div className="tag-item" key={tagId} onClick={(e) => handleClick(e, tagId)}>
<div className="tag-color" style={{ backgroundColor: tagColor }} />
<div className="tag-name">{tagName}</div>
</div>
);
})}
</div>
<div className="search-tags-divider" />
</div>
);
};
SearchTags.propTypes = {
tagsData: PropTypes.array.isRequired,
};
export default SearchTags;

View File

@ -12,7 +12,10 @@ import { Utils } from '../../utils/utils';
import toaster from '../toast';
import Loading from '../loading';
import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes';
import { PRIVATE_FILE_TYPE } from '../../constants';
import { PRIVATE_FILE_TYPE, SEARCH_FILTER_BY_DATE_OPTION_KEY, SEARCH_FILTER_BY_DATE_TYPE_KEY, SEARCH_FILTERS_KEY, SEARCH_FILTERS_SHOW_KEY } from '../../constants';
import SearchFilters from './search-filters';
import SearchTags from './search-tags';
import IconBtn from '../icon-btn';
const propTypes = {
repoID: PropTypes.string,
@ -21,6 +24,7 @@ const propTypes = {
onSearchedClick: PropTypes.func.isRequired,
isPublic: PropTypes.bool,
isViewFile: PropTypes.bool,
onSelectTag: PropTypes.func,
};
const PER_PAGE = 20;
@ -49,6 +53,19 @@ class Search extends Component {
isSearchInputShow: false, // for mobile
searchTypesMax: 0,
highlightSearchTypesIndex: 0,
isFiltersShow: true,
isFilterControllerActive: false,
filters: {
search_filename_only: false,
creator_list: [],
date: {
type: SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME,
value: '',
from: null,
to: null,
},
suffixes: '',
},
};
this.highlightRef = null;
this.source = null; // used to cancel request;
@ -64,6 +81,8 @@ class Search extends Component {
document.addEventListener('keydown', this.onDocumentKeydown);
document.addEventListener('compositionstart', this.onCompositionStart);
document.addEventListener('compositionend', this.onCompositionEnd);
const isFiltersShow = localStorage.getItem(SEARCH_FILTERS_SHOW_KEY) === 'true';
this.setState({ isFiltersShow });
}
UNSAFE_componentWillReceiveProps(nextProps) {
@ -119,7 +138,7 @@ class Search extends Component {
};
onFocusHandler = () => {
this.setState({ width: '570px', isMaskShow: true, isCloseShow: true });
this.setState({ width: '570px', isMaskShow: true });
this.calculateHighlightType();
};
@ -355,7 +374,7 @@ class Search extends Component {
if (this.state.showRecent) {
this.setState({ showRecent: false });
}
this.setState({ value: newValue });
this.setState({ value: newValue, isCloseShow: newValue.length > 0 });
setTimeout(() => {
const trimmedValue = newValue.trim();
const isInRepo = this.props.repoID;
@ -415,7 +434,7 @@ class Search extends Component {
this.queryData = queryData;
if (isPublic) {
seafileAPI.searchFilesInPublishedRepo(queryData.search_repo, queryData.q, page, PER_PAGE).then(res => {
seafileAPI.searchFilesInPublishedRepo(queryData.search_repo, queryData.q, page, PER_PAGE, queryData.search_filename_only).then(res => {
this.source = null;
if (res.data.total > 0) {
this.setState({
@ -483,6 +502,8 @@ class Search extends Component {
items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1);
items[i]['content'] = data[i].content_highlight;
items[i]['thumbnail_url'] = data[i].thumbnail_url;
items[i]['mtime'] = data[i].mtime || '';
items[i]['repo_owner_email'] = data[i].repo_owner_email || '';
}
return items;
}
@ -499,6 +520,18 @@ class Search extends Component {
highlightIndex: 0,
isSearchInputShow: false,
showRecent: true,
isFilterControllerActive: false,
filters: {
search_filename_only: false,
creator_list: [],
date: {
type: SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME,
value: '',
start: null,
end: null,
},
suffixes: '',
}
});
}
@ -510,6 +543,7 @@ class Search extends Component {
resultItems: [],
highlightIndex: 0,
isSearchInputShow: false,
isCloseShow: false,
});
}
@ -524,6 +558,7 @@ class Search extends Component {
}
}
const filteredItems = this.filterByCreator(resultItems);
if (isLoading) {
return <Loading />;
}
@ -533,8 +568,8 @@ class Search extends Component {
else if (!isResultGotten) {
return this.renderSearchTypes(this.state.inputValue.trim());
}
else if (resultItems.length > 0) {
return this.renderResults(resultItems);
else if (filteredItems.length > 0) {
return this.renderResults(filteredItems);
}
else {
return <div className="search-result-none">{gettext('No results matching')}</div>;
@ -587,7 +622,8 @@ class Search extends Component {
if (this.props.repoID) {
const { path } = this.props;
const isMetadataView = path && path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES);
if (path && path !== '/' && !this.props.isViewFile && !isMetadataView) {
const isTagView = path && path.startsWith('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES);
if (path && path !== '/' && !this.props.isViewFile && !isMetadataView && !isTagView) {
return (
<div className="search-types">
<div className={`search-types-repo ${highlightIndex === 0 ? 'search-types-highlight' : ''}`} onClick={this.searchRepo} tabIndex={0}>
@ -638,7 +674,7 @@ class Search extends Component {
search_repo: this.props.repoID,
search_ftypes: 'all',
};
this.getSearchResult(queryData);
this.getSearchResult(this.buildSearchParams(queryData));
};
searchFolder = () => {
@ -649,7 +685,7 @@ class Search extends Component {
search_ftypes: 'all',
search_path: this.props.path,
};
this.getSearchResult(queryData);
this.getSearchResult(this.buildSearchParams(queryData));
};
searchAllRepos = () => {
@ -659,7 +695,7 @@ class Search extends Component {
search_repo: 'all',
search_ftypes: 'all',
};
this.getSearchResult(queryData);
this.getSearchResult(this.buildSearchParams(queryData));
};
renderResults = (resultItems, isVisited) => {
@ -688,6 +724,7 @@ class Search extends Component {
return (
<>
<MediaQuery query="(min-width: 768px)">
{!isVisited && <h4 className="search-results-title">{gettext('Files')}</h4>}
<div className="search-result-list-container" ref={this.searchResultListContainerRef}>{results}</div>
</MediaQuery>
<MediaQuery query="(max-width: 767.8px)">
@ -704,11 +741,97 @@ class Search extends Component {
});
};
handleFiltersShow = () => {
const { isFiltersShow } = this.state;
localStorage.setItem(SEARCH_FILTERS_SHOW_KEY, !isFiltersShow);
this.setState({ isFiltersShow: !isFiltersShow });
}
buildSearchParams = (baseParams) => {
const { filters } = this.state;
const params = { ...baseParams };
if (filters.search_filename_only) {
params.search_filename_only = filters.search_filename_only;
}
if (filters.date.value) {
const isCustom = filters.date.value === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM;
params.time_from = isCustom ? filters.date.start?.unix() : filters.date.from;
params.time_to = isCustom ? filters.date.end?.unix() : filters.date.to;
}
if (filters.suffixes) {
params.input_fexts = filters.suffixes;
params.search_ftypes = 'custom';
}
if (filters.creator_list.length > 0) {
params.creator_emails = filters.creator_list.map(c => c.email).join(',');
}
return params;
};
handleFiltersChange = (key, value) => {
const newFilters = { ...this.state.filters, [key]: value };
const hasActiveFilter = newFilters.suffixes || newFilters.creator_list.length > 0 || newFilters.date.value;
this.setState({ filters: newFilters, isFilterControllerActive: hasActiveFilter });
// build query data
if (!this.state.value || !this.state.isResultGotten) return;
const queryUpdates = {};
if (key === SEARCH_FILTERS_KEY.CREATOR_LIST) return;
if (key === SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY) {
queryUpdates.search_filename_only = value;
}
if (key === SEARCH_FILTERS_KEY.SUFFIXES) {
queryUpdates.search_ftypes = 'custom';
queryUpdates.input_fexts = value;
if (!value) {
queryUpdates.search_ftypes = 'all';
}
}
if (key === SEARCH_FILTERS_KEY.DATE) {
const date = value;
const isCustom = date.value === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM;
queryUpdates.time_from = isCustom ? value.from?.unix() : value.from;
queryUpdates.time_to = isCustom ? value.to?.unix() : value.to;
}
const newQueryData = {
...this.queryData,
...queryUpdates,
};
this.getSearchResult(newQueryData);
}
filterByCreator = (results) => {
const { filters } = this.state;
return results.filter(item => {
if (filters.creator_list && filters.creator_list.length > 0) {
if (!filters.creator_list.some(creator => creator.email === item.repo_owner_email)) {
return false;
}
}
return true;
});
};
handleSelectTag = (tag) => {
this.props.onSelectTag(tag);
this.resetToDefault();
}
render() {
let width = this.state.width !== 'default' ? this.state.width : '';
let style = {'width': width};
const { isMaskShow } = this.state;
const { repoID, isTagEnabled, tagsData } = this.props;
const { isMaskShow, isResultGotten, isCloseShow, isFiltersShow, isFilterControllerActive, filters } = this.state;
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + k)`}`;
const isTagsShow = this.props.repoID && isTagEnabled && isMaskShow && isResultGotten;
return (
<Fragment>
<MediaQuery query="(min-width: 768px)">
@ -729,15 +852,33 @@ class Search extends Component {
autoComplete="off"
ref={this.inputRef}
/>
{this.state.isCloseShow &&
{isCloseShow &&
<button
type="button"
className="search-icon-right input-icon-addon sf3-font sf3-font-x-01"
className="search-icon-right sf3-font sf3-font-x-01"
onClick={this.onClearSearch}
aria-label={gettext('Clear search')}
></button>
}
{isMaskShow && (
<IconBtn
symbol="filter-circled"
size={20}
className={classnames('search-icon-right input-icon-addon search-filter-controller', { 'active': isFilterControllerActive })}
onClick={this.handleFiltersShow}
title={isFiltersShow ? gettext('Hide advanced search') : gettext('Show advanced search')}
aria-label={isFiltersShow ? gettext('Hide advanced search') : gettext('Show advanced search')}
tabIndex={0}
id="search-filter-controller"
/>
)}
</div>
{isMaskShow && isFiltersShow &&
<SearchFilters filters={filters} onChange={this.handleFiltersChange} />
}
{isTagsShow &&
<SearchTags repoID={repoID} tagsData={tagsData} keyword={this.state.value} onSelectTag={this.handleSelectTag} />
}
<div
className="search-result-container dropdown-search-result-container"
ref={this.searchContainer}

View File

@ -8,7 +8,7 @@ import DeleteTags from './delete-tags';
import { Utils } from '../../../../utils/utils';
import { KeyCodes } from '../../../../constants';
import { gettext } from '../../../../utils/constants';
import { getTagColor, getTagId, getTagName, getTagsByNameOrColor } from '../../../../tag/utils/cell';
import { getTagColor, getTagId, getTagName, getTagsByName } from '../../../../tag/utils/cell';
import { getRecordIdFromRecord } from '../../../../metadata/utils/cell';
import { SELECT_OPTION_COLORS } from '../../../../metadata/constants';
import { getRowById } from '../../utils/table';
@ -38,7 +38,7 @@ const TagsEditor = ({
const editorRef = useRef(null);
const selectItemRef = useRef(null);
const displayTags = useMemo(() => getTagsByNameOrColor(allTagsRef.current, searchValue), [searchValue, allTagsRef]);
const displayTags = useMemo(() => getTagsByName(allTagsRef.current, searchValue), [searchValue, allTagsRef]);
const isShowCreateBtn = useMemo(() => {
if (!canAddTag || !searchValue || !Utils.isFunction(addNewTag)) return false;

View File

@ -38,6 +38,11 @@
z-index: 1;
}
.sf-table-result-container.windows-browser::-webkit-scrollbar,
.sf-table-canvas::-webkit-scrollbar {
display: none;
}
.sf-table-result-container.record-draggable .sf-table-row:hover .drag-handler {
background-image: url(../../../../../../../media/img/grippy_large.png);
background-repeat: no-repeat;

View File

@ -185,3 +185,16 @@ export const getTreeChildNodes = (parentNode, tree) => {
}
return childNodes;
};
export const getNodesWithAncestors = (node, tree) => {
const nodeKey = getTreeNodeKey(node);
let nodesWithAncestors = [];
tree.forEach((node, i) => {
if (!nodeKey.includes(getTreeNodeKey(node))) {
return;
}
nodesWithAncestors.push({ ...node, node_index: i });
});
return nodesWithAncestors;
};

View File

@ -56,7 +56,7 @@ class Selector extends Component {
selectItem = (e, targetItem) => {
e.stopPropagation();
if (this.props.operationBeforeSelect) {
this.props.operationBeforeSelect();
this.props.operationBeforeSelect(targetItem);
} else {
this.props.selectOption(targetItem);
}
@ -73,8 +73,8 @@ class Selector extends Component {
<div onClick={this.onToggleClick}>
{customSelectorToggle ? customSelectorToggle : (
<span className="cur-option">
{currentSelectedOption.text}
{isDropdownToggleShown && <i className="sf3-font sf3-font-down ml-2 toggle-icon"></i>}
{currentSelectedOption ? currentSelectedOption.text : ''}
{isDropdownToggleShown && <i className="sf3-font sf3-font-down ml-1 toggle-icon"></i>}
</span>
)}
</div>

View File

@ -8,6 +8,7 @@ import '../css/item-dropdown-menu.css';
const propTypes = {
sortBy: PropTypes.string,
sortOrder: PropTypes.string,
sortOptions: PropTypes.array,
onSelectSortOption: PropTypes.func.isRequired
};
@ -15,7 +16,7 @@ class SortMenu extends React.Component {
constructor(props) {
super(props);
this.sortOptions = [
this.sortOptions = this.props.sortOptions || [
{ value: 'name-asc', text: gettext('By name ascending') },
{ value: 'name-desc', text: gettext('By name descending') },
{ value: 'size-asc', text: gettext('By size ascending') },
@ -43,6 +44,7 @@ class SortMenu extends React.Component {
isSelected: item.value == `${sortBy}-${sortOrder}`
};
});
return (
<Dropdown
isOpen={isDropdownMenuOpen}

View File

@ -0,0 +1,38 @@
import React from 'react';
import { gettext } from '../utils/constants';
import { notificationAPI } from '../utils/notification-api';
import '../css/system-notification.css';
import PropTypes from 'prop-types';
class SystemUserNotificationItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isClosed: false
};
}
close = () => {
this.setState({ isClosed: true });
notificationAPI.setSysUserNotificationToSeen(this.props.notificationID);
};
render() {
if (this.state.isClosed) {
return null;
}
return (
<div id="info-bar" className="d-flex justify-content-between">
<span className="mr-3" aria-hidden="true"></span>
<p id="info-bar-info" className="m-0" dangerouslySetInnerHTML={{ __html: this.props.msg }}></p>
<button className="close sf2-icon-x1" title={gettext('Close')} aria-label={gettext('Close')} onClick={this.close}></button>
</div>
);
}
}
SystemUserNotificationItem.propTypes = {
msg: PropTypes.string.isRequired,
notificationID: PropTypes.number.isRequired,
};
export default SystemUserNotificationItem;

View File

@ -0,0 +1,43 @@
import React from 'react';
import '../css/system-notification.css';
import SystemUserNotificationItem from './system-user-notification-item';
import { notificationAPI } from '../utils/notification-api';
class SystemUserNotification extends React.Component {
constructor(props) {
super(props);
this.state = {
userNoteMsgs: []
};
}
componentDidMount() {
notificationAPI.listSysUserUnseenNotifications().then((res) => {
this.setState({
userNoteMsgs: res.data.notifications
});
});
}
render() {
let { userNoteMsgs } = this.state;
if (!userNoteMsgs) {
return null;
}
const userNoteMsgItem = userNoteMsgs.map((item, index) => {
return (
<SystemUserNotificationItem
key={index}
notificationItem={item}
msg={item.msg_format}
notificationID={item.id}
/>
);
});
return userNoteMsgItem;
}
}
export default SystemUserNotification;

Some files were not shown because too many files have changed in this diff Show More