diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 11b2e698b0..861e62c206 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -179,7 +179,7 @@ "dependencies": { "reactstrap": { "version": "5.0.0", - "resolved": "http://registry.npmjs.org/reactstrap/-/reactstrap-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-5.0.0.tgz", "integrity": "sha512-y0eju/LAK7gbEaTFfq2iW92MF7/5Qh0tc1LgYr2mg92IX8NodGc03a+I+cp7bJ0VXHAiLy0bFL9UP89oSm4cBg==", "requires": { "classnames": "^2.2.3", @@ -3505,7 +3505,7 @@ }, "engine.io-client": { "version": "3.2.1", - "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", "requires": { "component-emitter": "1.2.1", @@ -9916,7 +9916,7 @@ }, "react-popper": { "version": "0.8.3", - "resolved": "http://registry.npmjs.org/react-popper/-/react-popper-0.8.3.tgz", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.8.3.tgz", "integrity": "sha1-D3MzMTfJ+wr27EB00tBYWgoEYeE=", "requires": { "popper.js": "^1.12.9", @@ -10745,9 +10745,9 @@ } }, "seafile-js": { - "version": "0.2.39", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.39.tgz", - "integrity": "sha512-qb6mNzcGCgv+iCuR3eya0aW/JWN17xrTBRc1xyutQwFFptasJzOfbsmVJXtqHfFVe/EbLHSw85nQzmaJqsCVJA==", + "version": "0.2.41", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.41.tgz", + "integrity": "sha512-yDNdzALYn5rMt6TeZwWbbZvmFWyS4xhFoEJQIZImGSHXiNHykcEuLkKA2YbUS1z6AwsDPWGJrU0UvzpmQUpX2Q==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", @@ -11062,7 +11062,7 @@ }, "socket.io-parser": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", "requires": { "component-emitter": "1.2.1", diff --git a/frontend/package.json b/frontend/package.json index 3e851e8ebe..63a5c6bb87 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,7 @@ "react-moment": "^0.7.9", "react-select": "^2.1.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.39", + "seafile-js": "^0.2.41", "seafile-ui": "^0.1.10", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/components/dialog/generate-share-link.js b/frontend/src/components/dialog/generate-share-link.js new file mode 100644 index 0000000000..ced6cebb5b --- /dev/null +++ b/frontend/src/components/dialog/generate-share-link.js @@ -0,0 +1,254 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext, shareLinkExpireDaysMin, shareLinkExpireDaysMax } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon } from 'reactstrap'; + +const propTypes = { + itemPath: PropTypes.string.isRequired, + repoID: PropTypes.string.isRequired +}; + +class GenerateShareLink extends React.Component { + + constructor(props) { + super(props); + this.state = { + passwordVisible: false, + showPasswordInput: false, + isValidate: false, + password: '', + passwdnew: '', + expireDays: '', + token: '', + link: '', + errorInfo: '' + }; + this.permissions = { + 'can_edit': false, + 'can_download': true + }; + } + + componentDidMount() { + this.getShareLink(); + } + + getShareLink = () => { + let path = this.props.itemPath; + let repoID = this.props.repoID; + seafileAPI.getShareLink(repoID, path).then((res) => { + if (res.data.length !== 0) { + this.setState({ + link: res.data[0].link, + token: res.data[0].token, + }); + } + }); + } + + addPassword = () => { + this.setState({ + showPasswordInput: !this.state.showPasswordInput, + password: '', + passwdnew: '', + errorInfo: '' + }); + } + + togglePasswordVisible = () => { + this.setState({ + passwordVisible: !this.state.passwordVisible + }); + } + + generatePassword = () => { + let val = Math.random().toString(36).substr(2); + this.setState({ + password: val, + passwordnew: val + }); + } + + inputPassword = (e) => { + this.setState({ + password: e.target.value + }); + } + + inputPasswordNew = (e) => { + this.setState({ + passwordnew: e.target.value + }); + } + + setPermission = (permission) => { + if (permission == 'previewAndDownload') { + this.permissions = { + 'can_edit': false, + 'can_download': true + }; + } else { + this.permissions = { + 'can_edit': false, + 'can_download': false + }; + } + } + + generateShareLink = () => { + let path = this.props.itemPath; + let repoID = this.props.repoID; + if (this.state.showPasswordInput && (this.state.password == '')) { + this.setState({ + errorInfo: gettext('Please enter password') + }); + } + else if (this.state.showPasswordInput && (this.state.showPasswordInput && this.state.password.length < 8)) { + this.setState({ + errorInfo: gettext('Password is too short') + }); + } + else if (this.state.showPasswordInput && (this.state.password !== this.state.passwordnew)) { + this.setState({ + errorInfo: gettext('Passwords don\'t match') + }); + } + else if (this.state.expireDays === '') { + this.setState({ + errorInfo: gettext('Please enter days') + }); + } else if (!this.state.isValidate) { + // errMessage had been setted + return; + } else { + let { password, expireDays } = this.state; + let permissions = this.permissions; + permissions = JSON.stringify(permissions); + seafileAPI.createShareLink(repoID, path, password, expireDays, permissions).then((res) => { + this.setState({ + link: res.data.link, + token: res.data.token + }); + }); + } + } + + deleteShareLink = () => { + seafileAPI.deleteShareLink(this.state.token).then(() => { + this.setState({ + link: '', + token: '', + showPasswordInput: false, + password: '', + passwordnew: '', + }); + this.permissions = { + 'can_edit': false, + 'can_download': true + }; + }); + } + + onExpireHandler = (e) => { + let day = e.target.value; + let reg = /^\d+$/; + let flag = reg.test(day); + if (!flag) { + this.setState({ + isValidate: false, + errorInfo: gettext('Please enter a non-negative integer'), + expireDays: day, + }); + return; + } + + day = parseInt(day); + + if (day < shareLinkExpireDaysMin || day > shareLinkExpireDaysMax) { + let errorMessage = gettext('Please enter a value between day1 and day2'); + errorMessage = errorMessage.replace('day1', shareLinkExpireDaysMin); + errorMessage = errorMessage.replace('day2', shareLinkExpireDaysMax); + this.setState({ + isValidate: false, + errorInfo: errorMessage, + expireDays: day + }); + return; + } + + this.setState({ + isValidate: true, + errorInfo: '', + expireDays: day + }); + } + + render() { + if (this.state.link) { + return ( +
+

{this.state.link}

+ +
+ ); + } else { + return ( +
+ + + + {this.state.showPasswordInput && + + ({gettext('at least 8 characters')}) + + + + + + + + + + + } + + + + + + + + + + + + +
+ +
+ ); + } + } +} + +GenerateShareLink.propTypes = propTypes; + +export default GenerateShareLink; diff --git a/frontend/src/components/dialog/generate-upload-link.js b/frontend/src/components/dialog/generate-upload-link.js new file mode 100644 index 0000000000..7d2af905b9 --- /dev/null +++ b/frontend/src/components/dialog/generate-upload-link.js @@ -0,0 +1,159 @@ +import React from 'react'; +import { gettext } from '../../utils/constants'; +import PropTypes from 'prop-types'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Button, Form, FormGroup, FormText, Label, Input, InputGroup, InputGroupAddon } from 'reactstrap'; + +const propTypes = { + itemPath: PropTypes.string.isRequired, + repoID: PropTypes.string.isRequired +}; + +class GenerateUploadLink extends React.Component { + constructor(props) { + super(props); + this.state = { + showPasswordInput: false, + passwordVisible: false, + password: '', + passwdnew: '', + link: '', + token:'' + }; + } + + componentDidMount() { + this.getUploadLink(); + } + + getUploadLink = () => { + let path = this.props.itemPath; + let repoID = this.props.repoID; + seafileAPI.getUploadLinks(repoID, path).then((res) => { + if (res.data.length !== 0) { + this.setState({ + link: res.data[0].link, + token: res.data[0].token, + }); + } + }); + } + + addPassword = () => { + this.setState({ + showPasswordInput: !this.state.showPasswordInput, + password: '', + passwdnew: '', + errorInfo: '' + }); + } + + togglePasswordVisible = () => { + this.setState({ + passwordVisible: !this.state.passwordVisible + }); + } + + generatePassword = () => { + let val = Math.random().toString(36).substr(2); + this.setState({ + password: val, + passwordnew: val + }); + } + + inputPassword = (e) => { + this.setState({ + password: e.target.value + }); + } + + inputPasswordNew = (e) => { + this.setState({ + passwordnew: e.target.value + }); + } + + generateUploadLink = () => { + let path = this.props.itemPath; + let repoID = this.props.repoID; + + if (this.state.showPasswordInput && (this.state.password == '')) { + this.setState({ + errorInfo: gettext('Please enter password') + }); + } + else if (this.state.showPasswordInput && (this.state.showPasswordInput && this.state.password.length < 8)) { + this.setState({ + errorInfo: gettext('Password is too short') + }); + } + else if (this.state.showPasswordInput && (this.state.password !== this.state.passwordnew)) { + this.setState({ + errorInfo: gettext('Passwords don\'t match') + }); + } else { + seafileAPI.createUploadLink(repoID, path, this.state.password).then((res) => { + this.setState({ + link: res.data.link, + token: res.data.token + }); + }); + } + } + + deleteUploadLink = () => { + seafileAPI.deleteUploadLink(this.state.token).then(() => { + this.setState({ + link: '', + token: '', + showPasswordInput: false, + password: '', + passwordnew: '', + }); + }); + } + + render() { + if (this.state.link) { + return ( +
+

{this.state.link}

+ +
+ ); + } + return ( +
+ + {gettext('You can share the generated link to others and then they can upload files to this directory via the link.')} + + + + + {this.state.showPasswordInput && + + ({gettext('at least 8 characters')}) + + + + + + + + + + + } +
+ +
+ ); + } +} + +GenerateUploadLink.propTypes = propTypes; + +export default GenerateUploadLink; diff --git a/frontend/src/components/dialog/share-dialog.js b/frontend/src/components/dialog/share-dialog.js new file mode 100644 index 0000000000..cefe1f2d4f --- /dev/null +++ b/frontend/src/components/dialog/share-dialog.js @@ -0,0 +1,125 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import ShareToUser from './share-to-user'; +import ShareToGroup from './share-to-group'; +import GenerateShareLink from './generate-share-link'; +import GenerateUploadLink from './generate-upload-link'; +import '../../css/share-link-dialog.css'; + +const propTypes = { + itemPath: PropTypes.string.isRequired, + itemName: PropTypes.string.isRequired, + toggleDialog: PropTypes.func.isRequired, + isDir: PropTypes.bool.isRequired, + repoID: PropTypes.string.isRequired +}; + +class ShareDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + activeTab: 'shareLink' + }; + } + + toggle = (tab) => { + if (this.state.activeTab !== tab) { + this.setState({activeTab: tab}); + } + } + + renderDirContent = () => { + let activeTab = this.state.activeTab; + return ( + +
+ +
+
+ + + + + + + + + + + + + + +
+
+ ); + } + + renderFileContent = () => { + let activeTab = this.state.activeTab; + return ( + +
+ +
+
+ + + + + +
+
+ ); + } + + render() { + let itemName = this.props.itemName; + + return ( +
+ + Share {itemName} + + {this.props.isDir && this.renderDirContent()} + {!this.props.isDir && this.renderFileContent()} + + +
+ ); + } +} + +ShareDialog.propTypes = propTypes; + +export default ShareDialog; diff --git a/frontend/src/components/dialog/share-to-group.js b/frontend/src/components/dialog/share-to-group.js new file mode 100644 index 0000000000..17efc0a02f --- /dev/null +++ b/frontend/src/components/dialog/share-to-group.js @@ -0,0 +1,185 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Input } from 'reactstrap'; +import Select from 'react-select'; +import makeAnimated from 'react-select/lib/animated'; +import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import { seafileAPI } from '../../utils/seafile-api.js'; + +const propTypes = { + itemPath: PropTypes.string.isRequired, + repoID: PropTypes.string.isRequired +}; + +class ShareToGroup extends React.Component { + + constructor(props) { + super(props); + this.state = { + selectedOption: null, + errorMsg: [], + permission: 'rw', + sharedItems: [] + }; + this.options = []; + } + + handleSelectChange = (option) => { + this.setState({ + selectedOption: option, + }); + } + + componentDidMount() { + this.loadOptions(); + this.listSharedGroups(); + } + + loadOptions = () => { + seafileAPI.shareableGroups().then((res) => { + this.options = []; + for (let i = 0 ; i < res.data.length; i++) { + let obj = {}; + obj.value = res.data[i].name; + obj.id = res.data[i].id; + obj.label = res.data[i].name; + this.options.push(obj); + } + }); + } + + listSharedGroups = () => { + let path = this.props.itemPath; + let repoID = this.props.repoID; + seafileAPI.listSharedItems(repoID, path, 'group').then((res) => { + if(res.data.length !== 0) { + this.setState({ + sharedItems: res.data + }); + } + }); + } + + setPermission = (e) => { + if (e.target.value == 'Read-Write') { + this.setState({ + permission: 'rw', + }); + } else if (e.target.value == 'Read-Only') { + this.setState({ + permission: 'r', + }); + } else if (e.target.value == 'Preview-Edit-on-Cloud') { + this.setState({ + permission: 'clod-edit', + }); + } else if (e.target.value == 'Preview-on-Cloud') { + this.setState({ + permission: 'preview', + }); + } + } + + shareToGroup = () => { + let groups = []; + let path = this.props.itemPath; + let repoID = this.props.repoID; + if (this.state.selectedOption.length > 0 ) { + for (let i = 0; i < this.state.selectedOption.length; i ++) { + groups[i] = this.state.selectedOption[i].id; + } + } + seafileAPI.shareFolder(repoID, path, 'group', this.state.permission, groups).then(res => { + if (res.data.failed.length > 0) { + let errorMsg = []; + for (let i = 0 ; i < res.data.failed.length ; i++) { + errorMsg[i] = res.data.failed[i]; + } + this.setState({ + errorMsg: errorMsg + }); + } + + this.setState({ + sharedItems: this.state.sharedItems.concat(res.data.success) + }); + }); + } + + deleteShareItem = (e, groupID) => { + e.preventDefault(); + let path = this.props.itemPath; + let repoID = this.props.repoID; + seafileAPI.deleteShareToGroupItem(repoID, path, 'group', groupID).then(() => { + this.setState({ + sharedItems: this.state.sharedItems.filter(item => { return item.group_info.id !== groupID; }) + }); + }); + } + + render() { + return ( + + + + + + + + + + + + + + {this.state.errorMsg.length > 0 && + this.state.errorMsg.map((item, index = 0, arr) => { + return ( +

{this.state.errorMsg[index].group_name} + {': '}{this.state.errorMsg[index].error_msg}

+ ); + }) + } + + + +
{gettext('Group')}{gettext('Permission')}
+ + + + + + + + + +
+ ); + } +} + +function GroupList(props) { + return ( + + {props.items.map((item, index) => ( + + {item.group_info.name} + {Utils.sharePerms[item.permission]} + {props.deleteShareItem(e, item.group_info.id);}} className="sf2-icon-delete" title="Delete"> + + ))} + + ); +} + +ShareToGroup.propTypes = propTypes; + +export default ShareToGroup; diff --git a/frontend/src/components/dialog/share-to-user.js b/frontend/src/components/dialog/share-to-user.js new file mode 100644 index 0000000000..0e0667b637 --- /dev/null +++ b/frontend/src/components/dialog/share-to-user.js @@ -0,0 +1,187 @@ +import React, { Fragment } from 'react'; +import AsyncSelect from 'react-select/lib/Async'; +import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import PropTypes from 'prop-types'; +import { Button, Input } from 'reactstrap'; +import { seafileAPI } from '../../utils/seafile-api.js'; + +const propTypes = { + itemPath: PropTypes.string.isRequired, + repoID: PropTypes.string.isRequired +}; + +class ShareToUser extends React.Component { + + constructor(props) { + super(props); + this.state = { + selectedOption: null, + errorMsg: [], + permission: 'rw', + sharedItems: [] + }; + this.options = []; + } + + handleSelectChange = (option) => { + this.setState({selectedOption: option}); + this.options = []; + } + + componentDidMount() { + let path = this.props.itemPath; + let repoID = this.props.repoID; + seafileAPI.listSharedItems(repoID, path, 'user').then((res) => { + if(res.data.length !== 0) { + this.setState({sharedItems: res.data}); + } + }); + } + + setPermission = (e) => { + if (e.target.value == 'Read-Write') { + this.setState({ + permission: 'rw', + }); + } else if (e.target.value == 'Read-Only') { + this.setState({ + permission: 'r', + }); + } else if (e.target.value == 'Admin') { + this.setState({ + permission: 'admin', + }); + } else if (e.target.value == 'Preview-Edit-on-Cloud') { + this.setState({ + permission: 'clod-edit', + }); + } else if (e.target.value == 'Preview-on-Cloud') { + this.setState({ + permission: 'preview', + }); + } + } + + loadOptions = (value, callback) => { + if (value.trim().length > 0) { + seafileAPI.searchUsers(value.trim()).then((res) => { + this.options = []; + for (let i = 0 ; i < res.data.users.length; i++) { + let obj = {}; + obj.value = res.data.users[i].name; + obj.email = res.data.users[i].email; + obj.label = + + + {res.data.users[i].name} + ; + this.options.push(obj); + } + callback(this.options); + }); + } + } + + shareToUser = () => { + let users = []; + let path = this.props.itemPath; + let repoID = this.props.repoID; + if (this.state.selectedOption.length > 0 ) { + for (let i = 0; i < this.state.selectedOption.length; i ++) { + users[i] = this.state.selectedOption[i].email; + } + } + seafileAPI.shareFolder(repoID, path, 'user', this.state.permission, users).then(res => { + if (res.data.failed.length > 0) { + let errorMsg = []; + for (let i = 0 ; i < res.data.failed.length ; i++) { + errorMsg[i] = res.data.failed[i]; + } + this.setState({errorMsg: errorMsg}); + } + this.setState({ + sharedItems: this.state.sharedItems.concat(res.data.success) + }); + }); + } + + deleteShareItem = (e, username) => { + e.preventDefault(); + let path = this.props.itemPath; + let repoID = this.props.repoID; + seafileAPI.deleteShareToUserItem(repoID, path, 'user', username).then(res => { + this.setState({ + sharedItems: this.state.sharedItems.filter( item => { return item.user_info.name !== username; }) + }); + }); + } + + render() { + let { sharedItems } = this.state; + return ( + + + + + + + + + + + + + + {this.state.errorMsg.length > 0 && + this.state.errorMsg.map((item, index = 0, arr) => { + return ( +

{this.state.errorMsg[index].email} + {': '}{this.state.errorMsg[index].error_msg}

+ ); + }) + } + + + +
{gettext('User')}{gettext('Permission')}
+ + + + + + + + + + + +
+ ); + } +} + +function UserList(props) { + return ( + + {props.items.map((item, index) => ( + + {item.user_info.nickname} + {Utils.sharePerms[item.permission]} + {props.deleteShareItem(e, item.user_info.name);}} className="sf2-icon-delete" title="Delete"> + + ))} + + ); +} + +ShareToUser.propTypes = propTypes; + +export default ShareToUser; diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index 1e44f6b9d6..506e2f3420 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -1,8 +1,8 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { gettext, siteRoot } from '../../utils/constants'; -import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; +import { seafileAPI } from '../../utils/seafile-api'; import URLDecorator from '../../utils/url-decorator'; import Toast from '../toast'; import DirentMenu from './dirent-menu'; @@ -11,6 +11,7 @@ import ModalPortal from '../modal-portal'; import ZipDownloadDialog from '../dialog/zip-download-dialog'; import MoveDirentDialog from '../dialog/move-dirent-dialog'; import CopyDirentDialog from '../dialog/copy-dirent-dialog'; +import ShareDialog from '../dialog/share-dialog'; const propTypes = { path: PropTypes.string.isRequired, @@ -45,6 +46,7 @@ class DirentListItem extends React.Component { isProgressDialogShow: false, isMoveDialogShow: false, isCopyDialogShow: false, + isShareDialogShow: false, isMutipleOperation: false, }; this.zipToken = null; @@ -148,6 +150,11 @@ class DirentListItem extends React.Component { this.props.onItemDelete(this.props.dirent); } + onItemShare = (e) => { + e.nativeEvent.stopImmediatePropagation(); //for document event + this.setState({isShareDialogShow: !this.state.isShareDialogShow}); + } + onMenuItemClick = (operation) => { switch(operation) { case 'Rename': @@ -467,6 +474,17 @@ class DirentListItem extends React.Component { /> } + {this.state.isShareDialogShow && + + + + } ); } diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js index be376d7ca6..998b1937df 100644 --- a/frontend/src/components/main-side-nav.js +++ b/frontend/src/components/main-side-nav.js @@ -8,7 +8,7 @@ import { Badge } from 'reactstrap'; import { canViewOrg } from '../utils/constants'; const propTypes = { - currentTab: PropTypes.string.isRequired, + currentTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, tabItemClick: PropTypes.func.isRequired, draftCounts: PropTypes.number, }; @@ -56,23 +56,27 @@ class MainSideNav extends React.Component { this.props.tabItemClick(param); } + getActiveClass = (tab) => { + return this.props.currentTab === tab ? 'active' : ''; + } + renderSharedGroups() { let style = {height: 0}; if (this.state.groupsExtended) { style = {height: this.groupsHeight}; } return ( -