diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 470358db0f..20483b73f0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10793,9 +10793,9 @@ } }, "seafile-js": { - "version": "0.2.41", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.41.tgz", - "integrity": "sha512-yDNdzALYn5rMt6TeZwWbbZvmFWyS4xhFoEJQIZImGSHXiNHykcEuLkKA2YbUS1z6AwsDPWGJrU0UvzpmQUpX2Q==", + "version": "0.2.42", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.42.tgz", + "integrity": "sha512-qbia7tcIRRtI16ks42ocJHd2i1z7QkfsvqGawATKtrnvij2kXNRnTnZqwbHpSjv1H1yctots+NN9F1JRziHDkQ==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index 4f6a4c760a..ddf844003c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "react-moment": "^0.7.9", "react-select": "^2.1.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.41", + "seafile-js": "^0.2.42", "seafile-ui": "^0.1.10", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/app.js b/frontend/src/app.js index 45d03eae4c..e7c2c410de 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -18,8 +18,8 @@ import ShareAdminUploadLinks from './pages/share-admin/upload-links'; import SharedLibraries from './pages/shared-libs/shared-libs'; import MyLibraries from './pages/my-libs/my-libs'; import DirView from './components/dir-view/dir-view'; -import Groups from './pages/group/groups'; -import Group from './pages/group/group'; +import Group from './pages/groups/group-view'; +import Groups from './pages/groups/groups-view'; import MainContentWrapper from './components/main-content-wrapper'; import './assets/css/fa-solid.css'; diff --git a/frontend/src/components/dialog/create-department-repo-dialog.js b/frontend/src/components/dialog/create-department-repo-dialog.js new file mode 100644 index 0000000000..b320cdae46 --- /dev/null +++ b/frontend/src/components/dialog/create-department-repo-dialog.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Col, FormText } from 'reactstrap'; +import { gettext, maxFileName } from '../../utils/constants'; + +const propTypes = { + onCreateRepo: PropTypes.func.isRequired, + onCreateToggle: PropTypes.func.isRequired +}; + +class CreateDepartmentRepoDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + repoName: '', + errMessage: '', + }; + this.newInput = React.createRef(); + } + + handleChange = (e) => { + this.setState({ + repoName: e.target.value, + }); + } + + handleSubmit = () => { + let isValid = this.validateRepoName(); + if (isValid) { + let repo = this.createRepo(this.state.repoName); + this.props.onCreateRepo(repo); + } + + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.handleSubmit(); + } + } + + toggle = () => { + this.props.onCreateToggle(); + } + + componentDidMount = () => { + this.newInput.focus(); + } + + validateRepoName = () => { + let errMessage = ''; + let repoName = this.state.repoName.trim(); + if (!repoName.length) { + errMessage = 'Name is required'; + this.setState({errMessage: errMessage}); + return false; + } + if (repoName.indexOf('/') > -1) { + errMessage = 'Name should not include \'/\'.'; + this.setState({errMessage: errMessage}); + return false; + } + if (repoName.length > maxFileName) { + errMessage = 'RepoName\'s length is must little than ' + maxFileName; + this.setState({errMessage: errMessage}); + return false; + } + return true; + } + + createRepo = (repoName) => { + let repo = { repo_name: repoName }; + return repo; + } + + render() { + return ( + + {gettext('New Department Library')} + +
+ + + {this.newInput = input;}} + value={this.state.repoName} + onChange={this.handleChange} + maxLength={maxFileName} + /> + +
+ +
+ + + +
+ ); + } +} + +CreateDepartmentRepoDialog.propTypes = propTypes; + +export default CreateDepartmentRepoDialog; diff --git a/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js b/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js new file mode 100644 index 0000000000..3f47415558 --- /dev/null +++ b/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js @@ -0,0 +1,309 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap'; +import { Utils } from '../../utils/utils'; +import { gettext, siteRoot, isPro, username, folderPermEnabled } from '../../utils/constants'; + +const propTypes = { + currentGroup: PropTypes.object, + repo: PropTypes.object.isRequired, + isItemFreezed: PropTypes.bool.isRequired, + onFreezedItem: PropTypes.func.isRequired, + onItemUnshare: PropTypes.func.isRequired, +}; + +class SharedRepoListItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + highlight: false, + isOperationShow: false, + isItemMenuShow: false, + }; + this.isDeparementOnwerGroupMember = false; + } + + onMouseEnter = () => { + if (!this.props.isItemFreezed) { + this.setState({ + highlight: true, + isOperationShow: true, + }); + } + } + + onMouseOver = () => { + if (!this.props.isItemFreezed) { + this.setState({ + highlight: true, + isOperationShow: true, + }); + } + } + + onMouseLeave = () => { + if (!this.props.isItemFreezed) { + this.setState({ + highlight: false, + isOperationShow: false, + }); + } + } + + clickOperationMenuToggle = (e) => { + e.preventDefault(); + this.toggleOperationMenu(); + } + + toggleOperationMenu = () => { + if (this.props.isItemFreezed) { + this.setState({ + highlight: false, + isOperationShow: false, + isItemMenuShow: !this.state.isItemMenuShow + }); + } else { + this.setState({ + isItemMenuShow: !this.state.isItemMenuShow + }); + } + this.props.onFreezedItem(); + } + + getRepoComputeParams = () => { + let repo = this.props.repo; + let currentGroup = this.props.currentGroup; //todo--change to libray + let isReadyOnly = false; + if ( repo.permission === 'r' || repo.permission === 'preview') { + isReadyOnly = true; + } + let iconUrl = Utils.getLibIconUrl({ + is_encryted: repo.encrypted, + is_readyonly: isReadyOnly, + size: Utils.isHiDPI() ? 48 : 24 + }); + let iconTitle = Utils.getLibIconTitle({ + 'encrypted': repo.encrypted, + 'is_admin': repo.is_admin, + 'permission': repo.permission + }); + + //todo change to library; div-view is not compatibility + let libPath = `${siteRoot}#group/${currentGroup.id}/lib/${this.props.repo.repo_id}/`; + + return { iconUrl, iconTitle, libPath }; + } + + onMenuItemClick = (e) => { + let operation = e.target.dataset.toggle; + switch(operation) { + case 'Rename': + this.onItemRename(); + break; + case 'Folder Permission': + this.onItemPermisionChanged(); + break; + case 'Details': + this.onItemDetails(); + break; + case 'Share': + this.onItemShared(); + break; + case 'Unshare': + this.onItemUnshare(); + break; + default: + break; + } + } + + onItemRename = () => { + // todo + } + + onItemPermisionChanged = () => { + // todo + } + + onItemDetails = () => { + // todo + } + + onItemShare = () => { + // todo + } + + onItemUnshare = () => { + // todo + this.props.onItemUnshare(this.props.repo); + } + + onItemDelete = () => { + // todo + } + + generatorOperations = () => { + let { repo, currentGroup } = this.props; + //todo this have a bug; use current api is not return admins param; + let isStaff = currentGroup.admins && currentGroup.admins.indexOf(username) > -1; //for group repolist; + let isRepoOwner = repo.owner_email === username; + let isAdmin = repo.is_admin; + let operations = []; + // todo ,shared width me shared width all; + if (isPro) { + if (repo.owner_email.indexOf('@seafile_group') != -1) { //current repo is belong to a group; + if (isStaff && repo.owner_email == currentGroup.id + '@seafile_group') { //is a member of this current group, + this.isDeparementOnwerGroupMember = true; + if (folderPermEnabled) { + operations = ['Rename', 'Folder Permission', 'deatils']; + } else { + operations = ['Rename', 'Details'] + } + } else { + operations.push('Unshare'); + } + } else { + if (isRepoOwner || isAdmin) { + operations.push('Share'); + } + if (isStaff || isRepoOwner || isAdmin) { + operations.push('Unshare'); + } + } + } else { + if (isRepoOwner) { + operations.push('share'); + } + if (isStaff || isRepoOwner) { + operations.push('Unshare'); + } + } + return operations; + } + + generatorMobileMenu = () => { + let operations = this.generatorOperations(); + if (this.isDeparementOnwerGroupMember) { + operations.unshift('unshare'); + operations.unshift('share'); + } + return ( + + +
+
+
+ {operations.map((item, index) => { + return ( + {gettext(item)} + ); + })} +
+
+
+ ); + } + + generatorPCMenu = () => { + // scene one: (share, delete, itemToggle and other operations); + // scene two: (share, unshare), (share), (unshare) + let operations = this.generatorOperations(); + const shareOperation = ; + const unshareOperation = + const deleteOperation = ; + + if (this.isDeparementOnwerGroupMember) { + return ( + + {shareOperation} + {deleteOperation} + + + + {operations.map((item, index) => { + return {gettext(item)} + })} + + + + ); + } else { + if (operations.length == 2) { + return ( + + {shareOperation} + {unshareOperation} + + ); + } + if (operations.length == 1 && operations[0] === 'share') { + return shareOperation; + } + + if (operations.length == 1 && operations[0] === 'unshare') { + return unshareOperation; + } + } + return null; + } + + renderPCUI = () => { + let { iconUrl, iconTitle, libPath } = this.getRepoComputeParams(); + let { repo } = this.props; + return ( + + {iconTitle} + {repo.repo_name} + {this.state.isOperationShow && this.generatorPCMenu()} + {repo.size} + {moment(repo.last_modified).fromNow()} + {repo.owner_name} + + ); + } + + renderMobileUI = () => { + let { iconUrl, iconTitle, libPath } = this.getRepoComputeParams(); + let { repo } = this.props; + return ( + + {iconTitle}/ + + {repo.repo_name}
+ {repo.owner_name} + {repo.size} + {moment(repo.last_modified).fromNow()} + + {this.generatorMobileMenu()} + + ); + } + + render() { + if (window.innerWidth >= 768) { + return this.renderPCUI(); + } else { + return this.renderMobileUI(); + } + } +} + +SharedRepoListItem.propTypes = propTypes; + +export default SharedRepoListItem; diff --git a/frontend/src/components/shared-repo-list-view/shared-repo-list-view.js b/frontend/src/components/shared-repo-list-view/shared-repo-list-view.js new file mode 100644 index 0000000000..70f9faca26 --- /dev/null +++ b/frontend/src/components/shared-repo-list-view/shared-repo-list-view.js @@ -0,0 +1,105 @@ +import React, {Fragment} from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; +import SharedRepoListItem from './shared-repo-list-item'; + +const propTypes = { + currentGroup: PropTypes.object, + repoList: PropTypes.array.isRequired, + isShowTableThread: PropTypes.bool, + onItemUnshare: PropTypes.func.isRequired, +}; + +class SharedRepoListView extends React.Component { + + constructor(props) { + super(props); + this.state = { + isItemFreezed: false, + }; + } + + onFreezedItem = () => { + this.setState({ + isItemFreezed: !this.state.isItemFreezed, + }); + } + + renderRepoListView = () => { + return ( + + {this.props.repoList.map(repo => { + return ( + + ); + })} + + ); + } + + renderPCUI = () => { + let isShowTableThread = this.props.isShowTableThread !== undefined ? this.props.isShowTableThread : true; + return ( + + + + + + + + + + + + + {this.renderRepoListView()} + +
{gettext("Library Type")}{gettext("Name")} + {/*TODO: sort*/} + {gettext("Actions")}{gettext("Size")}{gettext("Last Update")} + {/*TODO: sort*/} + {gettext("Owner")}
+ ); + } + + renderMobileUI = () => { + let isShowTableThread = this.props.isShowTableThread !== undefined ? this.props.isShowTableThread : true; + return ( + + + + + + + + + + {this.renderRepoListView()} + +
{gettext("Library Type")} + {gettext("Sort:")} {/* TODO: sort */} + {gettext("name")} + {gettext("last update")} + {gettext("Actions")}
+ ); + } + + render() { + if (window.innerWidth >= 768) { + return this.renderPCUI(); + } else { + return this.renderMobileUI(); + } + } +} + +SharedRepoListView.propTypes = propTypes; + +export default SharedRepoListView; diff --git a/frontend/src/models/group.js b/frontend/src/models/group.js new file mode 100644 index 0000000000..36960ed65d --- /dev/null +++ b/frontend/src/models/group.js @@ -0,0 +1,17 @@ +class Group { + constructor(object) { + this.id= object.id; + this.name = object.name; + this.owner = object.owner; + this.admins = object.admins || []; + this.avatar_url = object.avatar_url; + this.created_at = object.created_at; + this.parent_group_id = object.parent_group_id; + this.wiki_enabled = object.wiki_enabled; + if (object.repos) { + this.repos = object.repos; + } + } +} + +export default Group; diff --git a/frontend/src/models/repoInfo.js b/frontend/src/models/repoInfo.js new file mode 100644 index 0000000000..ca49afda7e --- /dev/null +++ b/frontend/src/models/repoInfo.js @@ -0,0 +1,24 @@ +import { Utils } from '../utils/utils'; + +class RepoInfo { + constructor(object) { + this.repo_id = object.repo_id; + this.repo_name = object.repo_name; + this.permission = object.permission; + this.size = Utils.bytesToSize(object.size); + this.owner_name = object.owner_name; + this.owner_email = object.owner_email; + this.owner_contact_email = object.owner_contact_name; + this.is_admin = object.is_admin; + this.encrypted = object.encrypted; + this.last_modified = object.last_modified; + this.modifier_contact_email = object.modifier_contact_email; + this.modifier_email = object.modifier_email; + this.modifier_name = object.modifier_name; + this.mtime = object.mtime; + } +} + +export default RepoInfo; + +//todo rename to repo, and rename repo to repoInfo diff --git a/frontend/src/pages/group/group-repo-item.js b/frontend/src/pages/group/group-repo-item.js deleted file mode 100644 index 8f81b4288f..0000000000 --- a/frontend/src/pages/group/group-repo-item.js +++ /dev/null @@ -1,270 +0,0 @@ -import React, { Component } from 'react'; -import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap'; -import moment from 'moment'; -import { seafileAPI } from '../../utils/seafile-api'; -import { Utils } from '../../utils/utils'; -import { gettext, siteRoot, loginUrl, isPro, folderPermEnabled, username } from '../../utils/constants'; - -class GroupRepoItem extends Component { - - constructor(props) { - super(props); - this.state = { - showOpIcon: false, - operationMenuOpen: false, - unshared: false - }; - - this.handleMouseOver = this.handleMouseOver.bind(this); - this.handleMouseOut = this.handleMouseOut.bind(this); - this.toggleOperationMenu = this.toggleOperationMenu.bind(this); - this.clickOperationMenuToggle = this.clickOperationMenuToggle.bind(this); - - this.share = this.share.bind(this); - this.unshare = this.unshare.bind(this); - - this.deleteItem = this.deleteItem.bind(this); - - this.rename = this.rename.bind(this); - this.folderPerm = this.folderPerm.bind(this); - this.showDetails = this.showDetails.bind(this); - } - - handleMouseOver() { - this.setState({ - showOpIcon: true - }); - } - - handleMouseOut() { - this.setState({ - showOpIcon: false - }); - } - - toggleOperationMenu() { - this.setState({ - operationMenuOpen: !this.state.operationMenuOpen - }); - } - - clickOperationMenuToggle(e) { - e.preventDefault(); - this.toggleOperationMenu(); - } - - share(e) { - e.preventDefault(); - // TODO - } - - unshare(e) { - e.preventDefault(); - // TODO - - const data = this.props.data; - - let request; - if (data.owner_email.indexOf('@seafile_group') == -1) { - let options = { - 'share_type': 'personal', - 'from': data.owner_email - }; - request = seafileAPI.leaveShareRepo(data.repo_id, options); - } else { - request = seafileAPI.leaveShareGroupOwnedRepo(data.repo_id); - } - - request.then((res) => { - this.setState({ - unshared: true - }); - // TODO: show feedback msg - }).catch((error) => { - // TODO: show feedback msg - }); - } - - deleteItem() { - // TODO - const data = this.props.data; - seafileAPI.deleteRepo(data.repo_id).then((res) => { - this.setState({ - deleted: true - }); - // TODO: show feedback msg - }).catch((error) => { - // TODO: show feedback msg - }); - } - - rename() { - } - - folderPerm() { - } - - showDetails() { - } - - render() { - - if (this.state.unshared) { - return null; - } - - const data = this.props.data; - const permission = data.permission; - - const {groupId, isStaff, showRepoOwner} = this.props.extra; - const isRepoOwner = username == data.owner_email; - const isAdmin = data.is_admin; - - let is_readonly = false; - if (permission == 'r' || permission == 'preview') { - is_readonly = true; - } - data.icon_url = Utils.getLibIconUrl({ - is_encrypted: data.encrypted, - is_readonly: is_readonly, - size: Utils.isHiDPI() ? 48 : 24 - }); - data.icon_title = Utils.getLibIconTitle({ - 'encrypted': data.encrypted, - 'is_admin': data.is_admin, - 'permission': permission - }); - data.url = `${siteRoot}#group/${groupId}/lib/${data.repo_id}/`; - - let iconVisibility = this.state.showOpIcon ? '' : ' invisible'; - let shareIconClassName = 'sf2-icon-share sf2-x repo-share-btn op-icon' + iconVisibility; - let unshareIconClassName = 'sf2-icon-x3 sf2-x op-icon' + iconVisibility; - let deleteIconClassName = 'sf2-icon-delete sf2-x op-icon' + iconVisibility; - let operationMenuToggleIconClassName = 'sf2-icon-caret-down item-operation-menu-toggle-icon op-icon'; - if (window.innerWidth >= 768) { - operationMenuToggleIconClassName += iconVisibility; - } - - const commonToggle = ( - - - ); - - const commonOperationsInMenu = ( - - {gettext('Rename')} - {folderPermEnabled ? {gettext('Folder Permission')} : null} - {gettext('Details')} - - ); - - let desktopOperations; - let mobileOperationMenu; - - const share = ; - const unshare = - - const shareDropdownItem = {gettext('Share')}; - const unshareDropdownItem = {gettext('Unshare')}; - - if (isPro) { - if (data.owner_email.indexOf('@seafile_group') != -1) { // group owned repo - if (isStaff) { - if (data.owner_email == groupId + '@seafile_group') { // this repo belongs to the current group - desktopOperations = ( - - {share} - - - {commonToggle} - - {commonOperationsInMenu} - - - - ); - mobileOperationMenu = ( - - {shareDropdownItem} - {gettext('Delete')} - {commonOperationsInMenu} - - ); - } else { - desktopOperations = unshare; - mobileOperationMenu = unshareDropdownItem; - } - } - } else { - desktopOperations = ( - - {isRepoOwner || isAdmin ? share : null} - {isStaff || isRepoOwner || isAdmin ? unshare : null} - - ); - mobileOperationMenu = ( - - {isRepoOwner || isAdmin ? shareDropdownItem : null} - {isStaff || isRepoOwner || isAdmin ? unshareDropdownItem : null} - - ); - } - } else { - desktopOperations = ( - - {isRepoOwner ? share : null} - {isStaff || isRepoOwner ? unshare : null} - - ); - mobileOperationMenu = ( - - {isRepoOwner ? shareDropdownItem : null} - {isStaff || isRepoOwner ? unshareDropdownItem : null} - - ); - } - - const mobileOperations = ( - - {commonToggle} -
-
-
- {mobileOperationMenu} -
-
-
- ); - - const desktopItem = ( - - {data.icon_title} - {data.repo_name} - {desktopOperations} - {Utils.formatSize({bytes: data.size})} - {moment(data.last_modified).fromNow()} - {showRepoOwner ? {data.owner_name} : null} - - ); - - const mobileItem = ( - - {data.icon_title} - - {data.repo_name}
- {showRepoOwner ? {data.owner_name} : null} - {Utils.formatSize({bytes: data.size})} - {moment(data.last_modified).fromNow()} - - {mobileOperations} - - ); - - return window.innerWidth >= 768 ? desktopItem : mobileItem; - } -} - -export default GroupRepoItem; diff --git a/frontend/src/pages/group/group.js b/frontend/src/pages/group/group.js deleted file mode 100644 index 74a78e15a1..0000000000 --- a/frontend/src/pages/group/group.js +++ /dev/null @@ -1,305 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import { seafileAPI } from '../../utils/seafile-api'; -import { Utils } from '../../utils/utils'; -import { gettext, siteRoot, loginUrl, username } from '../../utils/constants'; -import Loading from '../../components/loading'; -import GroupRepoItem from './group-repo-item'; -import CommonToolbar from '../../components/toolbar/common-toolbar'; -import RepoViewToolbar from '../../components/toolbar/repo-view-toobar'; - -import '../../css/groups.css'; - -class Header extends Component { - - render() { - const {loading, errorMsg, data} = this.props.data; - - if (loading) { - return ; - } else if (errorMsg) { - return

{errorMsg}

; - } else { - /* - admins: ["lj@1.com"] - avatar_url: "http://127.0.0.1:8000/media/avatars/groups/default.png" - created_at: "2018-10-25T08:18:11+00:00" - id: 2 - name: "g1" - owner: "lj@1.com" - parent_group_id: 0 - wiki_enabled: false - */ - const path = ( -
- {gettext("Groups")} - / - {data.name} - {data.parent_group_id == 0 ? null : - - } -
- ); - - let showSettingsIcon = true; - if (data.parent_group_id != 0 && data.admins.indexOf(username) == -1) { - showSettingsIcon = false; - } - - // TODO: click icon - const toolbar = ( -
- {showSettingsIcon ? : null} - -
- ); - - return ( - - {path} - {toolbar} - - ); - } - } -} - -class Content extends Component { - - render() { - const {loading, errorMsg, items} = this.props.data.repos; - - if (loading) { - return ; - } else if (errorMsg) { - return

{errorMsg}

; - } else { - if (!items) { - return null; - } - - const groupInfo = this.props.data.groupMetaInfo.data; - - let emptyTip; - if (groupInfo.parent_group_id == 0) { - emptyTip = ( -
-

{gettext('No library is shared to this group')}

-

{gettext('You can share libraries by clicking the "New Library" button above or the "Share" icon on your libraries list.')}

-

{gettext('Libraries shared as writable can be downloaded and synced by other group members. Read only libraries can only be downloaded, updates by others will not be uploaded.')}

-
- ); - } else { - if (groupInfo.admins.indexOf(username) == -1) { - emptyTip = ( -
-

{gettext('No libraries')}

-
- ); - } else { - emptyTip = ( -
-

{gettext('No libraries')}

-

{gettext('You can create libraries by clicking the "New Library" button above.')}

-
- ); - } - } - - const desktopThead = ( - - - {gettext("Library Type")} - {gettext("Name")}{/*TODO: sort*/} - {gettext("Actions")} - {gettext("Size")} - {gettext("Last Update")}{/*TODO: sort*/} - {gettext("Owner")} - - - ); - - const mobileThead = ( - - - {gettext("Library Type")} - - {gettext("Sort:")} {/* TODO: sort */} - {gettext("name")} - {gettext("last update")} - - {gettext("Actions")} - - - ); - - const extraData = { - groupId: groupInfo.id, - isStaff: groupInfo.admins.indexOf(username) != -1, - showRepoOwner: true - }; - - const table = ( - - {window.innerWidth >= 768 ? desktopThead : mobileThead} - -
- ); - - return items.length ? table : emptyTip; - } - } -} - -class TableBody extends Component { - - constructor(props) { - super(props); - this.state = { - items: this.props.items - }; - } - - render() { - - let listItems = this.state.items.map(function(item, index) { - return ; - }, this); - - return ( - {listItems} - ); - } -} - -class Group extends Component { - constructor(props) { - super(props); - this.state = { - groupMetaInfo: { - loading: true, - errorMsg: '' - }, - repos: { - loading: false, - errorMsg: '' - } - }; - } - - componentDidMount() { - this.updateGroupRepoList(this.props.groupID); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.groupID !== this.props.groupID) { - this.updateGroupRepoList(nextProps.groupID); - } - } - - updateGroupRepoList = (groupID) => { - seafileAPI.getGroup(groupID).then((res) => { - // res: {data: {...}, status: 200, statusText: "OK", headers: {…}, config: {…}, …} - this.setState({ - groupMetaInfo: { - loading: false, - data: res.data - } - }); - this.listGroupRepos(); - }).catch((error) => { - if (error.response) { - if (error.response.status == 403) { - this.setState({ - groupMetaInfo: { - loading: false, - errorMsg: gettext("Permission denied") - } - }); - location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; - } else { - this.setState({ - groupMetaInfo: { - loading: false, - errorMsg: gettext("Error") - } - }); - } - } else { - this.setState({ - groupMetaInfo: { - loading: false, - errorMsg: gettext("Please check the network.") - } - }); - } - }); - } - - listGroupRepos() { - seafileAPI.listGroupRepos(this.props.groupID).then((res) => { - // res: {data: [...], status: 200, statusText: "OK", headers: {…}, config: {…}, …} - this.setState({ - repos: { - loading: false, - items: res.data - } - }); - }).catch((error) => { - if (error.response) { - if (error.response.status == 403) { - this.setState({ - repos: { - loading: false, - errorMsg: gettext("Permission denied") - } - }); - location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; - } else { - this.setState({ - repos: { - loading: false, - errorMsg: gettext("Error") - } - }); - } - } else { - this.setState({ - repos: { - loading: false, - errorMsg: gettext("Please check the network.") - } - }); - } - }); - } - - onCreateRepo = (repo) => { - let groupId = this.props.groupID; - seafileAPI.createGroupRepo(groupId, repo).then(() => { - //todo update group list - }); - } - - render() { - return ( - -
- - -
-
-
-
-
-
-
- -
-
-
-
- ); - } -} - -export default Group; diff --git a/frontend/src/pages/group/groups.js b/frontend/src/pages/group/groups.js deleted file mode 100644 index e12873c1b2..0000000000 --- a/frontend/src/pages/group/groups.js +++ /dev/null @@ -1,178 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import { seafileAPI } from '../../utils/seafile-api'; -import { gettext, siteRoot, loginUrl, username } from '../../utils/constants'; -import Loading from '../../components/loading'; -import GroupRepoItem from './group-repo-item'; -import GeneralToolbar from '../../components/toolbar/general-toolbar'; - -import '../../css/groups.css'; - -class Content extends Component { - - render() { - const {loading, errorMsg, items} = this.props.data; - - if (loading) { - return ; - } else if (errorMsg) { - return

{errorMsg}

; - } else { - /* TODO: - {% if user.permissions.can_add_group %} -

{% blocktrans %}Groups allow multiple people to collaborate on libraries. You can create a group by clicking the "New Group" button.{% endblocktrans %}

- {% else %} -

{% trans "Groups allow multiple people to collaborate on libraries. Groups you join will be listed here." %}

- {% endif %} - */ - const emptyTip = ( -
-

{gettext('You are not in any groups')}

-

{gettext('Groups allow multiple people to collaborate on libraries.')}

-
- ); - - let listItems = items.map(function(item, index) { - return ; - }, this); - - const groupItems = ( - - {listItems} - - ); - - return items.length ? groupItems : emptyTip; - } - } -} - -class GroupItem extends Component { - - render() { - - const data = this.props.data; - - const desktopThead = ( - - - {gettext("Library Type")} - {gettext("Name")} - {gettext("Actions")} - {gettext("Size")} - {gettext("Last Update")} - - - ); - - const mobileThead = ( - - - {gettext("Library Type")} - {gettext("name")} - {gettext("Actions")} - - - ); - - const extraData = { - groupId: data.id, - isStaff: data.admins.indexOf(username) != -1, - showRepoOwner: false - }; - - const table = ( - - {window.innerWidth >= 768 ? desktopThead : mobileThead} - -
- ); - - const emptyTip =

{gettext('No libraries')}

; - - const item = ( - -

{data.name}

- {data.repos.length ? table : emptyTip} -
- ); - - return item; - } -} - -class TableBody extends Component { - - render() { - - let listItems = this.props.items.map(function(item, index) { - return ; - }, this); - - return ( - {listItems} - ); - } -} - -class Groups extends Component { - constructor(props) { - super(props); - this.state = { - loading: true, - errorMsg: '', - items: [] - }; - } - - componentDidMount() { - seafileAPI.listGroupsV2({'with_repos': 1}).then((res) => { // TODO: api name - // `{'with_repos': 1}`: list repos of every group - // res: {data: [...], status: 200, statusText: "OK", headers: {…}, config: {…}, …} - this.setState({ - loading: false, - items: res.data - }); - }).catch((error) => { - if (error.response) { - if (error.response.status == 403) { - this.setState({ - loading: false, - errorMsg: gettext("Permission denied") - }); - location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; - } else { - this.setState({ - loading: false, - errorMsg: gettext("Error") - }); - } - - } else { - this.setState({ - loading: false, - errorMsg: gettext("Please check the network.") - }); - } - }); - } - - render() { - return ( - - -
-
-
-

{gettext("My Groups")}

-
-
- -
-
-
-
- ); - } -} - -export default Groups; diff --git a/frontend/src/pages/groups/group-view.js b/frontend/src/pages/groups/group-view.js new file mode 100644 index 0000000000..c35f78b818 --- /dev/null +++ b/frontend/src/pages/groups/group-view.js @@ -0,0 +1,259 @@ +import React,{ Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { gettext, siteRoot, username, loginUrl } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import Loading from '../../components/loading'; +import ModalPortal from '../../components/modal-portal'; +import Group from '../../models/group'; +import RepoInfo from '../../models/repoInfo'; +import CommonToolbar from '../../components/toolbar/common-toolbar'; +import CreateRepoDialog from '../../components/dialog/create-repo-dialog'; +import CreateDepartmentRepoDialog from '../../components/dialog/create-department-repo-dialog'; +import SharedRepoListView from '../../components/shared-repo-list-view/shared-repo-list-view'; + +const propTypes = { + onShowSidePanel: PropTypes.func.isRequired, + onSearchedClick: PropTypes.func.isRequired, +}; + +const DEPARETMENT_GROUP_ONWER_NAME = 'system admin'; + +class GroupView extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading: true, + errMessage: '', + emptyTip: null, + currentGroup: null, + isStaff: false, + repoList: [], + libraryType: 'group', + isCreateRepoDialogShow: false, + isDepartmentGroup: false, + } + } + + componentDidMount() { + let groupID = this.props.groupID; + this.loadGroup(groupID); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.groupID !== this.props.groupID) { + this.loadGroup(nextProps.groupID); + } + } + + loadGroup = (groupID) => { + seafileAPI.getGroup(groupID).then((res) => { + let currentGroup = new Group(res.data); + let emptyTip = this.getEmptyTip(currentGroup); + let isStaff = currentGroup.admins.indexOf(username) > -1; //for item operations + let isDepartmentGroup = currentGroup.owner === DEPARETMENT_GROUP_ONWER_NAME; + this.setState({ + emptyTip: emptyTip, + currentGroup: currentGroup, + isStaff: isStaff, + isDepartmentGroup: isDepartmentGroup, + }); + this.loadRepos(groupID); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + isLoading: false, + errMessage: gettext("Permission denied") + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + isLoading: false, + errMessage: gettext("Error") + }); + } + } else { + this.setState({ + isLoading: false, + errMessage: gettext("Please check the network.") + }); + } + }); + } + + loadRepos = (groupID) => { + this.setState({isLoading: true}); + seafileAPI.listGroupRepos(groupID).then((res) => { + let repoList = res.data.map(item => { + let repoInfo = new RepoInfo(item); + return repoInfo; + }); + this.setState({ + isLoading: false, + repoList: repoList + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + isLoading: false, + errMessage: gettext("Permission denied") + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + isLoading: false, + errMessage: gettext("Error") + }); + } + } else { + this.setState({ + isLoading: false, + errMessage: gettext("Please check the network.") + }); + } + }); + } + + getEmptyTip = (currentGroup) => { + let emptyTip = null; + if (currentGroup) { + if (currentGroup.parent_group_id === 0) { + emptyTip = ( +
+

{gettext('No library is shared to this group')}

+

{gettext('You can share libraries by clicking the "New Library" button above or the "Share" icon on your libraries list.')}

+

{gettext('Libraries shared as writable can be downloaded and synced by other group members. Read only libraries can only be downloaded, updates by others will not be uploaded.')}

+
+ ); + } else { + if (currentGroup.admins.indexOf(username) == -1) { // is a member of this group + emptyTip = ( +
+

{gettext('No libraries')}

+
+ ); + } else { + emptyTip = ( +
+

{gettext('No libraries')}

+

{gettext('You can create libraries by clicking the "New Library" button above.')}

+
+ ); + } + } + } + return emptyTip; + } + + onCreateRepoToggle = () => { + this.setState({isCreateRepoDialogShow: !this.state.isCreateRepoDialogShow}); + } + + onCreateRepo = (repo) => { + let groupId = this.props.groupID; + seafileAPI.createGroupRepo(groupId, repo).then(res => { + let repo = new RepoInfo(res.data); + let repoList = this.addRepoItem(repo); + this.setState({repoList: repoList}); + }).catch(() => { + //todo + }); + this.onCreateRepoToggle(); + } + + addRepoItem = (repo) => { + let newRepoList = this.state.repoList.map(item => {return item;}); + newRepoList.push(repo); + return newRepoList; + } + + onItemUnshare = (repo) => { + let group = this.state.currentGroup; + seafileAPI.unshareRepo(repo.repo_id, {share_type: 'group', group_id: group.id}).then(() => { + let repoList = this.state.repoList.filter(item => { + return item.repo_id !== repo.repo_id; + }); + this.setState({repoList: repoList}); + }); + } + + render() { + let { errMessage, emptyTip, currentGroup } = this.state; + let isShowSettingIcon = !(currentGroup && currentGroup.parent_group_id !== 0 && currentGroup.admins.indexOf(username) === -1); + return ( + +
+
+ +
+ +
+
+ +
+
+
+
+ {currentGroup && ( + +
+ {gettext("Groups")} + / + {currentGroup.name} + {currentGroup.parent_group_id !== 0 && ( + + )} +
+
+ {isShowSettingIcon && ( + + )} + +
+
+ )} +
+
+ {this.state.isLoading && } + {(!this.state.isLoading && errMessage) && errMessage} + {(!this.state.isLoading && this.state.repoList.length === 0) && emptyTip} + {(!this.state.isLoading && this.state.repoList.length > 0) && + + } +
+
+
+ {this.state.isCreateRepoDialogShow && !this.state.isDepartmentGroup && ( + + + + )} + {this.state.isCreateRepoDialogShow && this.state.isDepartmentGroup && + + } +
+ ); + } +} + +GroupView.propTypes = propTypes; + +export default GroupView; diff --git a/frontend/src/pages/groups/groups-view.js b/frontend/src/pages/groups/groups-view.js new file mode 100644 index 0000000000..cd15a23cf4 --- /dev/null +++ b/frontend/src/pages/groups/groups-view.js @@ -0,0 +1,145 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { gettext, siteRoot, loginUrl } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import Loading from '../../components/loading'; +import Group from '../../models/group'; +import RepoInfo from '../../models/repoInfo'; +import GeneralToolbar from '../../components/toolbar/general-toolbar'; +import SharedRepoListView from '../../components/shared-repo-list-view/shared-repo-list-view'; + +import '../../css/groups.css'; + +const propTypes = { + group: PropTypes.object.isRequired, +}; + + +class RepoListViewPanel extends React.Component { + + constructor(props) { + super(props); + this.state = { + repoList: [], + }; + } + + componentDidMount() { + let group = this.props.group; + let repoList = group.repos.map(item => { + let repo = new RepoInfo(item); + return repo; + }); + this.setState({repoList: repoList}); + } + + onItemUnshare = (repo) => { + let group = this.props.group; + seafileAPI.unshareRepo(repo.repo_id, {share_type: 'group', group_id: group.id}).then(() => { + let repoList = this.state.repoList.filter(item => { + return item.repo_id !== repo.repo_id; + }); + this.setState({repoList: repoList}); + }); + } + + render() { + let group = this.props.group; + const emptyTip =

{gettext('No libraries')}

; + return ( + +

+ {group.name} +

+ {this.state.repoList.length === 0 ? + emptyTip : + + } +
+ ); + } + } + +RepoListViewPanel.propTypes = propTypes; + +class GroupsView extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading: true, + errorMsg: '', + groupList: [], + } + } + + componentDidMount() { + seafileAPI.listGroupsV2({'with_repos': 1}).then((res) => { // TODO: api name + // `{'with_repos': 1}`: list repos of every group + // res: {data: [...], status: 200, statusText: "OK", headers: {…}, config: {…}, …} + let groupList = res.data.map(item => { + let group = new Group(item); + return group; + }) + this.setState({ + isLoading: false, + groupList: groupList, + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + isLoading: false, + errorMsg: gettext("Permission denied") + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + isLoading: false, + errorMsg: gettext("Error") + }); + } + } else { + this.setState({ + isLoading: false, + errorMsg: gettext("Please check the network.") + }); + } + }); + } + + render() { + return ( + + +
+
+
+

{gettext("My Groups")}

+
+
+ {this.state.isLoading && } + {(!this.state.isLoading && this.state.errorMsg !== '') && this.state.errorMsg} + {/* {(!this.state.isLoading && this.state.groupList.length === 0 ) && emptyTip} //todo */} + {!this.state.isLoading && this.state.groupList.map((group, index) => { + return ( + + ); + })} +
+
+
+
+ ); + } +} + +// Groups.propTypes = propTypes; + +export default GroupsView; diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index e3c808ed31..e7acdb2347 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -27,6 +27,7 @@ export const storages = window.app.pageOptions.storages; // storage backends export const enableRepoSnapshotLabel = window.app.pageOptions.enableRepoSnapshotLabel; export const shareLinkExpireDaysMin = window.app.pageOptions.shareLinkExpireDaysMin; export const shareLinkExpireDaysMax = window.app.pageOptions.shareLinkExpireDaysMax; +export const maxFileName = window.app.pageOptions.maxFileName; // wiki export const slug = window.wiki ? window.wiki.config.slug : ''; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index 576d39cd51..90029ba071 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -80,6 +80,7 @@ .sf2-icon-move:before {content:"\e029"} .sf2-icon-menu:before { content: "\e031"; } .sf2-icon-more:before { content: "\e032"; } +.sf2-icon-x3:before {content:"\e035";} .sf2-icon-close:before { content:"\e035"; } .sf2-icon-two-columns:before { content:"\e036"; } .sf2-icon-tag:before {content:"\e037"} diff --git a/seahub/api2/endpoints/groups.py b/seahub/api2/endpoints/groups.py index 950031b357..9588835982 100644 --- a/seahub/api2/endpoints/groups.py +++ b/seahub/api2/endpoints/groups.py @@ -153,7 +153,7 @@ class Groups(APIView): contact_email_dict[email] = email2contact_email(email) for r in group_repos: - repo_owner = repo_id_owner_dict.get(r.id, r.user), + repo_owner = repo_id_owner_dict.get(r.id, r.user) repo = { "id": r.id, "repo_id": r.id, diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 7c52d81dd6..3aeb0fb049 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -61,6 +61,7 @@ enableRepoSnapshotLabel: {% if enable_repo_snapshot_label %} true {% else %} false {% endif %}, shareLinkExpireDaysMin: "{{ share_link_expire_days_min }}", shareLinkExpireDaysMax: "{{ share_link_expire_days_max }}", + maxFileName: "{{ max_file_name }}" } };