mirror of
https://github.com/haiwen/seahub.git
synced 2025-04-27 19:05:16 +00:00
Compare commits
432 Commits
v12.0.10-p
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
ea580c6569 | ||
|
281b469b78 | ||
|
f46da268ff | ||
|
5fc9b4877c | ||
|
2006ff57d6 | ||
|
371dd4a057 | ||
|
09ea1ce72d | ||
|
11734fe181 | ||
|
52f71ffae1 | ||
|
66d923ccca | ||
|
3488ea86b0 | ||
|
29fdd70f99 | ||
|
6549f48998 | ||
|
04aec222ce | ||
|
dda7d350be | ||
|
726e34d47c | ||
|
065ecb0b1f | ||
|
0b00f08e3f | ||
|
b43ad4132b | ||
|
2ec7bc4da9 | ||
|
75cde31370 | ||
|
bffdcdfa7e | ||
|
f2952cb6fb | ||
|
f10c51c461 | ||
|
c6afca8eba | ||
|
8e2be63d3f | ||
|
444e690255 | ||
|
0cd14ab806 | ||
|
1a4d7038a9 | ||
|
bfc2812a4f | ||
|
1ba5c44839 | ||
|
45c9411973 | ||
|
36af8862b1 | ||
|
145f2d17db | ||
|
e68a442a3e | ||
|
6874c2a40c | ||
|
52e020e227 | ||
|
d7e4866fc0 | ||
|
7ff4b52005 | ||
|
1cf26c3d2c | ||
|
84e763cddf | ||
|
63f51d6d2a | ||
|
8cc5815107 | ||
|
0b8aa00f4d | ||
|
4c3cc1ae19 | ||
|
920b7fe430 | ||
|
e11dd9e34c | ||
|
f24516e88a | ||
|
38c6ea36ae | ||
|
e20902279e | ||
|
62f31eee77 | ||
|
cb05c2390e | ||
|
de09b014a0 | ||
|
d733bbccc8 | ||
|
8cb5ecf83c | ||
|
b3602c6fa5 | ||
|
d7c3b459d2 | ||
|
d988c3f0cb | ||
|
a524757a92 | ||
|
a3df5ddd0e | ||
|
b398c493c2 | ||
|
759189ae78 | ||
|
ca4aa8b0cb | ||
|
344cd865b2 | ||
|
e157f8675e | ||
|
ef8eb9137f | ||
|
c23a153818 | ||
|
31e0b24e07 | ||
|
da86a8e1b0 | ||
|
1ec93e6f5a | ||
|
6e6f49beed | ||
|
1f77db68e5 | ||
|
9249e4da17 | ||
|
7a7079ed48 | ||
|
39f12f1279 | ||
|
59c719e64f | ||
|
17a4f2c637 | ||
|
19d555880b | ||
|
8d837c8195 | ||
|
8a18ada09a | ||
|
8ebf4e7225 | ||
|
034f1e2a04 | ||
|
c9984a8319 | ||
|
05f5d13c20 | ||
|
6b51e54596 | ||
|
1134469495 | ||
|
466e8f3a40 | ||
|
885b60f566 | ||
|
272010d55f | ||
|
7fb3b29e3a | ||
|
8e36f412da | ||
|
efb1ac8286 | ||
|
c3fab488f6 | ||
|
b599bed239 | ||
|
72f7e68bdc | ||
|
3ec8c646b1 | ||
|
951698555d | ||
|
618f75ab13 | ||
|
2e5b2797b3 | ||
|
2be71dd00b | ||
|
1be01e5186 | ||
|
e67fc4a3d9 | ||
|
cb69c6662e | ||
|
53bedae485 | ||
|
a865aecb6f | ||
|
ef4fbafa04 | ||
|
a1686622b3 | ||
|
cf262f09db | ||
|
ee61bbd7c5 | ||
|
b28e97970f | ||
|
baddc4bad7 | ||
|
641eb1fca5 | ||
|
dfa86ebc45 | ||
|
ee09f7b0f8 | ||
|
538bf10d18 | ||
|
04f948f7e4 | ||
|
b58e48db0c | ||
|
b27bbf6cce | ||
|
09581a961a | ||
|
247a5b06ae | ||
|
6d32b4409b | ||
|
b27198bf02 | ||
|
acd1a4a957 | ||
|
56d4ebc785 | ||
|
894679436c | ||
|
073af84027 | ||
|
cc99ce2e90 | ||
|
e332873d8e | ||
|
75460d1d7c | ||
|
792135a224 | ||
|
f6ede9c8a7 | ||
|
6614fd20a6 | ||
|
efe9ecce29 | ||
|
d58584e0d7 | ||
|
13098287d3 | ||
|
6d9d952079 | ||
|
dd3f25e216 | ||
|
c39c7c1f34 | ||
|
700e933863 | ||
|
71da2e685e | ||
|
4ef9496557 | ||
|
2ed3b81934 | ||
|
8eb1c65ae3 | ||
|
da16173f35 | ||
|
a4835a9d7b | ||
|
beef890a47 | ||
|
7723ef6fb7 | ||
|
5eb303c76b | ||
|
c8026ddb6c | ||
|
872ae595b8 | ||
|
796600eef6 | ||
|
281a81cbd2 | ||
|
ebe1c54153 | ||
|
29c8c12fa8 | ||
|
f7aaa0bff4 | ||
|
f06981267d | ||
|
30036bf83f | ||
|
005ddb4dca | ||
|
0abb343b4b | ||
|
0666b7a303 | ||
|
3ec456fff5 | ||
|
055bd575b9 | ||
|
28fb4f4887 | ||
|
8d4377f85b | ||
|
18c72b9391 | ||
|
e872b4eff8 | ||
|
27c5d0294f | ||
|
5b63ef83f0 | ||
|
db0e17b645 | ||
|
2152dca689 | ||
|
7b60cc38aa | ||
|
213927e1a7 | ||
|
ff7fd0f0d5 | ||
|
ef9be3fed1 | ||
|
7d46c7aaa2 | ||
|
5e456c569a | ||
|
75034bd9f1 | ||
|
8151f7cf1c | ||
|
3c9394ced4 | ||
|
61cfae3d08 | ||
|
1841d799a2 | ||
|
b93e47b606 | ||
|
41a12fa90e | ||
|
d27ea6be9f | ||
|
e9c61f2bec | ||
|
baa144cf80 | ||
|
cacef99651 | ||
|
03e326d56f | ||
|
67e78e76e3 | ||
|
a998903f52 | ||
|
39aac08f3d | ||
|
0aa2d11f36 | ||
|
592354b3cf | ||
|
db5a8b0695 | ||
|
34992f7ee7 | ||
|
7365db5295 | ||
|
ec9e513699 | ||
|
fa79c2b3a3 | ||
|
61426b04d9 | ||
|
9a8b731780 | ||
|
f6f39685a7 | ||
|
a25bb24b05 | ||
|
abf09b4593 | ||
|
618e25d1ab | ||
|
88de887f82 | ||
|
4cc3250c4a | ||
|
742d7eb311 | ||
|
34b63a771d | ||
|
0dda9864d0 | ||
|
0b698bf13c | ||
|
63c3b36b80 | ||
|
5c9edac317 | ||
|
020a04d6ba | ||
|
20866e43cc | ||
|
9263a445c5 | ||
|
acfc971ddd | ||
|
2c57086489 | ||
|
61ae1a34fb | ||
|
cba13592d0 | ||
|
ae9e109c46 | ||
|
339a149c0c | ||
|
a53d4978d1 | ||
|
de14104af6 | ||
|
e60eb83c93 | ||
|
f44e6eea7d | ||
|
19a5de3328 | ||
|
3f0228e829 | ||
|
9e2d9c49bb | ||
|
ca5225f6cc | ||
|
f1050952dd | ||
|
f46070e76d | ||
|
94553e0ef1 | ||
|
83be683b99 | ||
|
de2032e59c | ||
|
0c29734c5f | ||
|
d3c0a1c6ec | ||
|
bab0d71302 | ||
|
cb074ea304 | ||
|
b34596accf | ||
|
47852c1b4a | ||
|
c12696b7c5 | ||
|
796d2c3144 | ||
|
d4af41cbce | ||
|
40bd6b004b | ||
|
68fa0a311e | ||
|
5f412d7046 | ||
|
db178150ae | ||
|
d8156bbd25 | ||
|
bddbf3663b | ||
|
8dc9d0cdd1 | ||
|
8f99a7a651 | ||
|
571aad3f53 | ||
|
e33d14cbf7 | ||
|
bd92e7e5ef | ||
|
cc3cb2f7a8 | ||
|
a2ee1fede2 | ||
|
d58878ec39 | ||
|
890183fe6c | ||
|
b9be45d151 | ||
|
75401f88f6 | ||
|
752622f35e | ||
|
43cd67f8b1 | ||
|
9bd4f15d94 | ||
|
a205d62cd7 | ||
|
578ede3e87 | ||
|
c10575b730 | ||
|
79fae81959 | ||
|
0e492fad2a | ||
|
634646bb36 | ||
|
d180e6e662 | ||
|
63830e1edc | ||
|
b78e6767e9 | ||
|
33aa6d3c6b | ||
|
a58f877b94 | ||
|
f664bed3a1 | ||
|
8ba1eedb47 | ||
|
5fe0275432 | ||
|
0e7cc9f3c5 | ||
|
e4b9430ce1 | ||
|
770cc7e049 | ||
|
23e04a7431 | ||
|
60d48c1767 | ||
|
102273b5d5 | ||
|
0e2627cde3 | ||
|
eabb0faed5 | ||
|
ed018320d5 | ||
|
7f77c99ba0 | ||
|
5aefc8373c | ||
|
699da59aff | ||
|
4fb7552622 | ||
|
55719ccbdf | ||
|
00707d1d05 | ||
|
0cafb5b3f8 | ||
|
7b293d0771 | ||
|
5b1e3af9cb | ||
|
b857a42c9a | ||
|
9f36050ed9 | ||
|
587f709568 | ||
|
2a78a8e8c3 | ||
|
56901e2ac1 | ||
|
98e01baa8e | ||
|
ebeafd74ab | ||
|
884b4b1ed2 | ||
|
b2f2dc4d0b | ||
|
65ee3f8a8c | ||
|
0414a3f4bc | ||
|
3697c777f1 | ||
|
2912a422bf | ||
|
a83adb7929 | ||
|
df4e04d427 | ||
|
a8727ae408 | ||
|
e8de9ba848 | ||
|
a8785ec879 | ||
|
4a4d28222b | ||
|
e551580e67 | ||
|
5014855e79 | ||
|
9c32fe6833 | ||
|
801a4a86cf | ||
|
1230dda8bd | ||
|
cc0d3a281c | ||
|
fe331d95d2 | ||
|
6cb0522a4c | ||
|
a5dbce0f8b | ||
|
c9b82c70cd | ||
|
74c2b61b39 | ||
|
98ca860a16 | ||
|
c98fd74f66 | ||
|
f942992b81 | ||
|
dc60da99c5 | ||
|
27dae8ce74 | ||
|
c59e7aeb2d | ||
|
0fadf04686 | ||
|
b9acd92b8a | ||
|
bbe090bdb6 | ||
|
afd5d42abc | ||
|
890880a5c8 | ||
|
67083238c2 | ||
|
21f96dd369 | ||
|
36dd38ffa0 | ||
|
7cda15e19c | ||
|
e7f43e16f2 | ||
|
3899577631 | ||
|
2fb65580e4 | ||
|
bbd4f15cca | ||
|
32a1639aca | ||
|
b499dd87cc | ||
|
c9e2dea463 | ||
|
a7fb61a4d4 | ||
|
ea3a7798b9 | ||
|
314512d353 | ||
|
295f694baa | ||
|
d4b2e75b7e | ||
|
33c4c76608 | ||
|
e44962be9a | ||
|
47f016e6e9 | ||
|
88bb921c91 | ||
|
9bcbab1761 | ||
|
96fd59b54e | ||
|
453deac354 | ||
|
966fc65a9d | ||
|
30d0a34afa | ||
|
49896f53ef | ||
|
d1ac836de6 | ||
|
617a8fab72 | ||
|
669c423b07 | ||
|
67dd7e5eed | ||
|
a8a2fd6382 | ||
|
d66695954d | ||
|
13912eea7e | ||
|
b461005c18 | ||
|
0a11ce8282 | ||
|
b4ed45225b | ||
|
2f4b1e76e5 | ||
|
cd5f4511ab | ||
|
78cb08d5a1 | ||
|
5bca4562fb | ||
|
7d421b9afd | ||
|
89c37f7656 | ||
|
ebc97ad6ac | ||
|
35f30ab1cd | ||
|
bdc9aa24ec | ||
|
9807cbd752 | ||
|
b5a52668e9 | ||
|
3f17045e47 | ||
|
4f87a9e477 | ||
|
1fb671ad31 | ||
|
3bde4555d0 | ||
|
7d1e9e11ea | ||
|
ef77d90280 | ||
|
f4558a0f46 | ||
|
b1c44da366 | ||
|
631320c889 | ||
|
0f77313761 | ||
|
27dcce694b | ||
|
f6f606cb8f | ||
|
f00b81f8d8 | ||
|
175803baba | ||
|
a39765072c | ||
|
f4be8814c5 | ||
|
e1eb01b439 | ||
|
d91cdad79e | ||
|
83d615d3ed | ||
|
30ecd7a385 | ||
|
6d8a61627e | ||
|
7a94d66511 | ||
|
27abc38e7e | ||
|
5a76c6b2ae | ||
|
71966dd729 | ||
|
659ada62d3 | ||
|
d9019be22f | ||
|
9692c5e398 | ||
|
c957dd238f | ||
|
680006a883 | ||
|
e242c77202 | ||
|
02c2b98299 | ||
|
d8c6c0c684 | ||
|
2b74a02a67 | ||
|
b386240f28 | ||
|
9dce302e1e | ||
|
2e0fdf1049 | ||
|
cdd059fdb9 | ||
|
1cb4e37207 | ||
|
ef8cd06f72 | ||
|
79413cb4fd | ||
|
06410d217d | ||
|
91b9a7840f | ||
|
21162095fb | ||
|
df2d8def00 | ||
|
23c400a437 | ||
|
236ecf987a | ||
|
e2e2e98c43 | ||
|
ccab6f1552 |
5
.github/workflows/dist.yml
vendored
5
.github/workflows/dist.yml
vendored
@ -4,12 +4,15 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- "12.0"
|
||||
- "13.0"
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@ -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,9 +50,14 @@ 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
|
||||
rm -r tests/seahub/repo_metadata
|
||||
export CCNET_CONF_DIR=/tmp/ccnet SEAFILE_CONF_DIR=/tmp/seafile-data TRAVIS=1 SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=seahub
|
||||
if ./tests/test_seahub_changes.sh; then ./tests/seahubtests.sh init && ./tests/seahubtests.sh runserver && ./tests/seahubtests.sh test; else true; fi
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@ -22,8 +22,10 @@ const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
|
||||
// );
|
||||
|
||||
// reset by custom
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
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}`;
|
||||
|
||||
@ -79,5 +81,4 @@ module.exports = {
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports.moduleFileExtensions = moduleFileExtensions;
|
||||
|
@ -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: [
|
||||
|
@ -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',
|
||||
|
7148
frontend/package-lock.json
generated
7148
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,19 +8,19 @@
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@gatsbyjs/reach-router": "1.3.9",
|
||||
"@seafile/react-image-lightbox": "3.0.7",
|
||||
"@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": "1.0.222",
|
||||
"@seafile/sdoc-editor": "2.0.54",
|
||||
"@seafile/seafile-calendar": "0.0.28",
|
||||
"@seafile/seafile-editor": "1.0.133",
|
||||
"@seafile/sf-metadata-ui-component": "^0.0.68",
|
||||
"@seafile/stldraw-editor": "0.1.5",
|
||||
"@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",
|
||||
"@uiw/react-codemirror": "^4.19.4",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "3.6.0",
|
||||
"axios": "^1.8.2",
|
||||
"chart.js": "4.4.7",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^6.0.1",
|
||||
"copy-to-clipboard": "^3.0.8",
|
||||
@ -33,23 +33,24 @@
|
||||
"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": "^1.0.1",
|
||||
"react": "17.0.2",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-app-polyfill": "^2.0.0",
|
||||
"react-chartjs-2": "4.0.0",
|
||||
"react-chartjs-2": "5.3.0",
|
||||
"react-cookies": "^0.1.0",
|
||||
"react-dnd": "^2.6.0",
|
||||
"react-dnd-html5-backend": "^2.6.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "^10.12.2",
|
||||
"react-responsive": "9.0.2",
|
||||
"react-select": "5.7.0",
|
||||
"react-mentions": "4.4.10",
|
||||
"react-responsive": "10.0.0",
|
||||
"react-select": "5.9.0",
|
||||
"react-transition-group": "4.4.5",
|
||||
"reactstrap": "8.9.0",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
"reactstrap": "9.2.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"svgo-loader": "^3.0.1",
|
||||
"unified": "^7.0.0",
|
||||
"url-parse": "^1.4.3",
|
||||
@ -63,7 +64,7 @@
|
||||
"start": "node scripts/start.js",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "node scripts/test.js --env=jsdom",
|
||||
"dev": "export NODE_ENV=development && node config/server.js"
|
||||
"dev": "export NODE_ENV=development && node --max-old-space-size=4096 config/server.js"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
import { Router, navigate } from '@gatsbyjs/reach-router';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Router, navigate, LocationProvider, globalHistory } from '@gatsbyjs/reach-router';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { Modal } from 'reactstrap';
|
||||
import { siteRoot, siteTitle, mediaUrl, faviconPath } from './utils/constants';
|
||||
@ -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'} />
|
||||
@ -374,4 +363,9 @@ class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
ReactDom.render(<App />, document.getElementById('wrapper'));
|
||||
const root = createRoot(document.getElementById('wrapper'));
|
||||
root.render(
|
||||
<LocationProvider history={globalHistory}>
|
||||
<App />
|
||||
</LocationProvider>
|
||||
);
|
||||
|
15
frontend/src/assets/icons/filter-circled.svg
Normal file
15
frontend/src/assets/icons/filter-circled.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#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 |
2
frontend/src/assets/icons/left_arrow.svg
Normal file
2
frontend/src/assets/icons/left_arrow.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" xmlns="http://www.w3.org/2000/svg" aria-labelledby="title" viewBox="0 0 32 32" preserveAspectRatio="xMidYMid meet" fill="currentColor" width="48" height="48" title="view-back"><title id="title">view-back</title><g><path d="M19.768,23.89c0.354,-0.424 0.296,-1.055 -0.128,-1.408c-1.645,-1.377 -5.465,-4.762 -6.774,-6.482c1.331,-1.749 5.1,-5.085 6.774,-6.482c0.424,-0.353 0.482,-0.984 0.128,-1.408c-0.353,-0.425 -0.984,-0.482 -1.409,-0.128c-1.839,1.532 -5.799,4.993 -7.2,6.964c-0.219,0.312 -0.409,0.664 -0.409,1.054c0,0.39 0.19,0.742 0.409,1.053c1.373,1.932 5.399,5.462 7.2,6.964l0.001,0.001c0.424,0.354 1.055,0.296 1.408,-0.128Z"></path></g></svg>
|
After Width: | Height: | Size: 780 B |
2
frontend/src/assets/icons/right_arrow.svg
Normal file
2
frontend/src/assets/icons/right_arrow.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" xmlns="http://www.w3.org/2000/svg" aria-labelledby="title" viewBox="0 0 32 32" preserveAspectRatio="xMidYMid meet" fill="currentColor" width="48" height="48" title="view-forward"><title id="title">view-forward</title><g><path d="M12.982,23.89c-0.354,-0.424 -0.296,-1.055 0.128,-1.408c1.645,-1.377 5.465,-4.762 6.774,-6.482c-1.331,-1.749 -5.1,-5.085 -6.774,-6.482c-0.424,-0.353 -0.482,-0.984 -0.128,-1.408c0.353,-0.425 0.984,-0.482 1.409,-0.128c1.839,1.532 5.799,4.993 7.2,6.964c0.219,0.312 0.409,0.664 0.409,1.054c0,0.39 -0.19,0.742 -0.409,1.053c-1.373,1.932 -5.399,5.462 -7.2,6.964l-0.001,0.001c-0.424,0.354 -1.055,0.296 -1.408,-0.128Z"></path></g></svg>
|
After Width: | Height: | Size: 790 B |
@ -1 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M13.0123,19.3233l7.07107,-7.07107l-9.40841,-9.40842l-6.84754,-0.297719c-0.0144722,-0.00062923 -0.028965,-0.00062923 -0.0434372,0c-0.275882,0.0119949 -0.489804,0.245365 -0.477809,0.521247l0.297719,6.84754l9.40842,9.40842Zm-10.7052,-16.2125c-0.0359846,-0.827645 0.605783,-1.52776 1.43343,-1.56374c0.0434167,-0.00188768 0.086895,-0.00188768 0.130312,0l7.23616,0.314616l10.3906,10.3906l-8.48528,8.48528l-10.3906,-10.3906l-0.314616,-7.23616Zm5.75544,4.1917c-0.585786,0.585786 -1.53553,0.585786 -2.12132,0c-0.585786,-0.585786 -0.585786,-1.53553 0,-2.12132c0.585786,-0.585786 1.53553,-0.585786 2.12132,0c0.585786,0.585786 0.585786,1.53553 0,2.12132Zm-0.707107,-0.707107c0.195262,-0.195262 0.195262,-0.511845 0,-0.707107c-0.195262,-0.195262 -0.511845,-0.195262 -0.707107,0c-0.195262,0.195262 -0.195262,0.511845 0,0.707107c0.195262,0.195262 0.511845,0.195262 0.707107,0Z" stroke="none"></path></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="15px" height="15px" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M13.7515136,5.93572645 L8.28976642,0.621080351 C7.879571,0.222593643 7.32333684,-0.000847143079 6.7436592,2.41376939e-06 L2.18654926,2.41376939e-06 C0.979680972,2.41376939e-06 0,0.950120356 0,2.12526321 L0,6.55780208 C0,7.12087054 0.230271291,7.66193631 0.64044202,8.06098482 L6.10527317,13.3776312 C6.95834688,14.2074603 8.34132992,14.2074603 9.19440364,13.3776312 L13.7515136,8.94309205 C14.6047509,8.11299113 14.6047509,6.76682748 13.7515136,5.93672657 L13.7515136,5.93572645 Z M12.1129482,8.06825704 L8.34330379,11.7695888 C7.91766113,12.1834794 7.22778724,12.1834794 6.80214458,11.7695888 L2.11187052,7.05109716 C1.90701239,6.85210202 1.79175754,6.58211021 1.79144107,6.30047085 L1.79144107,2.74606218 C1.79169751,2.46489086 1.9068179,2.19534855 2.11144974,1.99679547 C2.31608157,1.79824239 2.59344239,1.68696248 2.88245014,1.68746078 L6.64338195,1.68746078 C6.93283334,1.68746078 7.20969987,1.79859659 7.41396155,1.99731949 L12.1119801,6.56794622 C12.5379286,6.98234468 12.5379286,7.65385856 12.1119801,8.06825704 L12.1129482,8.06825704 Z" id="image2"></path>
|
||||
<path d="M5.19151264,2.9941275 C4.08690365,2.9941275 3.19144113,3.86530858 3.19144113,4.93996491 C3.19144113,6.01462124 4.08690365,6.88580232 5.19151264,6.88580232 C6.29612164,6.88580232 7.19158415,6.01462124 7.19158415,4.93996491 C7.19158415,3.86530858 6.29612164,2.9941275 5.19151264,2.9941275 L5.19151264,2.9941275 Z M4.88150156,5.43615345 C4.69675086,5.322975 4.58976949,5.12109806 4.60216374,4.9090349 C4.61455799,4.69697174 4.74437157,4.50819414 4.94111847,4.41611989 C5.13786537,4.32404563 5.37049132,4.34320761 5.54852541,4.4661535 C5.81754925,4.64750572 5.88755949,5.00514843 5.70588358,5.2700074 C5.52420766,5.53486636 5.15787671,5.60921928 4.88250159,5.43712636 L4.88150156,5.43615345 Z" id="image"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.9 KiB |
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Button, Popover, PopoverBody } from 'reactstrap';
|
||||
import { gettext } from '../utils/constants';
|
||||
|
||||
@ -16,8 +16,7 @@ class ButtonQR extends React.Component {
|
||||
this.state = {
|
||||
isPopoverOpen: false
|
||||
};
|
||||
|
||||
this.btnID = 'btn-' + Math.random().toString().substr(2, 5);
|
||||
this.btn = null;
|
||||
}
|
||||
|
||||
togglePopover = () => {
|
||||
@ -30,14 +29,16 @@ class ButtonQR extends React.Component {
|
||||
const { link } = this.props;
|
||||
const { isPopoverOpen } = this.state;
|
||||
return (
|
||||
<div className="ml-2">
|
||||
<Button outline color="primary" className="btn-icon btn-qr-code-icon sf3-font sf3-font-qr-code" id={this.btnID} onClick={this.togglePopover} type="button"></Button>
|
||||
<Popover placement="bottom" isOpen={isPopoverOpen} target={this.btnID} toggle={this.togglePopover}>
|
||||
<PopoverBody>
|
||||
<QRCode value={link} size={128} />
|
||||
<p className="m-0 mt-1 text-center" style={{ 'maxWidth': '128px' }}>{gettext('Scan the QR code to view the shared content directly')}</p>
|
||||
</PopoverBody>
|
||||
</Popover>
|
||||
<div className="ml-2" ref={ref => this.btn = ref}>
|
||||
<Button outline color="primary" className="btn-icon btn-qr-code-icon sf3-font sf3-font-qr-code" onClick={this.togglePopover} type="button"></Button>
|
||||
{this.btn && (
|
||||
<Popover placement="bottom" isOpen={isPopoverOpen} target={this.btn} toggle={this.togglePopover}>
|
||||
<PopoverBody>
|
||||
<QRCodeSVG value={link} size={128} />
|
||||
<p className="m-0 mt-1 text-center" style={{ 'maxWidth': '128px' }}>{gettext('Scan the QR code to view the shared content directly')}</p>
|
||||
</PopoverBody>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
7
frontend/src/components/centered-loading/index.css
Normal file
7
frontend/src/components/centered-loading/index.css
Normal file
@ -0,0 +1,7 @@
|
||||
.sf-centered-loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
20
frontend/src/components/centered-loading/index.js
Normal file
20
frontend/src/components/centered-loading/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Loading from '../loading';
|
||||
|
||||
import './index.css';
|
||||
|
||||
function CenteredLoading(props) {
|
||||
return (
|
||||
<div className={classnames('sf-centered-loading', props.className)}>
|
||||
<Loading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CenteredLoading.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CenteredLoading;
|
@ -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;
|
||||
|
@ -1,26 +1,30 @@
|
||||
.add-item-btn {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-top: 1px solid #dedede;
|
||||
background: #fff;
|
||||
padding: 0 1rem;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
position: relative;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.add-item-btn:hover {
|
||||
background-color: #f5f5f5;
|
||||
cursor: pointer;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.add-item-btn .add-new-option {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.add-item-btn .seafile-multicolor-icon-add-table {
|
||||
margin-right: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transform: none;
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.add-item-btn .description {
|
||||
flex: 1;
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import '../../css/common-add-tool.css';
|
||||
import Icon from '../icon';
|
||||
|
||||
function CommonAddTool(props) {
|
||||
const { callBack, footerName, className, addIconClassName } = props;
|
||||
import './index.css';
|
||||
|
||||
function CommonAddTool({ callBack, footerName, className, addIconClassName, hideIcon, style }) {
|
||||
return (
|
||||
<div className={`add-item-btn ${className ? className : ''}`} onClick={(e) => {callBack(e);}}>
|
||||
<span className={`sf3-font sf3-font-enlarge mr-2 ${addIconClassName || ''}`}></span>
|
||||
<span className='add-new-option' title={footerName}>{footerName}</span>
|
||||
<div className={`add-item-btn ${className ? className : ''}`} style={style} onClick={(e) => {callBack(e);}}>
|
||||
{!hideIcon && <Icon symbol="add-table" className={addIconClassName} />}
|
||||
<span className="description text-truncate">{footerName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { siteRoot, isPro, gettext, appAvatarURL, enableSSOToThirdpartWebsite } from '../../utils/constants';
|
||||
@ -35,10 +34,6 @@ class Account extends Component {
|
||||
this.handleProps();
|
||||
}
|
||||
|
||||
getContainer = () => {
|
||||
return ReactDOM.findDOMNode(this);
|
||||
};
|
||||
|
||||
handleProps = () => {
|
||||
if (this.state.showInfo) {
|
||||
this.addEvents();
|
||||
@ -61,9 +56,7 @@ class Account extends Component {
|
||||
|
||||
handleDocumentClick = (e) => {
|
||||
if (e && (e.which === 3 || (e.type === 'keyup' && e.which !== Utils.keyCodes.tab))) return;
|
||||
const container = this.getContainer();
|
||||
|
||||
if (container.contains(e.target) && container !== e.target && (e.type !== 'keyup' || e.which === Utils.keyCodes.tab)) {
|
||||
if (this.accountDOM && this.accountDOM.contains(e.target) && this.accountDOM !== e.target && (e.type !== 'keyup' || e.which === Utils.keyCodes.tab)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -101,8 +94,9 @@ class Account extends Component {
|
||||
renderMenu = () => {
|
||||
let data;
|
||||
const { isStaff, isOrgStaff, isInstAdmin } = this.state;
|
||||
const { isAdminPanel = false } = this.props;
|
||||
|
||||
if (this.props.isAdminPanel) {
|
||||
if (isAdminPanel) {
|
||||
if (isStaff) {
|
||||
data = {
|
||||
url: siteRoot,
|
||||
@ -148,7 +142,7 @@ class Account extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="account">
|
||||
<div id="account" ref={ref => this.accountDOM = ref}>
|
||||
<a id="my-info" href="#" onClick={this.onClickAccount} className="account-toggle no-deco d-none d-md-block" aria-label={gettext('View profile and more')}>
|
||||
{this.renderAvatar()}
|
||||
</a>
|
||||
@ -180,10 +174,6 @@ class Account extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
Account.defaultProps = {
|
||||
isAdminPanel: false
|
||||
};
|
||||
|
||||
Account.propTypes = propTypes;
|
||||
|
||||
export default Account;
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import SearchInput from '../search-input';
|
||||
import SearchInput from '../../search-input';
|
||||
import Option from './option';
|
||||
import KeyCodes from '../../../constants/keyCodes';
|
||||
import ClickOutside from './click-outside';
|
||||
|
||||
import './select-option-group.css';
|
||||
|
||||
@ -26,6 +25,7 @@ class SelectOptionGroup extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', this.onHotKey);
|
||||
document.addEventListener('mousedown', this.handleDocumentClick);
|
||||
setTimeout(() => {
|
||||
this.resetMenuStyle();
|
||||
}, 1);
|
||||
@ -35,8 +35,13 @@ class SelectOptionGroup extends Component {
|
||||
this.filterOptions = null;
|
||||
this.timer && clearTimeout(this.timer);
|
||||
window.removeEventListener('keydown', this.onHotKey);
|
||||
document.removeEventListener('mousedown', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
handleDocumentClick = (e) => {
|
||||
this.props.onClickOutside(e);
|
||||
};
|
||||
|
||||
resetMenuStyle = () => {
|
||||
const { isInModal, position } = this.props;
|
||||
const { top, height } = this.optionGroupRef.getBoundingClientRect();
|
||||
@ -170,27 +175,25 @@ class SelectOptionGroup extends Component {
|
||||
};
|
||||
}
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.props.onClickOutside}>
|
||||
<div
|
||||
className={classnames('pt-0 option-group', className ? 'option-group-' + className : '')}
|
||||
ref={(ref) => this.optionGroupRef = ref}
|
||||
style={style}
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
<div className="option-group-search position-relative">
|
||||
<SearchInput
|
||||
className="option-search-control"
|
||||
autoFocus={isInModal}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={this.onChangeSearch}
|
||||
ref={this.searchInputRef}
|
||||
/>
|
||||
</div>
|
||||
<div className="option-group-content" ref={(ref) => this.optionGroupContentRef = ref}>
|
||||
{this.renderOptGroup(searchVal)}
|
||||
</div>
|
||||
<div
|
||||
className={classnames('pt-0 option-group', className ? 'option-group-' + className : '')}
|
||||
ref={(ref) => this.optionGroupRef = ref}
|
||||
style={style}
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
<div className="option-group-search position-relative">
|
||||
<SearchInput
|
||||
className="option-search-control"
|
||||
autoFocus={isInModal}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={this.onChangeSearch}
|
||||
ref={this.searchInputRef}
|
||||
/>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
<div className="option-group-content" ref={(ref) => this.optionGroupContentRef = ref}>
|
||||
{this.renderOptGroup(searchVal)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { processor } from '@seafile/seafile-editor';
|
||||
import '../../css/notice-item.css';
|
||||
|
||||
const propTypes = {
|
||||
noticeItem: PropTypes.object.isRequired,
|
||||
@ -17,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';
|
||||
@ -24,6 +27,8 @@ const MSG_TYPE_SAML_SSO_FAILED = 'saml_sso_failed';
|
||||
const MSG_TYPE_REPO_SHARE_PERM_CHANGE = 'repo_share_perm_change';
|
||||
const MSG_TYPE_REPO_SHARE_PERM_DELETE = 'repo_share_perm_delete';
|
||||
const MSG_TYPE_FACE_CLUSTER = 'face_cluster';
|
||||
const MSG_TYPE_SEADOC_REPLY = 'reply';
|
||||
const MSG_TYPE_SEADOC_COMMENT = 'comment';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@ -34,36 +39,44 @@ class NoticeItem extends React.Component {
|
||||
let noticeType = noticeItem.type;
|
||||
let detail = noticeItem.detail;
|
||||
|
||||
if (noticeType === MSG_TYPE_ADD_USER_TO_GROUP) {
|
||||
|
||||
let avatar_url = detail.group_staff_avatar_url;
|
||||
|
||||
let groupStaff = detail.group_staff_name;
|
||||
|
||||
// group name does not support special characters
|
||||
let userHref = siteRoot + 'profile/' + detail.group_staff_email + '/';
|
||||
let groupHref = siteRoot + 'group/' + detail.group_id + '/';
|
||||
let groupName = detail.group_name;
|
||||
|
||||
let notice = gettext('User {user_link} has added you to {group_link}');
|
||||
let userLink = '<a href=' + userHref + '>' + groupStaff + '</a>';
|
||||
let groupLink = '<a href=' + groupHref + '>' + groupName + '</a>';
|
||||
|
||||
notice = notice.replace('{user_link}', userLink);
|
||||
notice = notice.replace('{group_link}', groupLink);
|
||||
|
||||
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;
|
||||
// group name does not support special characters
|
||||
let userHref = siteRoot + 'profile/' + encodeURIComponent(detail.group_staff_email) + '/';
|
||||
let groupHref = siteRoot + 'group/' + detail.group_id + '/';
|
||||
let groupName = detail.group_name;
|
||||
let username = detail.group_staff_name;
|
||||
let notice = gettext('User {user_link} has added you to {group_link}');
|
||||
let userLink = '<a href=' + userHref + '>' + Utils.HTMLescape(groupStaff) + '</a>';
|
||||
let groupLink = '<a href=' + groupHref + '>' + Utils.HTMLescape(groupName) + '</a>';
|
||||
notice = notice.replace('{user_link}', userLink);
|
||||
notice = notice.replace('{group_link}', groupLink);
|
||||
return { avatar_url, notice, username };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_SHARE) {
|
||||
|
||||
let avatar_url = detail.share_from_user_avatar_url;
|
||||
|
||||
let shareFrom = detail.share_from_user_name;
|
||||
|
||||
let repoName = detail.repo_name;
|
||||
let repoUrl = siteRoot + 'library/' + detail.repo_id + '/' + repoName + '/';
|
||||
|
||||
let path = detail.path;
|
||||
let notice = '';
|
||||
// 1. handle translate
|
||||
@ -72,21 +85,17 @@ class NoticeItem extends React.Component {
|
||||
} else { // share folder
|
||||
notice = gettext('{share_from} has shared a folder named {repo_link} to you.');
|
||||
}
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{share_from}', shareFrom);
|
||||
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: shareFrom };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_SHARE_PERM_CHANGE) {
|
||||
|
||||
let avatar_url = detail.share_from_user_avatar_url;
|
||||
let shareFrom = detail.share_from_user_name;
|
||||
let permission = detail.permission;
|
||||
@ -100,22 +109,18 @@ class NoticeItem extends React.Component {
|
||||
} else { // share folder
|
||||
notice = gettext('{share_from} has changed the permission of folder {repo_link} to {permission}.');
|
||||
}
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{share_from}', shareFrom);
|
||||
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
|
||||
notice = notice.replace('{permission}', permission);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: shareFrom };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_SHARE_PERM_DELETE) {
|
||||
|
||||
let avatar_url = detail.share_from_user_avatar_url;
|
||||
let shareFrom = detail.share_from_user_name;
|
||||
let repoName = detail.repo_name;
|
||||
@ -127,26 +132,20 @@ class NoticeItem extends React.Component {
|
||||
} else { // share folder
|
||||
notice = gettext('{share_from} has cancelled the sharing of folder {repo_name}.');
|
||||
}
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{share_from}', shareFrom);
|
||||
notice = notice.replace('{repo_name}', repoName);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: shareFrom };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_SHARE_TO_GROUP) {
|
||||
|
||||
let avatar_url = detail.share_from_user_avatar_url;
|
||||
|
||||
let shareFrom = detail.share_from_user_name;
|
||||
|
||||
let repoName = detail.repo_name;
|
||||
let repoUrl = siteRoot + 'library/' + detail.repo_id + '/' + repoName + '/';
|
||||
|
||||
let groupUrl = siteRoot + 'group/' + detail.group_id + '/';
|
||||
let groupName = detail.group_name;
|
||||
|
||||
let path = detail.path;
|
||||
let notice = '';
|
||||
// 1. handle translate
|
||||
@ -155,60 +154,50 @@ class NoticeItem extends React.Component {
|
||||
} else {
|
||||
notice = gettext('{share_from} has shared a folder named {repo_link} to group {group_link}.');
|
||||
}
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{share_from}', shareFrom);
|
||||
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
|
||||
notice = notice.replace('{group_link}', `{tagB}${groupName}{/tagB}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
notice = notice.replace('{tagB}', `<a href='${Utils.encodePath(groupUrl)}'>`);
|
||||
notice = notice.replace('{/tagB}', '</a>');
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: shareFrom };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_REPO_TRANSFER) {
|
||||
|
||||
let avatar_url = detail.transfer_from_user_avatar_url;
|
||||
|
||||
let repoOwner = detail.transfer_from_user_name;
|
||||
|
||||
let repoName = detail.repo_name;
|
||||
let repoUrl = siteRoot + 'library/' + detail.repo_id + '/' + repoName + '/';
|
||||
// 1. handle translate
|
||||
let notice = gettext('{user} has transfered a library named {repo_link} to you.');
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{user}', repoOwner);
|
||||
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(repoUrl)}>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
return { avatar_url, notice };
|
||||
return { avatar_url, notice, username: repoOwner };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_FILE_UPLOADED) {
|
||||
let avatar_url = detail.uploaded_user_avatar_url;
|
||||
let fileName = detail.file_name;
|
||||
let fileLink = siteRoot + 'lib/' + detail.repo_id + '/' + 'file' + detail.file_path;
|
||||
|
||||
let folderName = detail.folder_name;
|
||||
let folderLink = siteRoot + 'library/' + detail.repo_id + '/' + detail.repo_name + detail.folder_path;
|
||||
let notice = '';
|
||||
if (detail.repo_id) { // todo is repo exist ?
|
||||
// 1. handle translate
|
||||
notice = gettext('A file named {upload_file_link} is uploaded to {uploaded_link}.');
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{upload_file_link}', `{tagA}${fileName}{/tagA}`);
|
||||
notice = notice.replace('{uploaded_link}', `{tagB}${folderName}{/tagB}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
|
||||
// 3. add jump link
|
||||
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(fileLink)}>`);
|
||||
notice = notice.replace('{/tagA}', '</a>');
|
||||
@ -217,7 +206,6 @@ class NoticeItem extends React.Component {
|
||||
} else {
|
||||
// 1. handle translate
|
||||
notice = gettext('A file named {upload_file_link} is uploaded.');
|
||||
|
||||
// 2. handle xss(cross-site scripting)
|
||||
notice = notice.replace('{upload_file_link}', `${fileName}`);
|
||||
notice = Utils.HTMLescape(notice);
|
||||
@ -341,17 +329,11 @@ class NoticeItem extends React.Component {
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_DELETED_FILES) {
|
||||
const {
|
||||
repo_id,
|
||||
repo_name,
|
||||
} = detail;
|
||||
|
||||
const { repo_id, repo_name } = detail;
|
||||
const repoURL = `${siteRoot}library/${repo_id}/${encodeURIComponent(repo_name)}/`;
|
||||
const repoLink = `<a href=${repoURL} target="_blank">${Utils.HTMLescape(repo_name)}</a>`;
|
||||
|
||||
let notice = gettext('Your library {libraryName} has recently deleted a large number of files.');
|
||||
notice = notice.replace('{libraryName}', repoLink);
|
||||
|
||||
return { avatar_url: null, notice };
|
||||
}
|
||||
|
||||
@ -371,15 +353,70 @@ class NoticeItem extends React.Component {
|
||||
if (noticeType === MSG_TYPE_SAML_SSO_FAILED) {
|
||||
const { error_msg } = detail;
|
||||
let notice = gettext(error_msg);
|
||||
|
||||
return { avatar_url: null, notice };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_SEADOC_COMMENT) {
|
||||
let avatar_url = detail.avatar_url;
|
||||
let notice = detail.comment;
|
||||
let username = detail.user_name;
|
||||
let is_resolved = detail.is_resolved;
|
||||
let sdoc_name = detail.sdoc_name;
|
||||
const repo_id = detail.repo_id;
|
||||
const sdoc_path = detail.sdoc_path;
|
||||
const sdoc_href = siteRoot + 'lib/' + repo_id + '/file' + sdoc_path;
|
||||
let sdoc_link = '<a href=' + sdoc_href + '>' + sdoc_name + '</a>';
|
||||
processor.process(notice, (error, vfile) => {
|
||||
notice = String(vfile);
|
||||
});
|
||||
if (is_resolved) {
|
||||
if (detail.resolve_comment && detail.resolve_comment !== '\u200B') {
|
||||
notice = gettext('Marked "{resolve_comment}" as resolved in document {sdoc_link}');
|
||||
notice = notice.replace('{resolve_comment}', detail.resolve_comment);
|
||||
notice = notice.replace('{sdoc_link}', sdoc_link);
|
||||
} else {
|
||||
notice = gettext('Marked as resolved in document {sdoc_link}');
|
||||
notice = notice.replace('{sdoc_link}', sdoc_link);
|
||||
}
|
||||
} else {
|
||||
notice = gettext('Added a new comment in document {sdoc_link}:').replace('{sdoc_link}', sdoc_link) + notice;
|
||||
}
|
||||
return { avatar_url, username, notice };
|
||||
}
|
||||
|
||||
if (noticeType === MSG_TYPE_SEADOC_REPLY) {
|
||||
let avatar_url = detail.avatar_url;
|
||||
let notice = detail.reply;
|
||||
let username = detail.user_name;
|
||||
let is_resolved = detail.is_resolved;
|
||||
let sdoc_name = detail.sdoc_name;
|
||||
const repo_id = detail.repo_id;
|
||||
const sdoc_path = detail.sdoc_path;
|
||||
const sdoc_href = siteRoot + 'lib/' + repo_id + '/file' + sdoc_path;
|
||||
let sdoc_link = '<a href=' + sdoc_href + '>' + sdoc_name + '</a>';
|
||||
processor.process(notice, (error, vfile) => {
|
||||
notice = String(vfile);
|
||||
});
|
||||
if (is_resolved) {
|
||||
if (detail.resolve_comment && detail.resolve_comment !== '\u200B') {
|
||||
notice = gettext('Marked "{resolve_comment}" as resolved in document {sdoc_link}');
|
||||
notice = notice.replace('{resolve_comment}', detail.resolve_comment);
|
||||
notice = notice.replace('{sdoc_link}', sdoc_link);
|
||||
} else {
|
||||
notice = gettext('Marked as resolved in document {sdoc_link}');
|
||||
notice = notice.replace('{sdoc_link}', sdoc_link);
|
||||
}
|
||||
} else {
|
||||
notice = gettext('Added a new reply in document {sdoc_link}:').replace('{sdoc_link}', sdoc_link) + notice;
|
||||
}
|
||||
return { avatar_url, username, notice };
|
||||
}
|
||||
|
||||
// if (noticeType === MSG_TYPE_GUEST_INVITATION_ACCEPTED) {
|
||||
|
||||
// }
|
||||
|
||||
return { avatar_url: null, notice: null };
|
||||
return { avatar_url: null, notice: null, username: null };
|
||||
}
|
||||
|
||||
onNoticeItemClick = () => {
|
||||
@ -392,16 +429,19 @@ class NoticeItem extends React.Component {
|
||||
|
||||
render() {
|
||||
let noticeItem = this.props.noticeItem;
|
||||
let { avatar_url, notice } = this.generatorNoticeInfo();
|
||||
|
||||
let { avatar_url, username, notice } = this.generatorNoticeInfo();
|
||||
if (!avatar_url && !notice) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.props.tr ? (
|
||||
<tr className={noticeItem.seen ? 'read' : 'unread font-weight-bold'}>
|
||||
<tr className='notification-item'>
|
||||
<td className="text-center">
|
||||
{!noticeItem.seen && <span className="notification-point" onClick={this.onMarkNotificationRead}></span>}
|
||||
</td>
|
||||
<td>
|
||||
<img src={avatar_url} width="32" height="32" className="avatar" alt="" />
|
||||
<span className="ml-2 notification-user-name">{username || gettext('System')}</span>
|
||||
</td>
|
||||
<td className="pr-1 pr-md-8">
|
||||
<p className="m-0" dangerouslySetInnerHTML={{ __html: notice }}></p>
|
||||
@ -411,13 +451,21 @@ class NoticeItem extends React.Component {
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<li onClick={this.onNoticeItemClick} className={noticeItem.seen ? 'read' : 'unread'}>
|
||||
<div className="notice-item">
|
||||
<div className="main-info">
|
||||
<img src={avatar_url} width="32" height="32" className="avatar" alt=""/>
|
||||
<p className="brief" dangerouslySetInnerHTML={{ __html: notice }}></p>
|
||||
<li className='notification-item' onClick={this.onNoticeItemClick}>
|
||||
<div className="notification-item-header">
|
||||
{!noticeItem.seen &&
|
||||
<span className="notification-point" onClick={this.onMarkNotificationRead}></span>
|
||||
}
|
||||
<div className="notification-header-info">
|
||||
<div className="notification-user-detail">
|
||||
<img className="notification-user-avatar" src={avatar_url} alt="" />
|
||||
<span className="ml-2 notification-user-name">{username || gettext('System')}</span>
|
||||
</div>
|
||||
<span className="notification-time">{dayjs(noticeItem.time).fromNow()}</span>
|
||||
</div>
|
||||
<p className="time">{dayjs(noticeItem.time).fromNow()}</p>
|
||||
</div>
|
||||
<div className="notification-content-wrapper">
|
||||
<div dangerouslySetInnerHTML={{ __html: notice }}></div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
@ -5,7 +5,7 @@
|
||||
.notification-container {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
width: 320px;
|
||||
width: 400px;
|
||||
right: -16px;
|
||||
top: -1px;
|
||||
border-radius: 3px;
|
||||
@ -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 {
|
||||
@ -65,21 +55,15 @@
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .mark-notifications {
|
||||
color: #b4b4b4;
|
||||
.notification-container .mark-all-read {
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #ededed;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .mark-notifications:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-body .notification-list-container {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
@ -190,3 +174,33 @@
|
||||
.notification-body .notification-footer:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .mark-notifications {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #ededed;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .mark-notifications .mark-all-read:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .nav .nav-item .nav-link {
|
||||
height: 46px;
|
||||
margin-right: 15px;
|
||||
margin-left: 15px;
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.notification-container .notification-body .nav .nav-item .nav-link.active {
|
||||
color: #ED7109;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.notification-container {
|
||||
right: -60px;
|
||||
width: 360px;
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,12 @@
|
||||
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';
|
||||
|
||||
export default class NotificationPopover extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
headerText: PropTypes.string.isRequired,
|
||||
bodyText: PropTypes.string.isRequired,
|
||||
footerText: PropTypes.string.isRequired,
|
||||
onNotificationListToggle: PropTypes.func,
|
||||
onNotificationDialogToggle: PropTypes.func,
|
||||
listNotifications: PropTypes.func,
|
||||
onMarkAllNotifications: PropTypes.func,
|
||||
children: PropTypes.any,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
headerText: '',
|
||||
bodyText: '',
|
||||
footerText: '',
|
||||
};
|
||||
class NotificationPopover extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('mousedown', this.handleOutsideClick, true);
|
||||
@ -47,8 +33,12 @@ export default class NotificationPopover extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
tabItemClick = (tab) => {
|
||||
this.props.tabItemClick(tab);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { headerText, bodyText, footerText } = this.props;
|
||||
const { headerText = '', bodyText = '', footerText = '', currentTab, generalNoticeListUnseen, discussionNoticeListUnseen } = this.props;
|
||||
return (
|
||||
<Popover
|
||||
className="notification-wrapper"
|
||||
@ -59,17 +49,44 @@ export default 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" onClick={this.props.onMarkAllNotifications}>{bodyText}</div>
|
||||
<div className="mark-notifications">
|
||||
<ul className="nav">
|
||||
<li className="nav-item" onClick={() => this.tabItemClick('general')}>
|
||||
<span className={`nav-link ${currentTab === 'general' ? 'active' : ''}`}>
|
||||
{gettext('General')}
|
||||
{generalNoticeListUnseen > 0 && <span>({generalNoticeListUnseen})</span>}
|
||||
</span>
|
||||
</li>
|
||||
<li className="nav-item" onClick={() => this.tabItemClick('discussion')}>
|
||||
<span className={`nav-link ${currentTab === 'discussion' ? 'active' : ''}`}>
|
||||
{gettext('Discussion')}
|
||||
{discussionNoticeListUnseen > 0 && <span>({discussionNoticeListUnseen})</span>}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<span className="mark-all-read" onClick={this.props.onMarkAllNotifications}>
|
||||
{bodyText}
|
||||
</span>
|
||||
</div>
|
||||
{currentTab === 'general' &&
|
||||
<div className="notification-list-container" onScroll={this.onHandleScroll} ref={ref => this.notificationListRef = ref}>
|
||||
<div ref={ref => this.notificationsWrapperRef = ref}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{currentTab === 'discussion' &&
|
||||
<div className="notification-list-container" onScroll={this.onHandleScroll} ref={ref => this.notificationListRef = ref}>
|
||||
<div ref={ref => this.notificationsWrapperRef = ref}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="notification-footer" onClick={this.onNotificationDialogToggle}>{footerText}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,3 +94,20 @@ export default class NotificationPopover extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationPopover.propTypes = {
|
||||
headerText: PropTypes.string.isRequired,
|
||||
bodyText: PropTypes.string.isRequired,
|
||||
footerText: PropTypes.string.isRequired,
|
||||
onNotificationListToggle: PropTypes.func,
|
||||
onNotificationDialogToggle: PropTypes.func,
|
||||
listNotifications: PropTypes.func,
|
||||
onMarkAllNotifications: PropTypes.func,
|
||||
tabItemClick: PropTypes.func,
|
||||
children: PropTypes.any,
|
||||
currentTab: PropTypes.string,
|
||||
generalNoticeListUnseen: PropTypes.number,
|
||||
discussionNoticeListUnseen: PropTypes.number,
|
||||
};
|
||||
|
||||
export default NotificationPopover;
|
||||
|
@ -12,15 +12,21 @@ class Notification extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
showNotice: false,
|
||||
unseenCount: 0,
|
||||
noticeList: [],
|
||||
totalUnseenCount: 0,
|
||||
generalNoticeList: [],
|
||||
discussionNoticeList: [],
|
||||
currentTab: 'general',
|
||||
isShowNotificationDialog: this.getInitDialogState(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.getUnseenNotificationCount().then(res => {
|
||||
this.setState({ unseenCount: res.data.unseen_count });
|
||||
seafileAPI.listAllNotifications().then(res => {
|
||||
this.setState({
|
||||
totalUnseenCount: res.data.total_unseen_count,
|
||||
generalNoticeListUnseen: res.data.general.unseen_count,
|
||||
discussionNoticeListUnseen: res.data.discussion.unseen_count
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -30,7 +36,7 @@ class Notification extends React.Component {
|
||||
seafileAPI.updateNotifications();
|
||||
this.setState({
|
||||
showNotice: false,
|
||||
unseenCount: 0
|
||||
totalUnseenCount: 0
|
||||
});
|
||||
} else {
|
||||
this.loadNotices();
|
||||
@ -38,28 +44,65 @@ class Notification extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
tabItemClick = (tab) => {
|
||||
const { currentTab } = this.state;
|
||||
if (currentTab === tab) return;
|
||||
this.setState({
|
||||
showNotice: true,
|
||||
currentTab: tab
|
||||
});
|
||||
};
|
||||
|
||||
loadNotices = () => {
|
||||
let page = 1;
|
||||
let perPage = 5;
|
||||
seafileAPI.listNotifications(page, perPage).then(res => {
|
||||
let noticeList = res.data.notification_list;
|
||||
this.setState({ noticeList: noticeList });
|
||||
let perPage = 25;
|
||||
seafileAPI.listAllNotifications(page, perPage).then(res => {
|
||||
let generalNoticeList = res.data.general.notification_list;
|
||||
let discussionNoticeList = res.data.discussion.notification_list;
|
||||
let generalNoticeListUnseen = res.data.general.unseen_count;
|
||||
let discussionNoticeListUnseen = res.data.discussion.unseen_count;
|
||||
this.setState({
|
||||
generalNoticeList: generalNoticeList,
|
||||
discussionNoticeList: discussionNoticeList,
|
||||
generalNoticeListUnseen: generalNoticeListUnseen,
|
||||
discussionNoticeListUnseen: discussionNoticeListUnseen
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onNoticeItemClick = (noticeItem) => {
|
||||
let noticeList = this.state.noticeList.map(item => {
|
||||
if (item.id === noticeItem.id) {
|
||||
item.seen = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
seafileAPI.markNoticeAsRead(noticeItem.id);
|
||||
let unseenCount = this.state.unseenCount === 0 ? 0 : this.state.unseenCount - 1;
|
||||
this.setState({
|
||||
noticeList: noticeList,
|
||||
unseenCount: unseenCount,
|
||||
});
|
||||
if (this.state.currentTab === 'general') {
|
||||
let noticeList = this.state.generalNoticeList.map(item => {
|
||||
if (item.id === noticeItem.id) {
|
||||
item.seen = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
let totalUnseenCount = this.state.totalUnseenCount === 0 ? 0 : this.state.totalUnseenCount - 1;
|
||||
let generalNoticeListUnseen = this.state.generalNoticeListUnseen === 0 ? 0 : this.state.generalNoticeListUnseen - 1;
|
||||
this.setState({
|
||||
generalNoticeList: noticeList,
|
||||
totalUnseenCount: totalUnseenCount,
|
||||
generalNoticeListUnseen: generalNoticeListUnseen
|
||||
});
|
||||
seafileAPI.markNoticeAsRead(noticeItem.id);
|
||||
}
|
||||
if (this.state.currentTab === 'discussion') {
|
||||
let noticeList = this.state.discussionNoticeList.map(item => {
|
||||
if (item.id === noticeItem.id) {
|
||||
item.seen = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
let totalUnseenCount = this.state.totalUnseenCount === 0 ? 0 : this.state.totalUnseenCount - 1;
|
||||
let discussionNoticeListUnseen = this.state.discussionNoticeListUnseen === 0 ? 0 : this.state.discussionNoticeListUnseen - 1;
|
||||
this.setState({
|
||||
discussionNoticeList: noticeList,
|
||||
totalUnseenCount: totalUnseenCount,
|
||||
discussionNoticeListUnseen: discussionNoticeListUnseen
|
||||
});
|
||||
seafileAPI.markSdocNoticeAsRead(noticeItem.id);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -79,43 +122,103 @@ class Notification extends React.Component {
|
||||
};
|
||||
|
||||
onMarkAllNotifications = () => {
|
||||
seafileAPI.updateNotifications().then(() => {
|
||||
this.setState({
|
||||
unseenCount: 0,
|
||||
let generalNoticeListUnseen = this.state.generalNoticeListUnseen;
|
||||
let discussionNoticeListUnseen = this.state.discussionNoticeListUnseen;
|
||||
if (this.state.currentTab === 'general') {
|
||||
seafileAPI.updateNotifications().then((res) => {
|
||||
this.setState({
|
||||
generalNoticeList: this.state.generalNoticeList.map(item => {
|
||||
item.seen = true;
|
||||
return item;
|
||||
}),
|
||||
generalNoticeListUnseen: 0,
|
||||
totalUnseenCount: discussionNoticeListUnseen
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
errorMsg: Utils.getErrorMsg(error, true)
|
||||
} else if (this.state.currentTab === 'discussion') {
|
||||
seafileAPI.updateSdocNotifications().then((res) => {
|
||||
this.setState({
|
||||
discussionNoticeList: this.state.discussionNoticeList.map(item => {
|
||||
item.seen = true;
|
||||
return item;
|
||||
}),
|
||||
discussionNoticeListUnseen: 0,
|
||||
totalUnseenCount: generalNoticeListUnseen
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateTotalUnseenCount = (noticeType) => {
|
||||
if (noticeType === 'general') {
|
||||
this.setState({
|
||||
generalNoticeListUnseen: 0,
|
||||
totalUnseenCount: this.state.discussionNoticeListUnseen
|
||||
});
|
||||
} else if (noticeType === 'discussion') {
|
||||
this.setState({
|
||||
discussionNoticeListUnseen: 0,
|
||||
totalUnseenCount: this.state.generalNoticeListUnseen
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { unseenCount } = this.state;
|
||||
const { totalUnseenCount, currentTab, generalNoticeList, discussionNoticeList, generalNoticeListUnseen, discussionNoticeListUnseen } = this.state;
|
||||
return (
|
||||
<div id="notifications">
|
||||
<a href="#" onClick={this.onClick} className="no-deco" id="notice-icon" title={gettext('Notifications')} aria-label={gettext('Notifications')}>
|
||||
<span className="sf2-icon-bell" id="notification-popover"></span>
|
||||
<span className={`num ${unseenCount ? '' : 'hide'}`}>{unseenCount}</span>
|
||||
<span className={`num ${totalUnseenCount ? '' : 'hide'}`}>{totalUnseenCount}</span>
|
||||
</a>
|
||||
{this.state.showNotice &&
|
||||
<NotificationPopover
|
||||
headerText={gettext('Notification')}
|
||||
bodyText={gettext('Mark all as read')}
|
||||
footerText={gettext('View all notifications')}
|
||||
currentTab={currentTab}
|
||||
onNotificationListToggle={this.onNotificationListToggle}
|
||||
onNotificationDialogToggle={this.onNotificationDialogToggle}
|
||||
onMarkAllNotifications={this.onMarkAllNotifications}
|
||||
tabItemClick={this.tabItemClick}
|
||||
generalNoticeListUnseen={generalNoticeListUnseen}
|
||||
discussionNoticeListUnseen={discussionNoticeListUnseen}
|
||||
>
|
||||
<ul className="notice-list list-unstyled" id="notice-popover">
|
||||
{this.state.noticeList.map(item => {
|
||||
return (<NoticeItem key={item.id} noticeItem={item} onNoticeItemClick={this.onNoticeItemClick}/>);
|
||||
})}
|
||||
</ul>
|
||||
{currentTab === 'general' &&
|
||||
<ul className="notice-list list-unstyled" id="notice-popover">
|
||||
{generalNoticeList.map(item => {
|
||||
return (
|
||||
<NoticeItem key={item.id} noticeItem={item} onNoticeItemClick={this.onNoticeItemClick}/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
{currentTab === 'discussion' &&
|
||||
<ul className="notice-list list-unstyled" id="notice-popover">
|
||||
{discussionNoticeList.map(item => {
|
||||
return (
|
||||
<NoticeItem key={item.id} noticeItem={item} onNoticeItemClick={this.onNoticeItemClick}/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
</NotificationPopover>
|
||||
}
|
||||
{this.state.isShowNotificationDialog &&
|
||||
<UserNotificationsDialog onNotificationDialogToggle={this.onNotificationDialogToggle} />
|
||||
<UserNotificationsDialog
|
||||
onNotificationDialogToggle={this.onNotificationDialogToggle}
|
||||
generalNoticeListUnseen={generalNoticeListUnseen}
|
||||
discussionNoticeListUnseen={discussionNoticeListUnseen}
|
||||
updateTotalUnseenCount={this.updateTotalUnseenCount}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
15
frontend/src/components/common/seahub-modal-close.js
Normal file
15
frontend/src/components/common/seahub-modal-close.js
Normal 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;
|
@ -54,47 +54,17 @@ Option.propTypes = {
|
||||
}),
|
||||
};
|
||||
|
||||
export default class SeahubSelect extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
isMulti: PropTypes.bool,
|
||||
options: PropTypes.array.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string]),
|
||||
isSearchable: PropTypes.bool,
|
||||
isClearable: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
classNamePrefix: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
form: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
menuPortalTarget: PropTypes.string,
|
||||
menuPosition: PropTypes.string,
|
||||
noOptionsMessage: PropTypes.func,
|
||||
innerRef: PropTypes.object,
|
||||
isDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
options: [],
|
||||
value: {},
|
||||
isDisabled: false,
|
||||
isSearchable: true,
|
||||
isClearable: true,
|
||||
placeholder: '',
|
||||
isMulti: false,
|
||||
menuPortalTarget: '.modal',
|
||||
noOptionsMessage: () => {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
class SeahubSelect extends React.Component {
|
||||
|
||||
getMenuPortalTarget = () => {
|
||||
return document.querySelector(this.props.menuPortalTarget);
|
||||
const { menuPortalTarget = '.modal' } = this.props;
|
||||
return document.querySelector(menuPortalTarget);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { options, onChange, value, isSearchable, placeholder, isMulti, menuPosition, isClearable, noOptionsMessage,
|
||||
classNamePrefix, innerRef, isDisabled, form, className } = this.props;
|
||||
const { options = [], onChange, value = {}, isSearchable = true, placeholder = '',
|
||||
isMulti = false, menuPosition, isClearable = true, noOptionsMessage = (() => { return null; }),
|
||||
classNamePrefix, innerRef, isDisabled = false, form, className } = this.props;
|
||||
|
||||
return (
|
||||
<Select
|
||||
@ -121,3 +91,23 @@ export default class SeahubSelect extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SeahubSelect.propTypes = {
|
||||
isMulti: PropTypes.bool,
|
||||
options: PropTypes.array.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string]),
|
||||
isSearchable: PropTypes.bool,
|
||||
isClearable: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
classNamePrefix: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
form: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
menuPortalTarget: PropTypes.string,
|
||||
menuPosition: PropTypes.string,
|
||||
noOptionsMessage: PropTypes.func,
|
||||
innerRef: PropTypes.object,
|
||||
isDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SeahubSelect;
|
||||
|
@ -228,7 +228,7 @@ class ContextMenu extends React.Component {
|
||||
onMouseMove={(e) => {e.stopPropagation();}}
|
||||
>
|
||||
<DropdownToggle
|
||||
tag='div'
|
||||
tag='span'
|
||||
className="dropdown-item font-weight-normal rounded-0 d-flex align-items-center"
|
||||
onMouseEnter={this.toggleSubMenuShown.bind(this, menuItem)}
|
||||
>
|
||||
|
@ -2,13 +2,12 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import DirOperationToolBar from '../../components/toolbar/dir-operation-toolbar';
|
||||
import DirOperationToolbar from '../../components/toolbar/dir-operation-toolbar';
|
||||
import MetadataViewName from '../../metadata/components/metadata-view-name';
|
||||
import TagViewName from '../../tag/components/tag-view-name';
|
||||
import { siteRoot, gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { debounce, Utils } from '../../utils/utils';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { debounce } from '../../metadata/utils/common';
|
||||
import { EVENT_BUS_TYPE } from '../../metadata/constants';
|
||||
import { ALL_TAGS_ID } from '../../tag/constants';
|
||||
|
||||
@ -34,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,
|
||||
};
|
||||
@ -129,15 +127,19 @@ class DirPath extends React.Component {
|
||||
return (
|
||||
<>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item">{gettext('Views')}</span>
|
||||
<span className="path-item path-item-read-only">{gettext('Views')}</span>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item" role={children ? 'button' : null} onClick={children ? this.handleRefresh : () => {}}>
|
||||
<span
|
||||
className="path-item path-item-read-only"
|
||||
role={children ? 'button' : null}
|
||||
onClick={children ? this.handleRefresh : () => {}}
|
||||
>
|
||||
<MetadataViewName id={viewId} />
|
||||
</span>
|
||||
{children && (
|
||||
<>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item">{children}</span>
|
||||
<span className="path-item path-item-read-only">{children}</span>
|
||||
</>
|
||||
)}
|
||||
<div className="path-item-refresh" id="sf-metadata-view-refresh" onClick={this.handleRefresh}>
|
||||
@ -157,13 +159,13 @@ class DirPath extends React.Component {
|
||||
return (
|
||||
<>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item">{gettext('Tags')}</span>
|
||||
<span className="path-item path-item-read-only">{gettext('Tags')}</span>
|
||||
<span className="path-split">/</span>
|
||||
<TagViewName id={tagId} canSelectAllTags={canSelectAllTags} />
|
||||
{children && (
|
||||
<>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item">{children}</span>
|
||||
<span className="path-item path-item-read-only">{children}</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -173,8 +175,12 @@ class DirPath extends React.Component {
|
||||
turnPathToLink = (path) => {
|
||||
path = path[path.length - 1] === '/' ? path.slice(0, path.length - 1) : path;
|
||||
const pathList = path.split('/');
|
||||
if (pathList.includes(PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) return this.turnViewPathToLink(pathList);
|
||||
if (pathList.includes(PRIVATE_FILE_TYPE.TAGS_PROPERTIES)) return this.turnTagPathToLink(pathList);
|
||||
if (pathList.includes(PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) {
|
||||
return this.turnViewPathToLink(pathList);
|
||||
}
|
||||
if (pathList.includes(PRIVATE_FILE_TYPE.TAGS_PROPERTIES)) {
|
||||
return this.turnTagPathToLink(pathList);
|
||||
}
|
||||
let nodePath = '';
|
||||
let pathElem = pathList.map((item, index) => {
|
||||
if (item === '') return null;
|
||||
@ -182,7 +188,7 @@ class DirPath extends React.Component {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<DirOperationToolBar
|
||||
<DirOperationToolbar
|
||||
path={this.props.currentPath}
|
||||
repoID={this.props.repoID}
|
||||
repoName={this.props.repoName}
|
||||
@ -199,7 +205,7 @@ class DirPath extends React.Component {
|
||||
loadDirentList={this.props.loadDirentList}
|
||||
>
|
||||
<span className="path-file-name">{item}</span>
|
||||
</DirOperationToolBar>
|
||||
</DirOperationToolbar>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
@ -241,19 +247,19 @@ class DirPath extends React.Component {
|
||||
);
|
||||
})}
|
||||
{this.props.pathPrefix && this.props.pathPrefix.length === 0 && (
|
||||
<Fragment>
|
||||
<>
|
||||
<Link to={siteRoot + 'libraries/'} className="path-item normal" onClick={(e) => this.onTabNavClick(e, 'libraries')}>{gettext('Files')}</Link>
|
||||
<span className="path-split">/</span>
|
||||
</Fragment>
|
||||
</>
|
||||
)}
|
||||
{!this.props.pathPrefix && (
|
||||
<Fragment>
|
||||
<>
|
||||
<Link to={siteRoot + 'libraries/'} className="path-item normal" onClick={(e) => this.onTabNavClick(e, 'libraries')}>{gettext('Files')}</Link>
|
||||
<span className="path-split">/</span>
|
||||
</Fragment>
|
||||
</>
|
||||
)}
|
||||
{(currentPath === '/' || currentPath === '') ?
|
||||
<DirOperationToolBar
|
||||
<DirOperationToolbar
|
||||
path={this.props.currentPath}
|
||||
repoID={this.props.repoID}
|
||||
repoName={this.props.repoName}
|
||||
@ -270,7 +276,7 @@ class DirPath extends React.Component {
|
||||
loadDirentList={this.props.loadDirentList}
|
||||
>
|
||||
<span className="path-repo-name">{repoName}</span>
|
||||
</DirOperationToolBar> :
|
||||
</DirOperationToolbar> :
|
||||
<span className="path-item" data-path="/" onClick={this.onPathClick} role="button">{repoName}</span>
|
||||
}
|
||||
{pathElem}
|
||||
|
@ -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 SeahubPopover from '../common/seahub-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,69 +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 right={true}>
|
||||
{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 &&
|
||||
<SeahubPopover
|
||||
popoverClassName="list-tag-popover"
|
||||
target="cur-folder-more-op-toggle"
|
||||
hideSeahubPopover={this.hidePopover}
|
||||
hideSeahubPopoverWithEsc={this.hidePopover}
|
||||
canHideSeahubPopover={true}
|
||||
boundariesElement={document.body}
|
||||
placement={'bottom-end'}
|
||||
>
|
||||
<ListTagPopover
|
||||
repoID={repoID}
|
||||
onListTagCancel={this.toggleCancel}
|
||||
/>
|
||||
</SeahubPopover>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Popover } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyCodes } from '../../constants';
|
||||
import { KeyCodes } from '../constants';
|
||||
import { getEventClassName } from '../utils/dom';
|
||||
|
||||
const propTypes = {
|
||||
target: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
@ -9,17 +10,17 @@ const propTypes = {
|
||||
innerClassName: PropTypes.string,
|
||||
popoverClassName: PropTypes.string,
|
||||
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
||||
hideSeahubPopover: PropTypes.func.isRequired,
|
||||
hideSeahubPopoverWithEsc: PropTypes.func,
|
||||
hidePopover: PropTypes.func.isRequired,
|
||||
hidePopoverWithEsc: PropTypes.func,
|
||||
hideArrow: PropTypes.bool,
|
||||
canHideSeahubPopover: PropTypes.bool,
|
||||
canHidePopover: PropTypes.bool,
|
||||
placement: PropTypes.string,
|
||||
modifiers: PropTypes.object
|
||||
};
|
||||
|
||||
class SeahubPopover extends React.Component {
|
||||
class CustomizePopover extends React.Component {
|
||||
|
||||
SeahubPopoverRef = null;
|
||||
popoverRef = null;
|
||||
isSelectOpen = false;
|
||||
|
||||
componentDidMount() {
|
||||
@ -32,28 +33,23 @@ class SeahubPopover extends React.Component {
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
getEventClassName = (e) => {
|
||||
// svg mouseEvent event.target.className is an object
|
||||
if (!e || !e.target) return '';
|
||||
return e.target.getAttribute('class') || '';
|
||||
};
|
||||
|
||||
onKeyDown = (e) => {
|
||||
const { canHideSeahubPopover, hideSeahubPopoverWithEsc } = this.props;
|
||||
if (e.keyCode === KeyCodes.Escape && typeof hideSeahubPopoverWithEsc === 'function' && !this.isSelectOpen) {
|
||||
const { canHidePopover = true, hidePopoverWithEsc } = this.props;
|
||||
if (e.keyCode === KeyCodes.Escape && typeof hidePopoverWithEsc === 'function' && !this.isSelectOpen) {
|
||||
e.preventDefault();
|
||||
hideSeahubPopoverWithEsc();
|
||||
hidePopoverWithEsc();
|
||||
} else if (e.keyCode === KeyCodes.Enter) {
|
||||
// Resolve the default behavior of the enter key when entering formulas is blocked
|
||||
if (canHideSeahubPopover) return;
|
||||
if (canHidePopover) return;
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (e) => {
|
||||
if (!this.props.canHideSeahubPopover) return;
|
||||
if (this.SeahubPopoverRef && e && this.getEventClassName(e).indexOf('popover') === -1 && !this.SeahubPopoverRef.contains(e.target)) {
|
||||
this.props.hideSeahubPopover(e);
|
||||
const { canHidePopover = true } = this.props;
|
||||
if (!canHidePopover) return;
|
||||
if (this.popoverRef && e && getEventClassName(e).indexOf('popover') === -1 && !this.popoverRef.contains(e.target)) {
|
||||
this.props.hidePopover(e);
|
||||
}
|
||||
};
|
||||
|
||||
@ -63,8 +59,8 @@ class SeahubPopover extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
target, boundariesElement, innerClassName, popoverClassName, hideArrow, modifiers,
|
||||
placement,
|
||||
target, boundariesElement, innerClassName, popoverClassName, hideArrow = true, modifiers,
|
||||
placement = 'bottom-start',
|
||||
} = this.props;
|
||||
let additionalProps = {};
|
||||
if (boundariesElement) {
|
||||
@ -82,7 +78,7 @@ class SeahubPopover extends React.Component {
|
||||
modifiers={modifiers}
|
||||
{...additionalProps}
|
||||
>
|
||||
<div ref={ref => this.SeahubPopoverRef = ref} onClick={this.onPopoverInsideClick}>
|
||||
<div ref={ref => this.popoverRef = ref} onClick={this.onPopoverInsideClick}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Popover>
|
||||
@ -90,12 +86,6 @@ class SeahubPopover extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
SeahubPopover.defaultProps = {
|
||||
placement: 'bottom-start',
|
||||
hideArrow: true,
|
||||
canHideSeahubPopover: true
|
||||
};
|
||||
CustomizePopover.propTypes = propTypes;
|
||||
|
||||
SeahubPopover.propTypes = propTypes;
|
||||
|
||||
export default SeahubPopover;
|
||||
export default CustomizePopover;
|
100
frontend/src/components/customize-select/index.css
Normal file
100
frontend/src/components/customize-select/index.css
Normal file
@ -0,0 +1,100 @@
|
||||
.seafile-customize-select {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0 10px;
|
||||
border-radius: 3px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 900px;
|
||||
user-select: none;
|
||||
text-align: left;
|
||||
line-height: 1.5;
|
||||
background-image: none;
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.seafile-customize-select:focus,
|
||||
.seafile-customize-select.focus {
|
||||
border-color: #1991eb !important;
|
||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
||||
}
|
||||
|
||||
.seafile-customize-select.disabled:focus,
|
||||
.seafile-customize-select.focus.disabled,
|
||||
.seafile-customize-select.disabled:hover {
|
||||
border-color: rgba(0, 40, 100, 0.12) !important;
|
||||
box-shadow: unset;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.seafile-customize-select:hover {
|
||||
cursor: pointer;
|
||||
border-color: rgb(179, 179, 179);
|
||||
}
|
||||
|
||||
.seafile-customize-select .sf3-font-down {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.seafile-customize-select .selected-option {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.seafile-customize-select .selected-option .custom-select-dropdown-icon {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-collaborator .seafile-option-group .seafile-option-group-content,
|
||||
.seafile-customize-select.selector-group .seafile-option-group .seafile-option-group-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-collaborator .seafile-option-group .seafile-option-group-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-collaborator .option {
|
||||
padding: 5px 0 5px 10px !important;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-group .option {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-group .select-group-option {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.seafile-customize-select.selector-group .selected-option .selected-group {
|
||||
padding: 0 2px;
|
||||
background: #eceff4;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.seafile-customize-select .selected-option-show {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.seafile-customize-select .select-placeholder {
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
173
frontend/src/components/customize-select/index.js
Normal file
173
frontend/src/components/customize-select/index.js
Normal file
@ -0,0 +1,173 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ModalPortal from '../modal-portal';
|
||||
import SelectOptionGroup from './select-option-group';
|
||||
import { getEventClassName } from '../../utils/dom';
|
||||
|
||||
import './index.css';
|
||||
|
||||
class CustomizeSelect extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isShowSelectOptions: false
|
||||
};
|
||||
}
|
||||
|
||||
onSelectToggle = (event) => {
|
||||
event.preventDefault();
|
||||
/*
|
||||
if select is showing, click events do not need to be monitored by other click events,
|
||||
so it can be closed when other select is clicked.
|
||||
*/
|
||||
if (this.state.isShowSelectOptions) event.stopPropagation();
|
||||
const eventClassName = getEventClassName(event);
|
||||
if (this.props.readOnly || eventClassName.indexOf('option-search-control') > -1 || eventClassName === 'seafile-option-group-search') return;
|
||||
// Prevent closing by pressing the space bar in the search input
|
||||
if (event.target.value === '') return;
|
||||
this.setState({
|
||||
isShowSelectOptions: !this.state.isShowSelectOptions
|
||||
});
|
||||
};
|
||||
|
||||
onClick = (event) => {
|
||||
if (this.props.isShowSelected && event.target.className.includes('icon-fork-number')) {
|
||||
return;
|
||||
}
|
||||
if (!this.selector.contains(event.target)) {
|
||||
this.closeSelect();
|
||||
}
|
||||
};
|
||||
|
||||
closeSelect = () => {
|
||||
this.setState({ isShowSelectOptions: false });
|
||||
};
|
||||
|
||||
getSelectedOptionTop = () => {
|
||||
if (!this.selector) return 38;
|
||||
const { height } = this.selector.getBoundingClientRect();
|
||||
return height;
|
||||
};
|
||||
|
||||
getFilterOptions = (searchValue) => {
|
||||
const { options, searchable } = this.props;
|
||||
if (!searchable) return options || [];
|
||||
const validSearchVal = searchValue.trim().toLowerCase();
|
||||
if (!validSearchVal) return options || [];
|
||||
return options.filter(option => {
|
||||
const { value, name } = option;
|
||||
if (typeof name === 'string') {
|
||||
return name.toLowerCase().indexOf(validSearchVal) > -1;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
if (value.column) {
|
||||
return value.column.name.toLowerCase().indexOf(validSearchVal) > -1;
|
||||
}
|
||||
if (value.name) {
|
||||
return value.name.toLowerCase().indexOf(validSearchVal) > -1;
|
||||
}
|
||||
return value.columnOption && value.columnOption.name.toLowerCase().indexOf(validSearchVal) > -1;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
renderDropDownIcon = () => {
|
||||
const { readOnly, component } = this.props;
|
||||
if (readOnly) return;
|
||||
const { DropDownIcon } = component || {};
|
||||
if (DropDownIcon) {
|
||||
return (
|
||||
<div className="custom-select-dropdown-icon">{DropDownIcon}</div>
|
||||
);
|
||||
}
|
||||
return (<i className="sf3-font sf3-font-down" aria-hidden="true"></i>);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, value, options, placeholder, searchable, searchPlaceholder, noOptionsPlaceholder,
|
||||
readOnly, isInModal, addOptionAble, component } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(node) => this.selector = node}
|
||||
className={classnames('seafile-customize-select custom-select',
|
||||
{ 'focus': this.state.isShowSelectOptions },
|
||||
{ 'disabled': readOnly },
|
||||
className
|
||||
)}
|
||||
onClick={this.onSelectToggle}>
|
||||
<div className="selected-option">
|
||||
{value && value.label ?
|
||||
<span className="selected-option-show">{value.label}</span>
|
||||
:
|
||||
<span className="select-placeholder">{placeholder}</span>
|
||||
}
|
||||
{this.renderDropDownIcon()}
|
||||
</div>
|
||||
{this.state.isShowSelectOptions && !isInModal && (
|
||||
<SelectOptionGroup
|
||||
value={value}
|
||||
addOptionAble={addOptionAble}
|
||||
component={component}
|
||||
isShowSelected={this.props.isShowSelected}
|
||||
top={this.getSelectedOptionTop()}
|
||||
options={options}
|
||||
onSelectOption={this.props.onSelectOption}
|
||||
searchable={searchable}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
noOptionsPlaceholder={noOptionsPlaceholder}
|
||||
onClickOutside={this.onClick}
|
||||
closeSelect={this.closeSelect}
|
||||
getFilterOptions={this.getFilterOptions}
|
||||
supportMultipleSelect={this.props.supportMultipleSelect}
|
||||
/>
|
||||
)}
|
||||
{this.state.isShowSelectOptions && isInModal && (
|
||||
<ModalPortal>
|
||||
<SelectOptionGroup
|
||||
className={className}
|
||||
value={value}
|
||||
addOptionAble={addOptionAble}
|
||||
component={component}
|
||||
isShowSelected={this.props.isShowSelected}
|
||||
position={this.selector.getBoundingClientRect()}
|
||||
isInModal={isInModal}
|
||||
top={this.getSelectedOptionTop()}
|
||||
options={options}
|
||||
onSelectOption={this.props.onSelectOption}
|
||||
searchable={searchable}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
noOptionsPlaceholder={noOptionsPlaceholder}
|
||||
onClickOutside={this.onClick}
|
||||
closeSelect={this.closeSelect}
|
||||
getFilterOptions={this.getFilterOptions}
|
||||
supportMultipleSelect={this.props.supportMultipleSelect}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomizeSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.object,
|
||||
options: PropTypes.array,
|
||||
placeholder: PropTypes.string,
|
||||
onSelectOption: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
searchable: PropTypes.bool,
|
||||
addOptionAble: PropTypes.bool,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
noOptionsPlaceholder: PropTypes.string,
|
||||
component: PropTypes.object,
|
||||
supportMultipleSelect: PropTypes.bool,
|
||||
isShowSelected: PropTypes.bool,
|
||||
isInModal: PropTypes.bool, // if select component in a modal (option group need ModalPortal to show)
|
||||
};
|
||||
|
||||
export default CustomizeSelect;
|
@ -0,0 +1,103 @@
|
||||
.seafile-option-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
min-height: 60px;
|
||||
max-height: 300px;
|
||||
min-width: 100%;
|
||||
max-width: 15rem;
|
||||
padding: 0.5rem 0;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border-radius: 3px;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.seafile-option-group .seafile-option-group-search {
|
||||
width: 100%;
|
||||
padding: 0 10px 6px 10px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.seafile-option-group-search .form-control {
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.seafile-option-group .none-search-result {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.seafile-option-group .seafile-option-group-content {
|
||||
max-height: 252px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.seafile-select-option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
line-height: 24px;
|
||||
padding: 0.25rem 10px;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
text-align: inherit;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.seafile-select-option.seafile-select-option-active {
|
||||
background-color: #20a0ff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seafile-select-option.seafile-select-option-active .select-option-name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.seafile-select-option:hover .header-icon .seafile-multicolor-icon,
|
||||
.seafile-select-option.seafile-select-option-active .header-icon .seafile-multicolor-icon {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.seafile-select-option:not(.seafile-select-option-active):hover .header-icon .seafile-multicolor-icon {
|
||||
fill: #aaa;
|
||||
}
|
||||
|
||||
.seafile-select-option .select-option-name .single-select-option {
|
||||
margin: 0 0 0 12px;
|
||||
}
|
||||
|
||||
.seafile-select-option .select-option-name .multiple-select-option {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.seafile-option-group-selector-single-select .select-option-name,
|
||||
.seafile-option-group-selector-multiple-select .multiple-option-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.seafile-option-group-selector-multiple-select .multiple-check-icon {
|
||||
display: inline-flex;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.seafile-option-group-selector-multiple-select .multiple-check-icon .seafile-multicolor-icon-check-mark {
|
||||
font-size: 12px;
|
||||
color: #798d99;
|
||||
}
|
||||
|
||||
.seafile-option-group-selector-single-select .seafile-select-option:hover,
|
||||
.seafile-option-group-selector-single-select .seafile-select-option.seafile-select-option-active,
|
||||
.seafile-option-group-selector-multiple-select .seafile-select-option:hover,
|
||||
.seafile-option-group-selector-multiple-select .seafile-select-option.seafile-select-option-active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
@ -0,0 +1,233 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ClickOutside from '../../click-outside';
|
||||
import SearchInput from '../../search-input';
|
||||
import Option from './option';
|
||||
import { KeyCodes } from '../../../constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const OPTION_HEIGHT = 32;
|
||||
|
||||
class SelectOptionGroup extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchVal: '',
|
||||
activeIndex: -1,
|
||||
disableHover: false,
|
||||
};
|
||||
this.filterOptions = null;
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', this.onHotKey);
|
||||
setTimeout(() => {
|
||||
this.resetMenuStyle();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.filterOptions = null;
|
||||
this.timer && clearTimeout(this.timer);
|
||||
window.removeEventListener('keydown', this.onHotKey);
|
||||
}
|
||||
|
||||
resetMenuStyle = () => {
|
||||
const { isInModal, position } = this.props;
|
||||
const { top, height } = this.optionGroupRef.getBoundingClientRect();
|
||||
if (isInModal) {
|
||||
if (position.y + position.height + height > window.innerHeight) {
|
||||
this.optionGroupRef.style.top = (position.y - height) + 'px';
|
||||
}
|
||||
this.optionGroupRef.style.opacity = 1;
|
||||
}
|
||||
else {
|
||||
if (height + top > window.innerHeight) {
|
||||
const borderWidth = 2;
|
||||
this.optionGroupRef.style.top = -1 * (height + borderWidth) + 'px';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onHotKey = (event) => {
|
||||
const keyCode = event.keyCode;
|
||||
if (keyCode === KeyCodes.UpArrow) {
|
||||
this.onPressUp();
|
||||
} else if (keyCode === KeyCodes.DownArrow) {
|
||||
this.onPressDown();
|
||||
} else if (keyCode === KeyCodes.Enter) {
|
||||
let option = this.filterOptions && this.filterOptions[this.state.activeIndex];
|
||||
if (option) {
|
||||
this.props.onSelectOption(option.value);
|
||||
if (!this.props.supportMultipleSelect) {
|
||||
this.props.closeSelect();
|
||||
}
|
||||
}
|
||||
} else if (keyCode === KeyCodes.Tab || keyCode === KeyCodes.Escape) {
|
||||
this.props.closeSelect();
|
||||
}
|
||||
};
|
||||
|
||||
onPressUp = () => {
|
||||
if (this.state.activeIndex > 0) {
|
||||
this.setState({ activeIndex: this.state.activeIndex - 1 }, () => {
|
||||
this.scrollContent();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onPressDown = () => {
|
||||
if (this.filterOptions && this.state.activeIndex < this.filterOptions.length - 1) {
|
||||
this.setState({ activeIndex: this.state.activeIndex + 1 }, () => {
|
||||
this.scrollContent();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (e) => {
|
||||
const { isInModal } = this.props;
|
||||
// prevent event propagation when click option or search input
|
||||
if (isInModal) {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
scrollContent = () => {
|
||||
const { offsetHeight, scrollTop } = this.optionGroupContentRef;
|
||||
this.setState({ disableHover: true });
|
||||
this.timer = setTimeout(() => {
|
||||
this.setState({ disableHover: false });
|
||||
}, 500);
|
||||
if (this.state.activeIndex * OPTION_HEIGHT === 0) {
|
||||
this.optionGroupContentRef.scrollTop = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.activeIndex * OPTION_HEIGHT < scrollTop) {
|
||||
this.optionGroupContentRef.scrollTop = scrollTop - OPTION_HEIGHT;
|
||||
}
|
||||
else if (this.state.activeIndex * OPTION_HEIGHT > offsetHeight + scrollTop) {
|
||||
this.optionGroupContentRef.scrollTop = scrollTop + OPTION_HEIGHT;
|
||||
}
|
||||
};
|
||||
|
||||
changeIndex = (index) => {
|
||||
this.setState({ activeIndex: index });
|
||||
};
|
||||
|
||||
onChangeSearch = (searchVal) => {
|
||||
let value = searchVal || '';
|
||||
if (value !== this.state.searchVal) {
|
||||
this.setState({ searchVal: value, activeIndex: -1, });
|
||||
}
|
||||
};
|
||||
|
||||
renderOptGroup = (searchVal) => {
|
||||
let { noOptionsPlaceholder, onSelectOption } = this.props;
|
||||
this.filterOptions = this.props.getFilterOptions(searchVal);
|
||||
if (this.filterOptions.length === 0) {
|
||||
return (
|
||||
<div className="none-search-result">{noOptionsPlaceholder}</div>
|
||||
);
|
||||
}
|
||||
return this.filterOptions.map((opt, i) => {
|
||||
let key = opt.value.column ? opt.value.column.key : i;
|
||||
let isActive = this.state.activeIndex === i;
|
||||
return (
|
||||
<Option
|
||||
key={key}
|
||||
index={i}
|
||||
isActive={isActive}
|
||||
value={opt.value}
|
||||
onSelectOption={onSelectOption}
|
||||
changeIndex={this.changeIndex}
|
||||
supportMultipleSelect={this.props.supportMultipleSelect}
|
||||
disableHover={this.state.disableHover}
|
||||
>
|
||||
{opt.label}
|
||||
</Option>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { searchable, searchPlaceholder, top, left, minWidth, value, isShowSelected, isInModal, position,
|
||||
className, addOptionAble, component } = this.props;
|
||||
const { AddOption } = component || {};
|
||||
let { searchVal } = this.state;
|
||||
let style = { top: top || 0, left: left || 0 };
|
||||
if (minWidth) {
|
||||
style = { top: top || 0, left: left || 0, minWidth };
|
||||
}
|
||||
if (isInModal) {
|
||||
style = {
|
||||
position: 'fixed',
|
||||
left: position.x,
|
||||
top: position.y + position.height,
|
||||
minWidth: position.width,
|
||||
opacity: 0,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.props.onClickOutside}>
|
||||
<div
|
||||
className={classnames('seafile-option-group', className ? 'seafile-option-group-' + className : '', {
|
||||
'pt-0': isShowSelected,
|
||||
'create-new-seafile-option-group': addOptionAble,
|
||||
})}
|
||||
ref={(ref) => this.optionGroupRef = ref}
|
||||
style={style}
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
{isShowSelected &&
|
||||
<div className="editor-list-delete mb-2" onClick={(e) => e.stopPropagation()}>{value.label || ''}</div>
|
||||
}
|
||||
{searchable && (
|
||||
<div className="seafile-option-group-search">
|
||||
<SearchInput
|
||||
className="option-search-control"
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={this.onChangeSearch}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="seafile-option-group-content" ref={(ref) => this.optionGroupContentRef = ref}>
|
||||
{this.renderOptGroup(searchVal)}
|
||||
</div>
|
||||
{addOptionAble && AddOption}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectOptionGroup.propTypes = {
|
||||
top: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
minWidth: PropTypes.number,
|
||||
options: PropTypes.array,
|
||||
onSelectOption: PropTypes.func,
|
||||
searchable: PropTypes.bool,
|
||||
addOptionAble: PropTypes.bool,
|
||||
component: PropTypes.object,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
noOptionsPlaceholder: PropTypes.string,
|
||||
onClickOutside: PropTypes.func.isRequired,
|
||||
closeSelect: PropTypes.func.isRequired,
|
||||
getFilterOptions: PropTypes.func.isRequired,
|
||||
supportMultipleSelect: PropTypes.bool,
|
||||
value: PropTypes.object,
|
||||
isShowSelected: PropTypes.bool,
|
||||
stopClickEvent: PropTypes.bool,
|
||||
isInModal: PropTypes.bool,
|
||||
position: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SelectOptionGroup;
|
@ -0,0 +1,50 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
class Option extends Component {
|
||||
|
||||
onSelectOption = (value, event) => {
|
||||
if (this.props.supportMultipleSelect) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.props.onSelectOption(value, event);
|
||||
};
|
||||
|
||||
onMouseEnter = () => {
|
||||
if (!this.props.disableHover) {
|
||||
this.props.changeIndex(this.props.index);
|
||||
}
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
if (!this.props.disableHover) {
|
||||
this.props.changeIndex(-1);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={classnames('seafile-select-option', { 'seafile-select-option-active': this.props.isActive })}
|
||||
onClick={this.onSelectOption.bind(this, this.props.value)}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Option.propTypes = {
|
||||
index: PropTypes.number,
|
||||
isActive: PropTypes.bool,
|
||||
changeIndex: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
onSelectOption: PropTypes.func,
|
||||
supportMultipleSelect: PropTypes.bool,
|
||||
disableHover: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Option;
|
@ -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
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Form, FormGroup, Label, Input, Modal, ModalBody, ModalFooter, Alert } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { SeahubSelect } from '../common/select';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import toaster from '../toast';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
@ -24,6 +25,12 @@ class AddAbuseReportDialog extends React.Component {
|
||||
reporter: this.props.contactEmail,
|
||||
errMessage: '',
|
||||
};
|
||||
this.typeOptions = [
|
||||
{ value: 'copyright', label: gettext('Copyright Infringement') },
|
||||
{ value: 'virus', label: gettext('Virus') },
|
||||
{ value: 'abuse_content', label: gettext('Abuse Content') },
|
||||
{ value: 'other', label: gettext('Other') },
|
||||
];
|
||||
}
|
||||
|
||||
onAbuseReport = () => {
|
||||
@ -45,8 +52,8 @@ class AddAbuseReportDialog extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
onAbuseTypeChange = (event) => {
|
||||
let type = event.target.value;
|
||||
onAbuseTypeChange = (option) => {
|
||||
let type = option.value;
|
||||
if (type === this.state.abuseType) {
|
||||
return;
|
||||
}
|
||||
@ -70,13 +77,13 @@ class AddAbuseReportDialog extends React.Component {
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label for="abuse-type-select">{gettext('Abuse Type')}</Label>
|
||||
<Input type="select" id="abuse-type-select" onChange={(event) => this.onAbuseTypeChange(event)}>
|
||||
<option value='copyright'>{gettext('Copyright Infringement')}</option>
|
||||
<option value='virus'>{gettext('Virus')}</option>
|
||||
<option value='abuse_content'>{gettext('Abuse Content')}</option>
|
||||
<option value='other'>{gettext('Other')}</option>
|
||||
</Input>
|
||||
<Label>{gettext('Abuse Type')}</Label>
|
||||
<SeahubSelect
|
||||
options={this.typeOptions}
|
||||
value={this.typeOptions.find(option => option.value === this.state.abuseType) || this.typeOptions[0]}
|
||||
onChange={this.onAbuseTypeChange}
|
||||
isClearable={false}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Contact Information')}</Label>
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Modal, ModalBody, ModalFooter, Input, Button } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Label, Input, Button } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
@ -68,7 +68,7 @@ class CreateGroupDialog extends React.Component {
|
||||
<Modal isOpen={true} toggle={this.props.toggleDialog} autoFocus={false}>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog}>{gettext('New Group')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<label htmlFor="groupName">{gettext('Name')}</label>
|
||||
<Label for="groupName">{gettext('Name')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="groupName"
|
||||
|
@ -117,8 +117,7 @@ class CreateRepoDialog extends React.Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
onPermissionChange = (e) => {
|
||||
let permission = e.target.value;
|
||||
onPermissionChange = (permission) => {
|
||||
this.setState({ permission: permission });
|
||||
};
|
||||
|
||||
@ -223,11 +222,19 @@ class CreateRepoDialog extends React.Component {
|
||||
|
||||
{this.props.libraryType === 'group' && (
|
||||
<FormGroup>
|
||||
<Label for="exampleSelect">{gettext('Permission')}</Label>
|
||||
<Input type="select" name="select" id="exampleSelect" onChange={this.onPermissionChange} value={this.state.permission}>
|
||||
<option value='rw'>{gettext('Read-Write')}</option>
|
||||
<option value='r'>{gettext('Read-Only')}</option>
|
||||
</Input>
|
||||
<Label>{gettext('Permission')}</Label>
|
||||
<SeahubSelect
|
||||
options={[
|
||||
{ value: 'rw', label: gettext('Read-Write') },
|
||||
{ value: 'r', label: gettext('Read-Only') }
|
||||
]}
|
||||
onChange={selectedOption => this.onPermissionChange(selectedOption.value)}
|
||||
value={{
|
||||
value: this.state.permission,
|
||||
label: this.state.permission === 'rw' ? gettext('Read-Write') : gettext('Read-Only')
|
||||
}}
|
||||
isClearable={false}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{enableEncryptedLibrary &&
|
||||
|
@ -1,126 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, ModalBody, ModalFooter, Input } 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 className="form-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 className="form-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;
|
@ -5,19 +5,7 @@ import { gettext } from '../../../utils/constants';
|
||||
import Loading from '../../loading';
|
||||
import OpIcon from '../../op-icon';
|
||||
|
||||
const propTypes = {
|
||||
mode: PropTypes.string,
|
||||
permission: PropTypes.object,
|
||||
onChangeMode: PropTypes.func.isRequired,
|
||||
onUpdateCustomPermission: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class CustomPermissionEditor extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
mode: 'add'
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@ -50,7 +38,6 @@ class CustomPermissionEditor extends React.Component {
|
||||
} else {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onChangePermissionName = (evt) => {
|
||||
@ -108,12 +95,9 @@ class CustomPermissionEditor extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
const { mode } = this.props;
|
||||
const { mode = 'add' } = this.props;
|
||||
const title = mode === 'add' ? gettext('Add permission') : gettext('Edit permission');
|
||||
|
||||
const { isLoading, permission_name, permission_desc, permission, errMessage } = this.state;
|
||||
|
||||
return (
|
||||
<div className="custom-permission">
|
||||
<div className="permission-header">
|
||||
@ -212,6 +196,11 @@ class CustomPermissionEditor extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
CustomPermissionEditor.propTypes = propTypes;
|
||||
CustomPermissionEditor.propTypes = {
|
||||
mode: PropTypes.string,
|
||||
permission: PropTypes.object,
|
||||
onChangeMode: PropTypes.func.isRequired,
|
||||
onUpdateCustomPermission: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CustomPermissionEditor;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, { Fragment, } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext, isOrgContext, username } from '../../utils/constants';
|
||||
import { Modal, ModalBody } from 'reactstrap';
|
||||
import { gettext, isOrgContext, username } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api.js';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../../components/toast';
|
||||
import EmptyTip from '../../components/empty-tip';
|
||||
import Loading from '../../components/loading';
|
||||
import toaster from '../toast';
|
||||
import EmptyTip from '../empty-tip';
|
||||
import Loading from '../loading';
|
||||
import Department from '../../models/department';
|
||||
import SeahubModalHeader from '../common/seahub-modal-header';
|
||||
import DepartmentGroup from './department-detail-widget/department-group';
|
||||
|
@ -2,8 +2,8 @@ import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tooltip } from 'reactstrap';
|
||||
import { gettext, mediaUrl } from '../../../utils/constants';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
import Loading from '../../../components/loading';
|
||||
import EmptyTip from '../../empty-tip';
|
||||
import Loading from '../../loading';
|
||||
|
||||
const ItemPropTypes = {
|
||||
member: PropTypes.object,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Loading from '../../../components/loading';
|
||||
import Loading from '../../loading';
|
||||
import { isOrgContext } from '../../../utils/constants';
|
||||
|
||||
const ItemPropTypes = {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, Alert } from 'reactstrap';
|
||||
import { Button, Form, FormGroup, Label, Input, InputGroup, Alert } from 'reactstrap';
|
||||
import { gettext, shareLinkForceUsePassword, shareLinkPasswordMinLength, shareLinkPasswordStrengthLevel, canSendShareLinkEmail, uploadLinkExpireDaysMin, uploadLinkExpireDaysMax, uploadLinkExpireDaysDefault } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
@ -300,14 +300,12 @@ class GenerateUploadLink extends React.Component {
|
||||
<Input type="text" readOnly={true} value={sharedUploadInfo.password} /> :
|
||||
<Input type="text" readOnly={true} value={'***************'} />
|
||||
}
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
aria-label={this.state.storedPasswordVisible ? gettext('Hide') : gettext('Show')}
|
||||
onClick={this.toggleStoredPasswordVisible}
|
||||
className={`link-operation-icon eye-icon sf3-font sf3-font-eye${this.state.storedPasswordVisible ? '' : '-slash'}`}
|
||||
>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button
|
||||
aria-label={this.state.storedPasswordVisible ? gettext('Hide') : gettext('Show')}
|
||||
onClick={this.toggleStoredPasswordVisible}
|
||||
className={`link-operation-icon eye-icon sf3-font sf3-font-eye${this.state.storedPasswordVisible ? '' : '-slash'}`}
|
||||
>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</dd>
|
||||
</>
|
||||
@ -338,15 +336,13 @@ class GenerateUploadLink extends React.Component {
|
||||
) : (
|
||||
<InputGroup className="share-link-details-item">
|
||||
<Input type="text" readOnly={true} value={dayjs(sharedUploadInfo.expire_date).format('YYYY-MM-DD HH:mm:ss')} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
aria-label={gettext('Edit')}
|
||||
title={gettext('Edit')}
|
||||
className="link-operation-icon sf3-font sf3-font-rename"
|
||||
onClick={this.editExpirationToggle}
|
||||
>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button
|
||||
aria-label={gettext('Edit')}
|
||||
title={gettext('Edit')}
|
||||
className="link-operation-icon sf3-font sf3-font-rename"
|
||||
onClick={this.editExpirationToggle}
|
||||
>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
)}
|
||||
</dd>
|
||||
@ -389,14 +385,12 @@ class GenerateUploadLink extends React.Component {
|
||||
<span className="tip">{passwordLengthTip}</span>
|
||||
<InputGroup style={{ width: inputWidth }}>
|
||||
<Input id="passwd" type={this.state.passwordVisible ? 'text' : 'password'} value={this.state.password || ''} onChange={this.inputPassword} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`link-operation-icon sf3-font sf3-font-eye${this.state.passwordVisible ? '' : '-slash'}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="link-operation-icon sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`link-operation-icon sf3-font sf3-font-eye${this.state.passwordVisible ? '' : '-slash'}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="link-operation-icon sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
|
111
frontend/src/components/dialog/group-invite-members-dialog.js
Normal file
111
frontend/src/components/dialog/group-invite-members-dialog.js
Normal file
@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalBody, Label } from 'reactstrap';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import toaster from '../toast';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
|
||||
import '../../css/group-invite-members-dialog.css';
|
||||
|
||||
const propTypes = {
|
||||
groupID: PropTypes.number.isRequired,
|
||||
toggleInviteMembersDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class GroupInviteMembersDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
inviteList: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.listInviteLinks();
|
||||
}
|
||||
|
||||
listInviteLinks = () => {
|
||||
seafileAPI.getGroupInviteLinks(this.props.groupID).then((res) => {
|
||||
this.setState({ inviteList: res.data.group_invite_link_list });
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
addInviteLink = () => {
|
||||
seafileAPI.addGroupInviteLinks(this.props.groupID).then(() => {
|
||||
this.listInviteLinks();
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
deleteLink = (token) => {
|
||||
seafileAPI.deleteGroupInviteLinks(this.props.groupID, token).then(() => {
|
||||
this.listInviteLinks();
|
||||
}).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
onError = (error) => {
|
||||
let errMsg = Utils.getErrorMsg(error, true);
|
||||
if (!error.response || error.response.status !== 403) {
|
||||
toaster.danger(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
copyLink = () => {
|
||||
const inviteLinkItem = this.state.inviteList[0];
|
||||
copy(inviteLinkItem.link);
|
||||
const message = gettext('Invitation link has been copied to clipboard');
|
||||
toaster.success((message), {
|
||||
duration: 2
|
||||
});
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.props.toggleInviteMembersDialog();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { inviteList } = this.state;
|
||||
const link = inviteList[0];
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle} className="group-invite-members">
|
||||
<SeahubModalHeader toggle={this.toggle}>{gettext('Invite members')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
{link ?
|
||||
<>
|
||||
<Label>{gettext('Group invitation link')}</Label>
|
||||
<div className="invite-link-item">
|
||||
<div className="form-item text-truncate">{link.link}</div>
|
||||
<div className="invite-link-copy">
|
||||
<Button color="primary" onClick={this.copyLink} className="invite-link-copy-btn text-truncate">{gettext('Copy')}</Button>
|
||||
</div>
|
||||
<Button color="primary" outline onClick={this.deleteLink.bind(this, link.token)} className="delete-link-btn ml-2">
|
||||
<i className="sf3-font-delete1 sf3-font"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<div className="no-link-tip mb-4">
|
||||
{gettext('No group invitation link yet. Group invitation link let registered users to join the group by clicking a link.')}
|
||||
</div>
|
||||
<Button color="primary" onClick={this.addInviteLink} className="my-4">{gettext('Generate')}</Button>
|
||||
</>
|
||||
}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupInviteMembersDialog.propTypes = propTypes;
|
||||
|
||||
export default GroupInviteMembersDialog;
|
@ -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
|
||||
};
|
||||
|
||||
|
100
frontend/src/components/dialog/image-dialog/index.css
Normal file
100
frontend/src/components/dialog/image-dialog/index.css
Normal file
@ -0,0 +1,100 @@
|
||||
.lightbox-side-panel {
|
||||
width: 10px;
|
||||
height: calc(100% - 100px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
font-size: 1rem;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
border: 1px solid #666;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .cur-view-detail {
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .side-panel-controller {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: -40px;
|
||||
width: 40px;
|
||||
height: 48px;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
border: 1px solid #666;
|
||||
border-right: none;
|
||||
border-top-left-radius: 50%;
|
||||
border-bottom-left-radius: 50%;
|
||||
margin-right: -1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .side-panel-controller .expand-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
opacity: 0.7;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .side-panel-controller:hover .expand-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .detail-header {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
padding: 20px 16px;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .detail-header .detail-title .name,
|
||||
.lightbox-side-panel .file-details-collapse .file-details-collapse-header .file-details-collapse-header-title,
|
||||
.lightbox-side-panel .dirent-detail-item .dirent-detail-item-name,
|
||||
.lightbox-side-panel .sf-metadata-number-property-detail-editor {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .detail-body {
|
||||
scrollbar-color: #666 #333;
|
||||
padding: 0 16px 8px;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .sf-metadata-ui.collaborator-item,
|
||||
.lightbox-side-panel .sf-metadata-text-property-detail-editor:not(.formatter),
|
||||
.lightbox-side-panel .sf-metadata-number-property-detail-editor:focus {
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .sf-metadata-text-property-detail-editor:not(.formatter) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .dirent-detail-item .dirent-detail-item-value:not(.editable) .sf-metadata-record-cell-empty:empty::before,
|
||||
.lightbox-side-panel .sf-metadata-property-detail-editor:empty::before,
|
||||
.lightbox-side-panel .sf-metadata-property-detail-capture-information-item .dirent-detail-item-value:empty::before,
|
||||
.lightbox-side-panel .file-details-collapse .file-details-collapse-header .sf3-font-down,
|
||||
.lightbox-side-panel .sf-metadata-number-property-detail-editor::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.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.editable:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.lightbox-side-panel .detail-body .dirent-detail-people {
|
||||
position: relative;
|
||||
transform: none;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
@ -1,13 +1,21 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import Lightbox from '@seafile/react-image-lightbox';
|
||||
import { useMetadataAIOperations } from '../../hooks/metadata-ai-operation';
|
||||
import { SYSTEM_FOLDERS } from '../../constants';
|
||||
import { useMetadataAIOperations } from '../../../hooks/metadata-ai-operation';
|
||||
import EmbeddedFileDetails from '../../dirent-detail/embedded-file-details';
|
||||
import { SYSTEM_FOLDERS } from '../../../constants';
|
||||
import Icon from '../../icon';
|
||||
|
||||
import '@seafile/react-image-lightbox/style.css';
|
||||
import './index.css';
|
||||
|
||||
const SIDE_PANEL_COLLAPSED_WIDTH = 10;
|
||||
const SIDE_PANEL_EXPANDED_WIDTH = 300;
|
||||
|
||||
const ImageDialog = ({ repoID, repoInfo, enableRotate: oldEnableRotate = true, imageItems, imageIndex, closeImagePopup, moveToPrevImage, moveToNextImage, onDeleteImage, onRotateImage, isCustomPermission }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const ImageDialog = ({ enableRotate: oldEnableRotate, imageItems, imageIndex, closeImagePopup, moveToPrevImage, moveToNextImage, onDeleteImage, onRotateImage }) => {
|
||||
const { enableOCR, enableMetadata, canModify, onOCR: onOCRAPI, OCRSuccessCallBack } = useMetadataAIOperations();
|
||||
|
||||
const downloadImage = useCallback((url) => {
|
||||
@ -18,18 +26,23 @@ const ImageDialog = ({ enableRotate: oldEnableRotate, imageItems, imageIndex, cl
|
||||
window.open(imageItems[imageIndex].url, '_blank');
|
||||
}, [imageItems, imageIndex]);
|
||||
|
||||
const onToggleSidePanel = useCallback(() => {
|
||||
setExpanded(!expanded);
|
||||
}, [expanded]);
|
||||
|
||||
const imageItemsLength = imageItems.length;
|
||||
if (imageItemsLength === 0) return null;
|
||||
const id = imageItems[imageIndex].id;
|
||||
const name = imageItems[imageIndex].name;
|
||||
const mainImg = imageItems[imageIndex];
|
||||
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;
|
||||
}
|
||||
|
||||
@ -39,6 +52,28 @@ const ImageDialog = ({ enableRotate: oldEnableRotate, imageItems, imageIndex, cl
|
||||
onOCR = () => onOCRAPI({ parentDir: mainImg.parentDir, fileName: mainImg.name }, { success_callback: OCRSuccessCallBack });
|
||||
}
|
||||
|
||||
const renderSidePanel = () => {
|
||||
return (
|
||||
<div
|
||||
className="lightbox-side-panel"
|
||||
style={{ width: expanded ? SIDE_PANEL_EXPANDED_WIDTH : SIDE_PANEL_COLLAPSED_WIDTH }}
|
||||
aria-expanded={expanded}
|
||||
>
|
||||
<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={mainImg.parentDir}
|
||||
dirent={{ id, name, type: 'file' }}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Lightbox
|
||||
wrapperClassName='custom-image-previewer'
|
||||
@ -64,6 +99,10 @@ const ImageDialog = ({ enableRotate: oldEnableRotate, imageItems, imageIndex, cl
|
||||
onRotateImage={(onRotateImage && enableRotate) ? (angle) => onRotateImage(imageIndex, angle) : null}
|
||||
onOCR={onOCR}
|
||||
OCRLabel={gettext('OCR')}
|
||||
sidePanel={!isCustomPermission ? {
|
||||
render: renderSidePanel,
|
||||
width: expanded ? SIDE_PANEL_EXPANDED_WIDTH : SIDE_PANEL_COLLAPSED_WIDTH,
|
||||
} : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -79,8 +118,4 @@ ImageDialog.propTypes = {
|
||||
enableRotate: PropTypes.bool,
|
||||
};
|
||||
|
||||
ImageDialog.defaultProps = {
|
||||
enableRotate: true,
|
||||
};
|
||||
|
||||
export default ImageDialog;
|
@ -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} />
|
||||
|
@ -1,94 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import FileChooser from '../file-chooser';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
import '../../css/insert-repo-image-dialog.css';
|
||||
|
||||
const { siteRoot, serviceUrl } = window.app.config;
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
filePath: PropTypes.string.isRequired,
|
||||
toggleCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class InsertRepoImageDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
repo: null,
|
||||
selectedPath: '',
|
||||
};
|
||||
}
|
||||
|
||||
insertImage = () => {
|
||||
const url = serviceUrl + '/lib/' + this.state.repo.repo_id + '/file' + Utils.encodePath(this.state.selectedPath) + '?raw=1';
|
||||
window.richMarkdownEditor.onInsertImage(url);
|
||||
this.props.toggleCancel();
|
||||
};
|
||||
|
||||
onDirentItemClick = (repo, selectedPath, dirent) => {
|
||||
if (dirent.type === 'file' && Utils.imageCheck(dirent.name)) {
|
||||
this.setState({
|
||||
repo: repo,
|
||||
selectedPath: selectedPath,
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.setState({ repo: null, selectedPath: '' });
|
||||
}
|
||||
};
|
||||
|
||||
onRepoItemClick = () => {
|
||||
this.setState({ repo: null, selectedPath: '' });
|
||||
};
|
||||
|
||||
render() {
|
||||
const toggle = this.props.toggleCancel;
|
||||
const fileSuffixes = ['jpg', 'png', 'jpeg', 'gif', 'bmp'];
|
||||
let imageUrl;
|
||||
if (this.state.repo) {
|
||||
imageUrl = siteRoot + 'thumbnail/' + this.state.repo.repo_id + '/1024' + this.state.selectedPath;
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={true} toggle={toggle} size='lg'>
|
||||
<SeahubModalHeader toggle={toggle}>{gettext('Select Image')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<div className="d-flex">
|
||||
<div className="col-6">
|
||||
<FileChooser
|
||||
isShowFile={true}
|
||||
repoID={this.props.repoID}
|
||||
onDirentItemClick={this.onDirentItemClick}
|
||||
onRepoItemClick={this.onRepoItemClick}
|
||||
mode="current_repo_and_other_repos"
|
||||
fileSuffixes={fileSuffixes}
|
||||
/>
|
||||
</div>
|
||||
<div className="insert-image-container col-6">
|
||||
{imageUrl ?
|
||||
<img src={imageUrl} className='d-inline-block mh-100 mw-100' alt=''/> :
|
||||
<span>{gettext('No preview')}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={toggle}>{gettext('Cancel')}</Button>
|
||||
{this.state.selectedPath ?
|
||||
<Button color="primary" onClick={this.insertImage}>{gettext('Submit')}</Button>
|
||||
: <Button color="primary" disabled>{gettext('Submit')}</Button>
|
||||
}
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InsertRepoImageDialog.propTypes = propTypes;
|
||||
|
||||
export default InsertRepoImageDialog;
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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) => {
|
||||
@ -46,12 +40,11 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
|
||||
|
||||
const { encrypted, is_admin } = currentRepoInfo;
|
||||
const { enableMetadataManagement } = window.app.pageOptions;
|
||||
const { enableFaceRecognition, updateEnableFaceRecognition } = useMetadata();
|
||||
const { enableMetadata, updateEnableMetadata, enableTags, tagsLang, updateEnableTags, enableOCR, updateEnableOCR } = useMetadataStatus();
|
||||
const { updateEnableFaceRecognition } = useMetadata();
|
||||
const { enableMetadata, updateEnableMetadata, enableTags, tagsLang, updateEnableTags, enableOCR, updateEnableOCR, enableFaceRecognition } = useMetadataStatus();
|
||||
const enableHistorySetting = is_admin; // repo owner, admin of the department which the repo belongs to, and ...
|
||||
const enableAutoDelSetting = is_admin && enableRepoAutoDel;
|
||||
const enableExtendedPropertiesSetting = !encrypted && is_admin && enableMetadataManagement;
|
||||
const enableMetadataOtherSettings = enableExtendedPropertiesSetting && enableMetadata;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -63,48 +56,102 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
|
||||
<Fragment>
|
||||
<div className="lib-setting-nav p-4">
|
||||
<Nav pills className="flex-column">
|
||||
{enableHistorySetting && (
|
||||
<NavItem role="tab" aria-selected={activeTab === TAB.HISTORY_SETTING} aria-controls="history-setting-panel">
|
||||
<NavLink className={activeTab === TAB.HISTORY_SETTING ? 'active' : ''} onClick={(toggleTab.bind(this, TAB.HISTORY_SETTING))} tabIndex="0" onKeyDown={onTabKeyDown}>
|
||||
{enableHistorySetting &&
|
||||
<NavItem
|
||||
role="tab"
|
||||
aria-selected={activeTab === TAB.HISTORY_SETTING}
|
||||
aria-controls="history-setting-panel"
|
||||
>
|
||||
<NavLink
|
||||
className={activeTab === TAB.HISTORY_SETTING ? 'active' : ''}
|
||||
onClick={toggleTab.bind(this, TAB.HISTORY_SETTING)}
|
||||
tabIndex="0"
|
||||
onKeyDown={onTabKeyDown}
|
||||
>
|
||||
{gettext('History')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
)}
|
||||
{enableAutoDelSetting && (
|
||||
<NavItem role="tab" aria-selected={activeTab === TAB.AUTO_DEL_SETTING} aria-controls="auto-del-setting-panel">
|
||||
<NavLink className={activeTab === TAB.AUTO_DEL_SETTING ? 'active' : ''} onClick={toggleTab.bind(this, TAB.AUTO_DEL_SETTING)} tabIndex="0" onKeyDown={onTabKeyDown}>
|
||||
}
|
||||
{enableAutoDelSetting &&
|
||||
<NavItem
|
||||
role="tab"
|
||||
aria-selected={activeTab === TAB.AUTO_DEL_SETTING}
|
||||
aria-controls="auto-del-setting-panel"
|
||||
>
|
||||
<NavLink
|
||||
className={activeTab === TAB.AUTO_DEL_SETTING ? 'active' : ''}
|
||||
onClick={toggleTab.bind(this, TAB.AUTO_DEL_SETTING)}
|
||||
tabIndex="0"
|
||||
onKeyDown={onTabKeyDown}
|
||||
>
|
||||
{gettext('Auto deletion')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
)}
|
||||
{enableExtendedPropertiesSetting && (
|
||||
<NavItem role="tab" aria-selected={activeTab === TAB.EXTENDED_PROPERTIES_SETTING} aria-controls="extended-properties-setting-panel">
|
||||
<NavLink className={activeTab === TAB.EXTENDED_PROPERTIES_SETTING ? 'active' : ''} onClick={toggleTab.bind(this, TAB.EXTENDED_PROPERTIES_SETTING)} tabIndex="0" onKeyDown={onTabKeyDown}>
|
||||
{gettext('Extended properties')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
)}
|
||||
{enableMetadataOtherSettings && (
|
||||
<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}>
|
||||
{gettext('Face recognition')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
)}
|
||||
{enableMetadataOtherSettings && (
|
||||
<NavItem role="tab" aria-selected={activeTab === TAB.TAGS_SETTING} aria-controls="tags-setting-panel">
|
||||
<NavLink className={activeTab === TAB.TAGS_SETTING ? 'active' : ''} onClick={toggleTab.bind(this, TAB.TAGS_SETTING)} tabIndex="0" onKeyDown={onTabKeyDown}>
|
||||
{gettext('Tags')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
)}
|
||||
{enableMetadataOtherSettings && (
|
||||
<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}>
|
||||
{gettext('OCR')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
)}
|
||||
}
|
||||
{enableExtendedPropertiesSetting &&
|
||||
<>
|
||||
<NavItem
|
||||
role="tab"
|
||||
aria-selected={activeTab === TAB.EXTENDED_PROPERTIES_SETTING}
|
||||
aria-controls="extended-properties-setting-panel"
|
||||
>
|
||||
<NavLink
|
||||
className={activeTab === TAB.EXTENDED_PROPERTIES_SETTING ? 'active' : ''}
|
||||
onClick={toggleTab.bind(this, TAB.EXTENDED_PROPERTIES_SETTING)}
|
||||
tabIndex="0"
|
||||
onKeyDown={onTabKeyDown}
|
||||
>
|
||||
{gettext('Extended properties')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{enableSeafileAI &&
|
||||
<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}
|
||||
>
|
||||
{gettext('Face recognition')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
}
|
||||
<NavItem
|
||||
role="tab"
|
||||
aria-selected={activeTab === TAB.TAGS_SETTING}
|
||||
aria-controls="tags-setting-panel"
|
||||
>
|
||||
<NavLink
|
||||
className={activeTab === TAB.TAGS_SETTING ? 'active' : ''}
|
||||
onClick={toggleTab.bind(this, TAB.TAGS_SETTING)}
|
||||
tabIndex="0"
|
||||
onKeyDown={onTabKeyDown}
|
||||
>
|
||||
{gettext('Tags')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{enableSeafileAI && enableSeafileOCR &&
|
||||
<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}
|
||||
>
|
||||
{gettext('OCR')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</Nav>
|
||||
</div>
|
||||
<TabContent activeTab={activeTab} className="flex-fill">
|
||||
@ -134,17 +181,18 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
|
||||
/>
|
||||
</TabPane>
|
||||
)}
|
||||
{(enableMetadataOtherSettings && activeTab === TAB.FACE_RECOGNITION_SETTING) && (
|
||||
{(enableExtendedPropertiesSetting && activeTab === TAB.FACE_RECOGNITION_SETTING) && (
|
||||
<TabPane tabId={TAB.FACE_RECOGNITION_SETTING} role="tabpanel" id="face-recognition-setting-panel">
|
||||
<LibFaceRecognitionSettingPanel
|
||||
repoID={repoID}
|
||||
value={enableFaceRecognition}
|
||||
submit={updateEnableFaceRecognition}
|
||||
toggleDialog={toggleDialog}
|
||||
enableMetadata={enableMetadata}
|
||||
/>
|
||||
</TabPane>
|
||||
)}
|
||||
{(enableMetadataOtherSettings && activeTab === TAB.TAGS_SETTING) && (
|
||||
{(enableExtendedPropertiesSetting && activeTab === TAB.TAGS_SETTING) && (
|
||||
<TabPane tabId={TAB.TAGS_SETTING} role="tabpanel" id="tags-setting-panel">
|
||||
<LibMetadataTagsStatusSettingPanel
|
||||
repoID={repoID}
|
||||
@ -152,10 +200,12 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
|
||||
lang={tagsLang}
|
||||
submit={updateEnableTags}
|
||||
toggleDialog={toggleDialog}
|
||||
enableMetadata={enableMetadata}
|
||||
showMigrateTip={showMigrateTip}
|
||||
/>
|
||||
</TabPane>
|
||||
)}
|
||||
{(enableMetadataOtherSettings && activeTab === TAB.OCR_SETTING) && (
|
||||
{(enableExtendedPropertiesSetting && activeTab === TAB.OCR_SETTING) && (
|
||||
<TabPane tabId={TAB.OCR_SETTING} role="tabpanel" id="ocr-setting-panel">
|
||||
<LibMetadataOCRStatusSettingPanel
|
||||
repoID={repoID}
|
||||
@ -163,6 +213,7 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
|
||||
lang={tagsLang}
|
||||
submit={updateEnableOCR}
|
||||
toggleDialog={toggleDialog}
|
||||
enableMetadata={enableMetadata}
|
||||
/>
|
||||
</TabPane>
|
||||
)}
|
||||
@ -177,5 +228,3 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => {
|
||||
LibSettingsDialog.propTypes = propTypes;
|
||||
|
||||
export default LibSettingsDialog;
|
||||
|
||||
export { TAB };
|
||||
|
@ -48,21 +48,25 @@ class LibHistorySetting extends React.Component {
|
||||
days = this.state.expireDays;
|
||||
}
|
||||
let repoID = this.props.repoID;
|
||||
if (Number(days) > 0) {
|
||||
let message = gettext('Successfully set library history.');
|
||||
seafileAPI.setRepoHistoryLimit(repoID, parseInt(days)).then(res => {
|
||||
toaster.success(message);
|
||||
this.setState({ keepDays: res.data.keep_days });
|
||||
this.props.toggleDialog();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
} else {
|
||||
// If it's allHistory, days is always -1, no validation is needed;
|
||||
// If it's noHistory, days is always 0, no validation is needed;
|
||||
// If it's autoHistory, days needs to be validated to be greater than 0."
|
||||
if (this.state.autoHistory && Number(days) <= 0) {
|
||||
this.setState({
|
||||
errorInfo: gettext('Please enter a non-negative integer'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let message = gettext('Successfully set library history.');
|
||||
seafileAPI.setRepoHistoryLimit(repoID, parseInt(days)).then(res => {
|
||||
toaster.success(message);
|
||||
this.setState({ keepDays: res.data.keep_days });
|
||||
this.props.toggleDialog();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||
import { Button, Input, InputGroup } from 'reactstrap';
|
||||
import { gettext, isPro, siteRoot } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
@ -279,18 +279,21 @@ class LibSubFolderSetGroupPermissionDialog extends React.Component {
|
||||
|
||||
if (this.state.showFileChooser) {
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<div className="d-flex align-items-center justify-content-between pb-2 border-bottom">
|
||||
<h6 className="font-weight-normal m-0">
|
||||
<button className="sf3-font sf3-font-arrow rotate-180 d-inline-block back-icon border-0 bg-transparent text-secondary p-0 mr-2" onClick={this.toggleFileChooser} title={gettext('Back')} aria-label={gettext('Back')}></button>
|
||||
{gettext('Add Folder')}
|
||||
</h6>
|
||||
<Button color="primary" size="sm" outline={true} onClick={this.handleSubmit}>{gettext('Submit')}</Button>
|
||||
</div>
|
||||
<FileChooser
|
||||
repoID={this.props.repoID}
|
||||
mode={'only_current_library'}
|
||||
onDirentItemClick={this.toggleSubFolder}
|
||||
onRepoItemClick={this.onRepoItemClick}
|
||||
/>
|
||||
<div className="modal-footer">
|
||||
<Button color="secondary" onClick={this.toggleFileChooser}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -328,7 +331,7 @@ class LibSubFolderSetGroupPermissionDialog extends React.Component {
|
||||
<td>
|
||||
<InputGroup>
|
||||
<Input value={this.state.folderPath} onChange={this.onSetSubFolder} />
|
||||
<InputGroupAddon addonType="append"><Button className="sf2-icon-plus" onClick={this.toggleFileChooser}></Button></InputGroupAddon>
|
||||
<Button className="sf2-icon-plus" onClick={this.toggleFileChooser}></Button>
|
||||
</InputGroup>
|
||||
</td>
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext, isPro, siteRoot } from '../../utils/constants';
|
||||
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||
import { Button, Input, InputGroup } from 'reactstrap';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import UserSelect from '../user-select';
|
||||
@ -16,6 +16,7 @@ class UserItem extends React.Component {
|
||||
this.state = {
|
||||
isOperationShow: false
|
||||
};
|
||||
this.userSelect = React.createRef();
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
@ -111,6 +112,8 @@ class LibSubFolderSetUserPermissionDialog extends React.Component {
|
||||
} else {
|
||||
this.permissions = ['r', 'rw', 'cloud-edit', 'preview', 'invisible'];
|
||||
}
|
||||
|
||||
this.userSelect = React.createRef();
|
||||
}
|
||||
|
||||
handleUserSelectChange = (option) => {
|
||||
@ -162,7 +165,7 @@ class LibSubFolderSetUserPermissionDialog extends React.Component {
|
||||
permission: 'rw',
|
||||
folderPath: '',
|
||||
});
|
||||
this.refs.userSelect.clearSelect();
|
||||
this.userSelect.current.clearSelect();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
@ -241,18 +244,21 @@ class LibSubFolderSetUserPermissionDialog extends React.Component {
|
||||
|
||||
if (this.state.showFileChooser) {
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<div className="d-flex align-items-center justify-content-between pb-2 border-bottom">
|
||||
<h6 className="font-weight-normal m-0">
|
||||
<button className="sf3-font sf3-font-arrow rotate-180 d-inline-block back-icon border-0 bg-transparent text-secondary p-0 mr-2" onClick={this.toggleFileChooser} title={gettext('Back')} aria-label={gettext('Back')}></button>
|
||||
{gettext('Add Folder')}
|
||||
</h6>
|
||||
<Button color="primary" size="sm" outline={true} onClick={this.handleFileChooserSubmit}>{gettext('Submit')}</Button>
|
||||
</div>
|
||||
<FileChooser
|
||||
repoID={this.props.repoID}
|
||||
mode={'only_current_library'}
|
||||
onDirentItemClick={this.toggleSubFolder}
|
||||
onRepoItemClick={this.onRepoItemClick}
|
||||
/>
|
||||
<div className="modal-footer">
|
||||
<Button color="secondary" onClick={this.toggleFileChooser}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.handleFileChooserSubmit}>{gettext('Submit')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -277,7 +283,7 @@ class LibSubFolderSetUserPermissionDialog extends React.Component {
|
||||
<tr>
|
||||
<td>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
ref={this.userSelect}
|
||||
isMulti={true}
|
||||
placeholder={gettext('Search users')}
|
||||
onSelectChange={this.handleUserSelectChange}
|
||||
@ -288,7 +294,7 @@ class LibSubFolderSetUserPermissionDialog extends React.Component {
|
||||
<td>
|
||||
<InputGroup>
|
||||
<Input value={this.state.folderPath} onChange={this.onSetSubFolder} />
|
||||
<InputGroupAddon addonType="append"><Button className="sf2-icon-plus" onClick={this.toggleFileChooser}></Button></InputGroupAddon>
|
||||
<Button className="sf2-icon-plus" onClick={this.toggleFileChooser}></Button>
|
||||
</InputGroup>
|
||||
</td>
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -22,6 +22,7 @@ class AddOrgAdminDialog extends React.Component {
|
||||
errMessage: '',
|
||||
};
|
||||
this.options = [];
|
||||
this.userSelect = React.createRef();
|
||||
}
|
||||
|
||||
handleSelectChange = (option) => {
|
||||
@ -54,7 +55,7 @@ class AddOrgAdminDialog extends React.Component {
|
||||
<SeahubModalHeader toggle={this.toggle}>{gettext('Add Admins')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
ref={this.userSelect}
|
||||
isMulti={false}
|
||||
placeholder={gettext('Select a user as admin')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, Input, ModalBody, ModalFooter, Label, Form, InputGroup, InputGroupAddon, FormGroup } from 'reactstrap';
|
||||
import { Button, Modal, Input, ModalBody, ModalFooter, Label, Form, InputGroup, FormGroup } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
import { Utils } from '../../utils/utils';
|
||||
|
||||
const propTypes = {
|
||||
toggle: PropTypes.func.isRequired,
|
||||
@ -48,7 +49,7 @@ class AddOrgUserDialog extends React.Component {
|
||||
};
|
||||
|
||||
generatePassword = () => {
|
||||
let val = Math.random().toString(36).substr(5);
|
||||
let val = Utils.generatePassword(8);
|
||||
this.setState({
|
||||
password: val,
|
||||
newPassword: val,
|
||||
@ -146,14 +147,12 @@ class AddOrgUserDialog extends React.Component {
|
||||
<Label for="userPwd">{gettext('Password')}</Label>
|
||||
<InputGroup className="passwd">
|
||||
<Input id="userPwd" innerRef={input => {this.passwdInput = input;}} value={this.state.password || ''} onChange={this.inputPassword} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`link-operation-icon sf3-font sf3-font-eye${this.state.isPasswordVisible ? '-slash' : ''}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="link-operation-icon sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`link-operation-icon sf3-font sf3-font-eye${this.state.isPasswordVisible ? '-slash' : ''}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="link-operation-icon sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalBody, ModalFooter, Input, InputGroupAddon, InputGroup } from 'reactstrap';
|
||||
import { Button, Modal, ModalBody, ModalFooter, Input, InputGroup, InputGroupText } from 'reactstrap';
|
||||
import { gettext, orgID } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
import { orgAdminAPI } from '../../utils/org-admin-api';
|
||||
@ -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}
|
||||
@ -66,7 +71,7 @@ class SetGroupQuotaDialog extends React.Component {
|
||||
onChange={this.handleChange}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">{'MB'}</InputGroupAddon>
|
||||
<InputGroupText addonType="append">{'MB'}</InputGroupText>
|
||||
</InputGroup>
|
||||
<p className="tip">
|
||||
<br/><span>{gettext('An integer that is greater than 0 or equal to -2.')}</span><br/>
|
||||
|
@ -55,7 +55,6 @@ class PublishWikiDialog extends React.Component {
|
||||
});
|
||||
} else {
|
||||
this.props.onPublish(DEFAULT_URL + this.state.url.trim());
|
||||
this.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -66,6 +66,7 @@ class Rename extends React.Component {
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.handleSubmit();
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -5,7 +5,7 @@ import { Modal, ModalBody } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { gettext, siteRoot, enableRepoSnapshotLabel as showLabel } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import Loading from '../../components/loading';
|
||||
import Loading from '../loading';
|
||||
import Paginator from '../../components/paginator';
|
||||
import ModalPortal from '../../components/modal-portal';
|
||||
import CommitDetails from '../../components/dialog/commit-details';
|
||||
|
@ -4,7 +4,7 @@ import { Link } from '@gatsbyjs/reach-router';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, siteRoot, isPro, username } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
import Loading from '../../loading';
|
||||
import toaster from '../../../components/toast';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
import SharePermissionEditor from '../../../components/select-editor/share-permission-editor';
|
||||
|
@ -7,7 +7,7 @@ import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { repoShareAdminAPI } from '../../../utils/repo-share-admin-api';
|
||||
import { gettext, siteRoot } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
import Loading from '../../loading';
|
||||
import toaster from '../../../components/toast';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
||||
|
@ -7,7 +7,7 @@ import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { repoShareAdminAPI } from '../../../utils/repo-share-admin-api';
|
||||
import { gettext, siteRoot } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
import Loading from '../../loading';
|
||||
import toaster from '../../../components/toast';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
||||
|
@ -4,7 +4,7 @@ import { Link } from '@gatsbyjs/reach-router';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, siteRoot, isPro, username } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
import Loading from '../../loading';
|
||||
import toaster from '../../../components/toast';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
import SharePermissionEditor from '../../../components/select-editor/share-permission-editor';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody, ModalFooter, Alert, Button, Input, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Alert, Button, Input, InputGroup } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
@ -74,14 +74,12 @@ class ResetWebdavPassword extends Component {
|
||||
<ModalBody>
|
||||
<InputGroup>
|
||||
<Input type={this.state.isPasswordVisible ? 'text' : 'password'} value={this.state.password} onChange={this.handleInputChange} autoComplete="new-password"/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`sf3-font sf3-font-eye${this.state.isPasswordVisible ? '' : '-slash'}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`sf3-font sf3-font-eye${this.state.isPasswordVisible ? '' : '-slash'}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
<p className="form-text text-muted m-0">{passwordTip}</p>
|
||||
{this.state.errMsg && <Alert color="danger" className="m-0 mt-2">{gettext(this.state.errMsg)}</Alert>}
|
||||
|
@ -6,7 +6,6 @@ import { MODE_TYPE_MAP } from '../../constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { RepoInfo } from '../../models';
|
||||
import { ModalPortal } from '@seafile/sf-metadata-ui-component';
|
||||
import CreateFolder from '../dialog/create-folder-dialog';
|
||||
|
||||
const LibraryOption = ({ mode, label, currentMode, onUpdateMode }) => {
|
||||
@ -123,7 +122,7 @@ class SelectDirentBody extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { mode, repoList, currentRepo, selectedRepo, currentPath, selectedPath, isSupportOtherLibraries, errMessage, searchStatus, searchResults, selectedSearchedRepo, selectedSearchedItem } = this.props;
|
||||
const { mode, repoList, currentRepo, selectedRepo, currentPath, selectedPath, isSupportOtherLibraries = true, errMessage, searchStatus, searchResults, selectedSearchedRepo, selectedSearchedItem } = this.props;
|
||||
let repoListWrapperKey = 'repo-list-wrapper';
|
||||
if (selectedSearchedItem && selectedSearchedItem.repoID) {
|
||||
repoListWrapperKey = `${repoListWrapperKey}-${selectedSearchedItem.repoID}`;
|
||||
@ -131,7 +130,7 @@ class SelectDirentBody extends React.Component {
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col className='repo-list-col border-right'>
|
||||
<Col className='repo-list-col border-end'>
|
||||
<LibraryOption
|
||||
mode={MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY}
|
||||
label={gettext('Current Library')}
|
||||
@ -193,14 +192,12 @@ class SelectDirentBody extends React.Component {
|
||||
</ModalFooter>
|
||||
</Col>
|
||||
{this.state.showCreateFolderDialog && (
|
||||
<ModalPortal>
|
||||
<CreateFolder
|
||||
parentPath={this.props.selectedPath}
|
||||
onAddFolder={this.createFolder}
|
||||
checkDuplicatedName={this.checkDuplicatedName}
|
||||
addFolderCancel={this.onToggleCreateFolder}
|
||||
/>
|
||||
</ModalPortal>
|
||||
<CreateFolder
|
||||
parentPath={this.props.selectedPath}
|
||||
onAddFolder={this.createFolder}
|
||||
checkDuplicatedName={this.checkDuplicatedName}
|
||||
addFolderCancel={this.onToggleCreateFolder}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
@ -233,8 +230,4 @@ SelectDirentBody.propTypes = {
|
||||
selectedSearchedItem: PropTypes.object,
|
||||
};
|
||||
|
||||
SelectDirentBody.defaultProps = {
|
||||
isSupportOtherLibraries: true,
|
||||
};
|
||||
|
||||
export default SelectDirentBody;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody, ModalFooter, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, InputGroup, InputGroupText } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { orgAdminAPI } from '../../utils/org-admin-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
@ -66,9 +66,7 @@ class SetOrgUserDefaultQuota extends React.Component {
|
||||
<React.Fragment>
|
||||
<InputGroup>
|
||||
<input type="text" className="form-control" value={inputValue} onChange={this.handleInputChange} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<InputGroupText>MB</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupText>MB</InputGroupText>
|
||||
</InputGroup>
|
||||
<p className="small text-secondary mt-2 mb-2">{gettext('Tip: 0 means default limit')}</p>
|
||||
{formErrorMsg && <p className="error m-0 mt-2">{formErrorMsg}</p>}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody, ModalFooter, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, InputGroup, InputGroupText } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { orgAdminAPI } from '../../utils/org-admin-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
@ -68,9 +68,7 @@ class SetOrgUserQuota extends React.Component {
|
||||
<React.Fragment>
|
||||
<InputGroup>
|
||||
<input type="text" className="form-control" value={inputValue} onChange={this.handleInputChange} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<InputGroupText>MB</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupText>MB</InputGroupText>
|
||||
</InputGroup>
|
||||
<p className="small text-secondary mt-2 mb-2">{gettext('Tip: 0 means default limit')}</p>
|
||||
{formErrorMsg && <p className="error m-0 mt-2">{formErrorMsg}</p>}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody, ModalFooter, Alert, Button, Input, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Alert, Button, Input, InputGroup } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
@ -74,14 +74,12 @@ class SetWebdavPassword extends Component {
|
||||
<ModalBody>
|
||||
<InputGroup>
|
||||
<Input type={this.state.isPasswordVisible ? 'text' : 'password'} value={this.state.password} onChange={this.handleInputChange} autoComplete="new-password"/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`sf3-font sf3-font-eye${this.state.isPasswordVisible ? '' : 'slash'}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button onClick={this.togglePasswordVisible}>
|
||||
<i className={`sf3-font sf3-font-eye${this.state.isPasswordVisible ? '' : '-slash'}`}></i>
|
||||
</Button>
|
||||
<Button onClick={this.generatePassword}>
|
||||
<i className="sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
<p className="form-text text-muted m-0">{passwordTip}</p>
|
||||
{this.state.errMsg && <Alert color="danger" className="m-0 mt-2">{gettext(this.state.errMsg)}</Alert>}
|
||||
|
@ -326,7 +326,9 @@ class ShareDialog extends React.Component {
|
||||
<div>
|
||||
<Modal isOpen={true} style={{ maxWidth: '760px' }} className="share-dialog" toggle={this.props.toggleDialog}>
|
||||
<SeahubModalHeader toggle={this.props.toggleDialog} tag="div">
|
||||
<h5 className="text-truncate">{gettext('Share')} <span className="op-target" title={itemName}>{itemName}</span></h5>
|
||||
<h5 className="text-truncate m-0">
|
||||
{gettext('Share')} <span className="op-target" title={itemName}>{itemName}</span>
|
||||
</h5>
|
||||
{this.renderExternalShareMessage()}
|
||||
</SeahubModalHeader>
|
||||
<ModalBody className="share-dialog-content" role="tablist">
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Button } from 'reactstrap';
|
||||
import { gettext, isPro, enableShareToDepartment } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { Utils, isMobile } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import SharePermissionEditor from '../select-editor/share-permission-editor';
|
||||
import EventBus from '../common/event-bus';
|
||||
@ -40,6 +40,36 @@ class GroupItem extends React.Component {
|
||||
render() {
|
||||
let item = this.props.item;
|
||||
let currentPermission = Utils.getSharedPermission(item);
|
||||
if (isMobile) {
|
||||
return (
|
||||
<tr>
|
||||
<td className='name'>{item.group_info.name}</td>
|
||||
<td>
|
||||
<SharePermissionEditor
|
||||
repoID={this.props.repoID}
|
||||
isTextMode={true}
|
||||
autoFocus={true}
|
||||
isEditIconShow={this.state.isOperationShow}
|
||||
currentPermission={currentPermission}
|
||||
permissions={this.props.permissions}
|
||||
onPermissionChanged={this.onChangeUserPermission}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className='sf2-icon-x3 action-icon'
|
||||
onClick={this.deleteShareItem}
|
||||
onKeyDown={Utils.onKeyDown}
|
||||
title={gettext('Delete')}
|
||||
aria-label={gettext('Delete')}
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} tabIndex="0" onFocus={this.onMouseEnter}>
|
||||
<td className='name'>{item.group_info.name}</td>
|
||||
@ -352,7 +382,7 @@ class ShareToGroup extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const thead = (
|
||||
let thead = (
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="47%">{gettext('Group')}</th>
|
||||
@ -361,9 +391,20 @@ class ShareToGroup extends React.Component {
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
if (isMobile) {
|
||||
thead = (
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="43%">{gettext('Group')}</th>
|
||||
<th width="35%">{gettext('Permission')}</th>
|
||||
<th width="22%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<table className="w-xs-200">
|
||||
<table>
|
||||
{thead}
|
||||
<tbody>
|
||||
<tr>
|
||||
@ -392,7 +433,7 @@ class ShareToGroup extends React.Component {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Button color="primary" onClick={this.shareToGroup}>{gettext('Submit')}</Button>
|
||||
<Button color="primary" onClick={this.shareToGroup} size={isMobile ? 'sm' : 'md'}>{gettext('Submit')}</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{this.state.errorMsg.length > 0 &&
|
||||
@ -408,7 +449,7 @@ class ShareToGroup extends React.Component {
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="share-list-container">
|
||||
<table className="table-thead-hidden w-xs-200">
|
||||
<table className="table-thead-hidden">
|
||||
{thead}
|
||||
<GroupList
|
||||
repoID={this.props.repoID}
|
||||
|
@ -4,7 +4,7 @@ import classnames from 'classnames';
|
||||
import { gettext, isPro, cloudMode, isOrgContext } from '../../utils/constants';
|
||||
import { Button } from 'reactstrap';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { Utils, isMobile } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import UserSelect from '../user-select';
|
||||
import SharePermissionEditor from '../select-editor/share-permission-editor';
|
||||
@ -21,6 +21,7 @@ class UserItem extends React.Component {
|
||||
isOperationShow: false,
|
||||
isUserDetailsPopoverOpen: false
|
||||
};
|
||||
this.userSelect = React.createRef();
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
@ -51,6 +52,65 @@ class UserItem extends React.Component {
|
||||
let item = this.props.item;
|
||||
let currentPermission = Utils.getSharedPermission(item);
|
||||
const { isUserDetailsPopoverOpen } = this.state;
|
||||
if (isMobile) {
|
||||
return (
|
||||
<tr>
|
||||
<td className="name">
|
||||
<div className="position-relative d-flex align-items-center">
|
||||
<img
|
||||
src={item.user_info.avatar_url}
|
||||
width="24"
|
||||
alt={item.user_info.nickname}
|
||||
className="rounded-circle mr-2 cursor-pointer"
|
||||
onMouseEnter={this.userAvatarOnMouseEnter}
|
||||
onMouseLeave={this.userAvatarOnMouseLeave}
|
||||
/>
|
||||
<span>{item.user_info.nickname}</span>
|
||||
{isUserDetailsPopoverOpen && (
|
||||
<div className="user-details-popover p-4 position-absolute w-100 mt-1">
|
||||
<div className="user-details-main pb-3">
|
||||
<img
|
||||
src={item.user_info.avatar_url}
|
||||
width="40"
|
||||
alt={item.user_info.nickname}
|
||||
className="rounded-circle mr-2"
|
||||
/>
|
||||
<span className="user-details-name">{item.user_info.nickname}</span>
|
||||
</div>
|
||||
<dl className="m-0 mt-3 d-flex">
|
||||
<dt className="m-0 mr-3">{gettext('Email')}</dt>
|
||||
<dd className="m-0">{item.user_info.contact_email}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<SharePermissionEditor
|
||||
repoID={this.props.repoID}
|
||||
isTextMode={true}
|
||||
autoFocus={true}
|
||||
isEditIconShow={true}
|
||||
currentPermission={currentPermission}
|
||||
permissions={this.props.permissions}
|
||||
onPermissionChanged={this.onChangeUserPermission}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className='sf2-icon-x3 action-icon'
|
||||
onClick={this.deleteShareItem}
|
||||
onKeyDown={Utils.onKeyDown}
|
||||
title={gettext('Delete')}
|
||||
aria-label={gettext('Delete')}
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} tabIndex="0" onFocus={this.onMouseEnter}>
|
||||
<td className="name">
|
||||
@ -255,7 +315,7 @@ class ShareToUser extends React.Component {
|
||||
selectedOption: null,
|
||||
permission: 'rw',
|
||||
});
|
||||
this.refs.userSelect.clearSelect();
|
||||
this.userSelect.current.clearSelect();
|
||||
}).catch(error => {
|
||||
if (error.response) {
|
||||
let message = gettext('Library can not be shared to owner.');
|
||||
@ -281,7 +341,7 @@ class ShareToUser extends React.Component {
|
||||
selectedOption: null,
|
||||
permission: 'rw',
|
||||
});
|
||||
this.refs.userSelect.clearSelect();
|
||||
this.userSelect.current.clearSelect();
|
||||
}).catch(error => {
|
||||
if (error.response) {
|
||||
let message = gettext('Library can not be shared to owner.');
|
||||
@ -386,7 +446,7 @@ class ShareToUser extends React.Component {
|
||||
selectedOption: null,
|
||||
permission: 'rw',
|
||||
});
|
||||
this.refs.userSelect.clearSelect();
|
||||
this.userSelect.current.clearSelect();
|
||||
}).catch(error => {
|
||||
if (error.response) {
|
||||
let message = gettext('Library can not be shared to owner.');
|
||||
@ -412,7 +472,7 @@ class ShareToUser extends React.Component {
|
||||
selectedOption: null,
|
||||
permission: 'rw',
|
||||
});
|
||||
this.refs.userSelect.clearSelect();
|
||||
this.userSelect.current.clearSelect();
|
||||
}).catch(error => {
|
||||
if (error.response) {
|
||||
let message = gettext('Library can not be shared to owner.');
|
||||
@ -437,7 +497,7 @@ class ShareToUser extends React.Component {
|
||||
showDeptBtn = false;
|
||||
}
|
||||
let { sharedItems } = this.state;
|
||||
const thead = (
|
||||
let thead = (
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="47%">{gettext('User')}</th>
|
||||
@ -446,16 +506,27 @@ class ShareToUser extends React.Component {
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
if (isMobile) {
|
||||
thead = (
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="43%">{gettext('User')}</th>
|
||||
<th width="35%">{gettext('Permission')}</th>
|
||||
<th width="22%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="share-link-container">
|
||||
<table className="w-xs-200">
|
||||
<table>
|
||||
{thead}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div className='add-members'>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
ref={this.userSelect}
|
||||
isMulti={true}
|
||||
className={classnames('reviewer-select', { 'user-select-right-btn': showDeptBtn })}
|
||||
placeholder={gettext('Search users...')}
|
||||
@ -484,7 +555,7 @@ class ShareToUser extends React.Component {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Button color="primary" onClick={this.shareToUser}>{gettext('Submit')}</Button>
|
||||
<Button color="primary" onClick={this.shareToUser} size={isMobile ? 'sm' : 'md'}>{gettext('Submit')}</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{this.state.errorMsg.length > 0 &&
|
||||
@ -505,7 +576,7 @@ class ShareToUser extends React.Component {
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="share-list-container">
|
||||
<table className="table-thead-hidden w-xs-200">
|
||||
<table className="table-thead-hidden">
|
||||
{thead}
|
||||
<UserList
|
||||
repoID={this.props.repoID}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody, ModalFooter, Button, Form, FormGroup, Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button, Form, FormGroup, Input, InputGroup, InputGroupText } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
@ -59,9 +59,7 @@ class SetQuotaDialog extends React.Component {
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleQuotaChange}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<InputGroupText>MB</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupText>MB</InputGroupText>
|
||||
</InputGroup>
|
||||
<p className="small text-secondary mt-2 mb-2">
|
||||
{gettext('An integer that is greater than or equal to 0.')}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalBody, ModalFooter, Button, Form, FormGroup, Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalFooter, Button, Form, FormGroup, Input, InputGroup, InputGroupText } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
@ -60,9 +60,7 @@ class SysAdminSetUploadDownloadRateLimitDialog extends React.Component {
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleRateLimitChange}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<InputGroupText>kB/s</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupText>kB/s</InputGroupText>
|
||||
</InputGroup>
|
||||
<p className="small text-secondary mt-2 mb-2">
|
||||
{gettext('An integer that is greater than or equal to 0.')}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Modal, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||
import { Alert, Modal, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input, InputGroup } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import SysAdminUserRoleEditor from '../../../components/select-editor/sysadmin-user-role-editor';
|
||||
@ -154,14 +154,12 @@ class SysAdminAddUserDialog extends React.Component {
|
||||
<Label>{gettext('Password')}</Label>
|
||||
<InputGroup>
|
||||
<Input autoComplete="new-password" type={isPasswordVisible ? 'text' : 'password'} value={password || ''} onChange={this.inputPassword} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button className="mt-0" onClick={this.togglePasswordVisible}>
|
||||
<i className={`link-operation-icon sf3-font sf3-font-eye${this.state.isPasswordVisible ? '' : '-slash'}`}></i>
|
||||
</Button>
|
||||
<Button className="mt-0" onClick={this.generatePassword}>
|
||||
<i className="link-operation-icon sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<Button className="mt-0" onClick={this.togglePasswordVisible}>
|
||||
<i className={`link-operation-icon sf3-font sf3-font-eye${this.state.isPasswordVisible ? '' : '-slash'}`}></i>
|
||||
</Button>
|
||||
<Button className="mt-0" onClick={this.generatePassword}>
|
||||
<i className="link-operation-icon sf3-font sf3-font-magic"></i>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
|
@ -18,6 +18,7 @@ class SysAdminGroupAddMemberDialog extends React.Component {
|
||||
selectedOptions: null,
|
||||
isSubmitBtnDisabled: true
|
||||
};
|
||||
this.userSelect = React.createRef();
|
||||
}
|
||||
|
||||
handleSelectChange = (options) => {
|
||||
@ -40,7 +41,7 @@ class SysAdminGroupAddMemberDialog extends React.Component {
|
||||
<SeahubModalHeader toggle={this.props.toggle}>{gettext('Add Member')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
ref={this.userSelect}
|
||||
isMulti={true}
|
||||
placeholder={gettext('Search users')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
|
@ -20,6 +20,7 @@ class SysAdminTransferGroupDialog extends React.Component {
|
||||
selectedOptions: null,
|
||||
submitBtnDisabled: true
|
||||
};
|
||||
this.userSelect = React.createRef();
|
||||
}
|
||||
|
||||
handleSelectChange = (options) => {
|
||||
@ -48,7 +49,7 @@ class SysAdminTransferGroupDialog extends React.Component {
|
||||
</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
ref={this.userSelect}
|
||||
isMulti={false}
|
||||
placeholder={gettext('Select a user')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
|
@ -50,21 +50,25 @@ class SysAdminLibHistorySettingDialog extends React.Component {
|
||||
days = this.state.expireDays;
|
||||
}
|
||||
let repoID = this.props.repoID;
|
||||
if (Number(days) > 0) {
|
||||
let message = gettext('Successfully set library history.');
|
||||
systemAdminAPI.sysAdminUpdateRepoHistorySetting(repoID, parseInt(days)).then(res => {
|
||||
toaster.success(message);
|
||||
this.setState({ keepDays: res.data.keep_days });
|
||||
this.props.toggleDialog();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
} else {
|
||||
// If it's allHistory, days is always -1, no validation is needed;
|
||||
// If it's noHistory, days is always 0, no validation is needed;
|
||||
// If it's autoHistory, days needs to be validated to be greater than 0."
|
||||
if (this.state.autoHistory && Number(days) <= 0) {
|
||||
this.setState({
|
||||
errorInfo: gettext('Please enter a non-negative integer'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let message = gettext('Successfully set library history.');
|
||||
systemAdminAPI.sysAdminUpdateRepoHistorySetting(repoID, parseInt(days)).then(res => {
|
||||
toaster.success(message);
|
||||
this.setState({ keepDays: res.data.keep_days });
|
||||
this.props.toggleDialog();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
|
@ -18,6 +18,7 @@ class SysAdminRepoTransferDialog extends React.Component {
|
||||
selectedOption: null,
|
||||
errorMsg: [],
|
||||
};
|
||||
this.userSelect = React.createRef();
|
||||
}
|
||||
|
||||
handleSelectChange = (option) => {
|
||||
@ -41,7 +42,7 @@ class SysAdminRepoTransferDialog extends React.Component {
|
||||
</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
ref={this.userSelect}
|
||||
isMulti={false}
|
||||
placeholder={gettext('Search users')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalBody, ModalFooter, Input, InputGroupAddon, InputGroup } from 'reactstrap';
|
||||
import { Button, Modal, ModalBody, ModalFooter, Input, InputGroup, InputGroupText } from 'reactstrap';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
@ -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}
|
||||
@ -66,7 +71,7 @@ class SetGroupQuotaDialog extends React.Component {
|
||||
onChange={this.handleChange}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">{'MB'}</InputGroupAddon>
|
||||
<InputGroupText>{'MB'}</InputGroupText>
|
||||
</InputGroup>
|
||||
<p className="tip">
|
||||
<br/><span>{gettext('An integer that is greater than 0 or equal to -2.')}</span><br/>
|
||||
|
@ -15,6 +15,7 @@ class UserItem extends React.Component {
|
||||
this.state = {
|
||||
isOperationShow: false
|
||||
};
|
||||
this.userSelect = React.createRef();
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
@ -169,7 +170,7 @@ class SysAdminShareToUser extends React.Component {
|
||||
selectedOption: null,
|
||||
permission: 'rw',
|
||||
});
|
||||
this.refs.userSelect.clearSelect();
|
||||
this.userSelect.current.clearSelect();
|
||||
}).catch(error => {
|
||||
if (error.response) {
|
||||
let message = gettext('Library can not be shared to owner.');
|
||||
@ -235,7 +236,7 @@ class SysAdminShareToUser extends React.Component {
|
||||
<tr>
|
||||
<td>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
ref={this.userSelect}
|
||||
isMulti={true}
|
||||
placeholder={gettext('Search users')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -5,13 +5,6 @@ import { SimpleEditor } from '@seafile/seafile-editor';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
onCommit: PropTypes.func.isRequired,
|
||||
onCloseEditorDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class TermsEditorDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
@ -22,10 +15,6 @@ class TermsEditorDialog extends React.Component {
|
||||
this.editorRef = React.createRef();
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
title: gettext('Terms'),
|
||||
};
|
||||
|
||||
onKeyDown = (event) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
@ -52,7 +41,7 @@ class TermsEditorDialog extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
let { content, title } = this.props;
|
||||
let { content, title = gettext('Terms') } = this.props;
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
@ -77,6 +66,11 @@ class TermsEditorDialog extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
TermsEditorDialog.propTypes = propTypes;
|
||||
TermsEditorDialog.propTypes = {
|
||||
title: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
onCommit: PropTypes.func.isRequired,
|
||||
onCloseEditorDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default TermsEditorDialog;
|
||||
|
@ -13,17 +13,12 @@ const propTypes = {
|
||||
|
||||
class TermsPreviewDialog extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
title: gettext('Terms'),
|
||||
};
|
||||
|
||||
|
||||
toggle = () => {
|
||||
this.props.onClosePreviewDialog();
|
||||
};
|
||||
|
||||
render() {
|
||||
let { title, content } = this.props;
|
||||
let { title = gettext('Terms'), content } = this.props;
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
|
@ -12,7 +12,7 @@ import { Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import UserSelect from '../user-select';
|
||||
import { SeahubSelect } from '../common/select';
|
||||
import Switch from '../common/switch';
|
||||
import Switch from '../switch';
|
||||
import '../../css/transfer-dialog.css';
|
||||
|
||||
const propTypes = {
|
||||
@ -40,6 +40,7 @@ class TransferDialog extends React.Component {
|
||||
reshare: false,
|
||||
activeTab: !this.props.isDepAdminTransfer ? TRANS_USER : TRANS_DEPART
|
||||
};
|
||||
this.userSelect = React.createRef();
|
||||
}
|
||||
|
||||
handleSelectChange = (option) => {
|
||||
@ -157,7 +158,7 @@ class TransferDialog extends React.Component {
|
||||
<TabPane tabId="transUser" role="tabpanel" id="transfer-user-panel">
|
||||
<Label className='transfer-repo-label'>{gettext('Users')}</Label>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
ref={this.userSelect}
|
||||
isMulti={false}
|
||||
placeholder={gettext('Select a user')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
|
@ -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,18 +21,14 @@ class TransferGroupDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedOption: null,
|
||||
errMessage: '',
|
||||
selectedOption: null
|
||||
};
|
||||
this.options = [];
|
||||
}
|
||||
|
||||
handleSelectChange = (option) => {
|
||||
this.setState({
|
||||
selectedOption: option,
|
||||
errMessage: '',
|
||||
selectedOption: option
|
||||
});
|
||||
this.options = [];
|
||||
};
|
||||
|
||||
transferGroup = () => {
|
||||
@ -41,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() {
|
||||
@ -63,12 +61,11 @@ class TransferGroupDialog extends React.Component {
|
||||
<ModalBody>
|
||||
<p>{gettext('Transfer group to')}</p>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
ref={this.userSelect}
|
||||
isMulti={false}
|
||||
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>
|
||||
|
@ -1,18 +1,38 @@
|
||||
.trash-dialog {
|
||||
max-width: 1100px;
|
||||
height: calc(100% - 56px);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-content {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.trash-dialog {
|
||||
max-width: 1100px;
|
||||
height: calc(100% - 56px);
|
||||
overflow: hidden;
|
||||
margin: 1.75rem auto;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-content {
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.trash-dialog .modal-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trash-dialog .back-icon {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.trash-dialog .modal-header .button-control {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -45,6 +65,19 @@
|
||||
background: #dfdfdf;
|
||||
}
|
||||
|
||||
.trash-dialog .path-container {
|
||||
border-bottom: 1px solid #eee;
|
||||
height: 40px;
|
||||
padding: 0 .5rem 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.trash-dialog .path-container {
|
||||
border-bottom: none;
|
||||
padding: 0 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.trash-dialog .modal-header .trash-dialog-old-page {
|
||||
font-size: 14px;
|
||||
|
@ -188,25 +188,38 @@ class TrashDialog extends React.Component {
|
||||
let title = gettext('{placeholder} Trash');
|
||||
title = title.replace('{placeholder}', '<span class="op-target text-truncate mr-1">' + Utils.HTMLescape(repoFolderName) + '</span>');
|
||||
|
||||
const isDesktop = Utils.isDesktop();
|
||||
return (
|
||||
<>
|
||||
<Modal className="trash-dialog" isOpen={showTrashDialog} toggle={toggleTrashDialog}>
|
||||
<ModalHeader
|
||||
close={
|
||||
<div className="button-control">
|
||||
<a className="trash-dialog-old-page" href={oldTrashUrl}>{gettext('Visit old version page')}</a>
|
||||
{isDesktop && <a className="trash-dialog-old-page" href={oldTrashUrl}>{gettext('Visit old version page')}</a>}
|
||||
{(enableUserCleanTrash && !showFolder && isRepoAdmin) &&
|
||||
<button className="btn btn-secondary clean flex-shrink-0 ml-4" onClick={this.cleanTrash}>{gettext('Clean')}</button>
|
||||
}
|
||||
<button type="button" className="close seahub-modal-btn" aria-label={gettext('Close')} onClick={toggleTrashDialog}>
|
||||
<span className="seahub-modal-btn-inner">
|
||||
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
{isDesktop && (
|
||||
<button type="button" className="close seahub-modal-btn" aria-label={gettext('Close')} onClick={toggleTrashDialog}>
|
||||
<span className="seahub-modal-btn-inner">
|
||||
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{ __html: title }}></div>
|
||||
{!isDesktop &&
|
||||
<span
|
||||
role="button"
|
||||
className="sf3-font sf3-font-arrow rotate-180 d-inline-block back-icon mr-2"
|
||||
title={gettext('Back')}
|
||||
aria-label={gettext('Back')}
|
||||
onClick={toggleTrashDialog}
|
||||
>
|
||||
</span>
|
||||
}
|
||||
<span dangerouslySetInnerHTML={{ __html: title }}></span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{isLoading && <Loading />}
|
||||
@ -214,15 +227,15 @@ class TrashDialog extends React.Component {
|
||||
<EmptyTip text={gettext('No file')} className="m-0" />
|
||||
}
|
||||
{!isLoading && items.length > 0 &&
|
||||
<div>
|
||||
<div className="path-container dir-view-path mb-2">
|
||||
<>
|
||||
<div className="path-container dir-view-path mw-100 pb-2">
|
||||
<span className="path-label mr-1">{gettext('Current path: ')}</span>
|
||||
{showFolder ?
|
||||
this.renderFolderPath() :
|
||||
<span className="last-path-item" title={repoFolderName}>{repoFolderName}</span>
|
||||
}
|
||||
</div>
|
||||
<Table repoID={repoID} data={this.state} renderFolder={this.renderFolder} />
|
||||
<Table repoID={repoID} data={this.state} renderFolder={this.renderFolder} isDesktop={isDesktop} />
|
||||
<Paginator
|
||||
gotoPreviousPage={this.getPreviousPage}
|
||||
gotoNextPage={this.getNextPage}
|
||||
@ -232,7 +245,7 @@ class TrashDialog extends React.Component {
|
||||
resetPerPage={this.resetPerPage}
|
||||
noURLUpdate={true}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import dayjs from 'dayjs';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { Utils, isMobile } from '../../../../../utils/utils';
|
||||
import { gettext, siteRoot } from '../../../../../utils/constants';
|
||||
import { seafileAPI } from '../../../../../utils/seafile-api';
|
||||
import toaster from '../../../../toast';
|
||||
import MobileItemMenu from '../../../../../components/mobile-item-menu';
|
||||
|
||||
class FileRecord extends React.Component {
|
||||
|
||||
@ -24,8 +26,12 @@ class FileRecord extends React.Component {
|
||||
this.setState({ isIconShown: false });
|
||||
};
|
||||
|
||||
restoreItem = (e) => {
|
||||
onRestoreClicked = (e) => {
|
||||
e.preventDefault();
|
||||
this.restoreItem();
|
||||
};
|
||||
|
||||
restoreItem = () => {
|
||||
const { record } = this.props;
|
||||
const { commit_id, parent_dir, obj_name, is_dir } = record;
|
||||
const path = parent_dir + obj_name;
|
||||
@ -55,40 +61,92 @@ class FileRecord extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { record } = this.props;
|
||||
const { record, isDesktop } = this.props;
|
||||
const { restored, isIconShown } = this.state;
|
||||
|
||||
if (restored) return null;
|
||||
|
||||
return record.is_dir ? (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||
<td className="pl-2 pr-2"><img src={Utils.getFolderIconUrl()} alt={gettext('Folder')} width="24" /></td>
|
||||
<td><a href="#" onClick={this.renderFolder}>{record.obj_name}</a></td>
|
||||
<td>{record.parent_dir}</td>
|
||||
<td title={dayjs(record.deleted_time).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(record.deleted_time).format('YYYY-MM-DD')}</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="#" className={(isIconShown || isMobile) ? '' : 'invisible'} onClick={this.restoreItem} role="button">{gettext('Restore')}</a>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||
<td className="pl-2 pr-2"><img src={Utils.getFileIconUrl(record.obj_name)} alt={gettext('File')} width="24" /></td>
|
||||
<td>
|
||||
<a href={`${siteRoot}repo/${this.props.repoID}/trash/files/?obj_id=${record.obj_id}&commit_id=${record.commit_id}&base=${encodeURIComponent(record.parent_dir)}&p=${encodeURIComponent('/' + record.obj_name)}`} target="_blank" rel="noreferrer">
|
||||
{record.obj_name}
|
||||
</a>
|
||||
</td>
|
||||
<td>{record.parent_dir}</td>
|
||||
<td title={dayjs(record.deleted_time).format('dddd, MMMM D, YYYY h:mm:ss A')}>
|
||||
{dayjs(record.deleted_time).format('YYYY-MM-DD')}
|
||||
</td>
|
||||
<td>{Utils.bytesToSize(record.size)}</td>
|
||||
<td>
|
||||
<a href="#" className={(isIconShown || isMobile) ? '' : 'invisible'} onClick={this.restoreItem} role="button">{gettext('Restore')}</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
if (isDesktop) {
|
||||
return record.is_dir ? (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||
<td className="pl-2 pr-2"><img src={Utils.getFolderIconUrl()} alt={gettext('Folder')} width="24" /></td>
|
||||
<td><a href="#" onClick={this.renderFolder}>{record.obj_name}</a></td>
|
||||
<td>{record.parent_dir}</td>
|
||||
<td title={dayjs(record.deleted_time).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(record.deleted_time).format('YYYY-MM-DD')}</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="#" className={(isIconShown || isMobile) ? '' : 'invisible'} onClick={this.onRestoreClicked} role="button">{gettext('Restore')}</a>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||
<td className="pl-2 pr-2"><img src={Utils.getFileIconUrl(record.obj_name)} alt={gettext('File')} width="24" /></td>
|
||||
<td>
|
||||
<a href={`${siteRoot}repo/${this.props.repoID}/trash/files/?obj_id=${record.obj_id}&commit_id=${record.commit_id}&base=${encodeURIComponent(record.parent_dir)}&p=${encodeURIComponent('/' + record.obj_name)}`} target="_blank" rel="noreferrer">
|
||||
{record.obj_name}
|
||||
</a>
|
||||
</td>
|
||||
<td>{record.parent_dir}</td>
|
||||
<td title={dayjs(record.deleted_time).format('dddd, MMMM D, YYYY h:mm:ss A')}>
|
||||
{dayjs(record.deleted_time).format('YYYY-MM-DD')}
|
||||
</td>
|
||||
<td>{Utils.bytesToSize(record.size)}</td>
|
||||
<td>
|
||||
<a href="#" className={(isIconShown || isMobile) ? '' : 'invisible'} onClick={this.onRestoreClicked} role="button">{gettext('Restore')}</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
} else { // for mobile
|
||||
return record.is_dir ? (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||
<td
|
||||
onClick={this.renderFolder}
|
||||
className="text-center"
|
||||
>
|
||||
<img src={Utils.getFolderIconUrl()} alt={gettext('Folder')} width="24" />
|
||||
</td>
|
||||
<td
|
||||
onClick={this.renderFolder}
|
||||
>
|
||||
<a href="#" onClick={this.renderFolder}>{record.obj_name}</a>
|
||||
<br />
|
||||
<span className="item-meta-info">{record.parent_dir}</span>
|
||||
<br />
|
||||
<span className="item-meta-info" title={dayjs(record.deleted_time).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(record.deleted_time).format('YYYY-MM-DD')}</span>
|
||||
</td>
|
||||
<td>
|
||||
<MobileItemMenu>
|
||||
<DropdownItem className="mobile-menu-item" onClick={this.restoreItem}>
|
||||
{gettext('Restore')}
|
||||
</DropdownItem>
|
||||
</MobileItemMenu>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||
<td className="text-center"><img src={Utils.getFileIconUrl(record.obj_name)} alt={gettext('File')} width="24" /></td>
|
||||
<td>
|
||||
<a href={`${siteRoot}repo/${this.props.repoID}/trash/files/?obj_id=${record.obj_id}&commit_id=${record.commit_id}&base=${encodeURIComponent(record.parent_dir)}&p=${encodeURIComponent('/' + record.obj_name)}`} target="_blank" rel="noreferrer">
|
||||
{record.obj_name}
|
||||
</a>
|
||||
<br />
|
||||
<span className="item-meta-info">{record.parent_dir}</span>
|
||||
<br />
|
||||
<span className="item-meta-info mr-2">{Utils.bytesToSize(record.size)}</span>
|
||||
<span className="item-meta-info">
|
||||
{dayjs(record.deleted_time).format('YYYY-MM-DD')}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<MobileItemMenu>
|
||||
<DropdownItem className="mobile-menu-item" onClick={this.restoreItem}>
|
||||
{gettext('Restore')}
|
||||
</DropdownItem>
|
||||
</MobileItemMenu>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user