diff --git a/frontend/src/components/dialog/department-detail-dialog.js b/frontend/src/components/dialog/department-detail-dialog.js new file mode 100644 index 00000000000..90c7538bc1b --- /dev/null +++ b/frontend/src/components/dialog/department-detail-dialog.js @@ -0,0 +1,249 @@ +import React, { Fragment, } from 'react'; +import PropTypes from 'prop-types'; +import { gettext, isOrgContext, username } from '../../utils/constants'; +import { Modal, ModalBody } from 'reactstrap'; +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 Department from '../../models/department'; +import SeahubModalHeader from '../common/seahub-modal-header'; +import DepartmentGroup from './department-detail-widget/department-group'; +import DepartmentGroupMembers from './department-detail-widget/department-group-members'; +import DepartmentGroupMemberSelected from './department-detail-widget/department-group-member-selected'; +import '../../css/manage-members-dialog.css'; +import '../../css/group-departments.css'; + +const propTypes = { + groupID: PropTypes.any, + toggleManageMembersDialog: PropTypes.func, + toggleDepartmentDetailDialog: PropTypes.func, + isOwner: PropTypes.bool, + addUserShares: PropTypes.func, + usedFor: PropTypes.oneOf(['add_group_member', 'add_user_share']), + userList: PropTypes.array, +}; + +class DepartmentDetailDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + departments: [], + departmentMembers: [], + newMembersTempObj: {}, + currentDepartment: {}, + departmentsLoading: true, + membersLoading: true, + selectedMemberMap: {}, + departmentsTree: [], + }; + } + + componentDidMount() { + this.getSelectedMembers(); + this.getDepartmentsList(); + } + + getSelectedMembers = () => { + const { usedFor, userList, groupID } = this.props; + if (usedFor === 'add_user_share') { + let selectedMemberMap = {}; + selectedMemberMap[username] = true; + userList.forEach(member => { + selectedMemberMap[member.email] = true; + }); + this.setState({ selectedMemberMap }); + } + else if (usedFor === 'add_group_member') { + seafileAPI.listGroupMembers(groupID).then((res) => { + const groupMembers = res.data; + let selectedMemberMap = {}; + selectedMemberMap[username] = true; + groupMembers.forEach(member => { + selectedMemberMap[member.email] = true; + }); + this.setState({ selectedMemberMap }); + }).catch(error => { + this.onError(error); + }); + } + }; + + onError = (error) => { + let errMsg = Utils.getErrorMsg(error, true); + if (!error.response || error.response.status !== 403) { + toaster.danger(errMsg); + } + }; + + initDepartments(departments) { + const parentIdMap = {}; + for (let i = 0; i < departments.length; i++) { + let item = departments[i]; + parentIdMap[item.parent_group_id] = true; + } + return departments.map(depart => { + depart.hasChild = !!parentIdMap[depart.id]; + depart.isExpanded = false; + return depart; + }); + } + + getDepartmentsList = () => { + seafileAPI.listAddressBookDepartments().then((res) => { + let departments = res.data.departments.map(item => { + return new Department(item); + }); + let currentDepartment = departments.length > 0 ? departments[0] : {}; + let departmentsTree = this.initDepartments(departments); + this.setState({ + departments: departments, + currentDepartment: currentDepartment, + departmentsLoading: false, + departmentsTree: departmentsTree + }); + this.getMembers(currentDepartment.id); + }).catch(error => { + this.onError(error); + }); + }; + + getMembers = (department_id) => { + this.setState({ membersLoading: true }); + seafileAPI.listAddressBookDepartmentMembers(department_id).then((res) => { + this.setState({ + departmentMembers: res.data.members, + membersLoading: false, + }); + }).catch(error => { + this.onError(error); + }); + }; + + toggle = () => { + this.props.toggleDepartmentDetailDialog(); + }; + + onMemberChecked = (member) => { + if (this.state.departmentMembers.indexOf(member) !== -1) { + let newMembersTempObj = this.state.newMembersTempObj; + if (member.email in newMembersTempObj) { + delete newMembersTempObj[member.email]; + } else { + newMembersTempObj[member.email] = member; + } + this.setState({ newMembersTempObj: newMembersTempObj }); + } + }; + + addGroupMember = () => { + let emails = Object.keys(this.state.newMembersTempObj); + seafileAPI.addGroupMembers(this.props.groupID, emails).then((res) => { + this.toggle(); + this.props.toggleManageMembersDialog(); + }).catch(error => { + this.onError(error); + }); + }; + + addUserShares = () => { + this.props.addUserShares(this.state.newMembersTempObj); + }; + + removeSelectedMember = (email) => { + let newMembersTempObj = this.state.newMembersTempObj; + delete newMembersTempObj[email]; + this.setState({ newMembersTempObj: newMembersTempObj }); + }; + + setCurrent = (department) => { + this.setState({ currentDepartment: department }); + }; + + selectAll = (members) => { + let { newMembersTempObj, selectedMemberMap } = this.state; + for (let member of members) { + if (Object.keys(selectedMemberMap).indexOf(member.email) !== -1) { + continue; + } + newMembersTempObj[member.email] = member; + } + this.setState({ newMembersTempObj: newMembersTempObj }); + }; + + renderHeader = () => { + const title = this.props.usedFor === 'add_group_member' ? gettext('Select group members') : gettext('Select shared users'); + return {title}; + }; + + render() { + let { departmentsLoading, departments } = this.state; + if (departmentsLoading) { + return ( + + {this.renderHeader()} + +
+
+
+ ); + } + + const emptyTips = ( + + {this.renderHeader()} + + +

{gettext('No departments')}

+
+
+
+ ); + + const details = ( + + {this.renderHeader()} + + + + + + + ); + return ( + + {(departments.length > 0 || isOrgContext) ? details : emptyTips} + + ); + } +} + +DepartmentDetailDialog.propTypes = propTypes; + +export default DepartmentDetailDialog; diff --git a/frontend/src/components/dialog/department-detail-widget/department-group-member-selected.js b/frontend/src/components/dialog/department-detail-widget/department-group-member-selected.js new file mode 100644 index 00000000000..9cd00ba743c --- /dev/null +++ b/frontend/src/components/dialog/department-detail-widget/department-group-member-selected.js @@ -0,0 +1,107 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Button, ModalFooter } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; + +const ItemPropTypes = { + member: PropTypes.object, + removeSelectedMember: PropTypes.func +}; + +class Item extends Component { + constructor(props) { + super(props); + this.state = { + highlight: false, + }; + } + + handleMouseEnter = () => { + this.setState({ highlight: true }); + }; + + handleMouseLeave = () => { + this.setState({ highlight: false }); + }; + + removeSelectedMember = (email) => { + this.props.removeSelectedMember(email); + }; + + render() { + const { member } = this.props; + return ( + + + {member.name} + + + + + + ); + } +} + +Item.propTypes = ItemPropTypes; + + +const DepartmentGroupMemberSelectedPropTypes = { + members: PropTypes.object.isRequired, + removeSelectedMember: PropTypes.func.isRequired, + addGroupMember: PropTypes.func.isRequired, + toggle: PropTypes.func.isRequired, + usedFor: PropTypes.string, + addUserShares: PropTypes.func, +}; + +class DepartmentGroupMemberSelected extends Component { + + render() { + const { members, usedFor } = this.props; + return ( +
+
+
+
{gettext('Selected')}
+
+ {Object.keys(members).length > 0 && + + + {Object.keys(members).map((email, index) => { + return ( + + ); + })} + +
+ } +
+ + + {usedFor === 'add_group_member' && + + } + {usedFor === 'add_user_share' && + + } + +
+ ); + } +} + +DepartmentGroupMemberSelected.propTypes = DepartmentGroupMemberSelectedPropTypes; + +export default DepartmentGroupMemberSelected; diff --git a/frontend/src/components/dialog/department-detail-widget/department-group-members.js b/frontend/src/components/dialog/department-detail-widget/department-group-members.js new file mode 100644 index 00000000000..44db25333e1 --- /dev/null +++ b/frontend/src/components/dialog/department-detail-widget/department-group-members.js @@ -0,0 +1,175 @@ +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'; + +const ItemPropTypes = { + member: PropTypes.object, + index: PropTypes.number, + tip: PropTypes.string, + memberSelected: PropTypes.object, + isMemberSelected: PropTypes.bool, + onUserChecked: PropTypes.func.isRequired, +}; + +class Item extends Component { + constructor(props) { + super(props); + this.state = { + highlight: false, + tooltipOpen: false, + }; + } + + handleMouseEnter = () => { + this.setState({ highlight: true }); + }; + + handleMouseLeave = () => { + this.setState({ highlight: false }); + }; + + onChange = (e) => { + const { member } = this.props; + this.props.onUserChecked(member); + }; + + toggleTooltip = () => { + this.setState({ tooltipOpen: !this.state.tooltipOpen }); + }; + + render() { + const { member, memberSelected, isMemberSelected, index, tip } = this.props; + if (isMemberSelected) { + return ( + + + + + + {member.name} + + + + {tip} + + + + ); + } + return ( + + + + + + {member.name} + + ); + } +} + +Item.propTypes = ItemPropTypes; + + +const DepartmentGroupMembersPropTypes = { + members: PropTypes.array.isRequired, + memberSelected: PropTypes.object.isRequired, + onUserChecked: PropTypes.func.isRequired, + currentDepartment: PropTypes.object.isRequired, + selectedMemberMap: PropTypes.object, + selectAll: PropTypes.func.isRequired, + loading: PropTypes.bool, + usedFor: PropTypes.oneOf(['add_group_member', 'add_user_share']), +}; + +class DepartmentGroupMembers extends Component { + + selectAll = () => { + const { members } = this.props; + this.props.selectAll(members); + }; + + render() { + const { members, memberSelected, loading, selectedMemberMap, currentDepartment, usedFor } = this.props; + let headerTitle; + if (currentDepartment.id === -1) { + headerTitle = gettext('All users'); + } else { + headerTitle = currentDepartment.name + ' ' + gettext('members'); + } + if (loading) { + return ( +
+
+
+ +
+
+
+ ); + } + const enableSelectAll = Object.keys(memberSelected).length < members.length; + const tip = usedFor === 'add_group_member' ? gettext('User is already in this group') : gettext('It is already shared to user'); + return ( +
+
+
+
+ {headerTitle} +
+ {enableSelectAll ? +
{gettext('Select All')}
+ : +
{gettext('Select All')}
+ } +
+ {members.length > 0 ? + + + + {members.map((member, index) => { + return ( + + ); + })} + +
+
+ : + +

{gettext('No members')}

+
+ } +
+
+ ); + } +} + +DepartmentGroupMembers.propTypes = DepartmentGroupMembersPropTypes; + +export default DepartmentGroupMembers; diff --git a/frontend/src/components/dialog/department-detail-widget/department-group.js b/frontend/src/components/dialog/department-detail-widget/department-group.js new file mode 100644 index 00000000000..8b2beac86e2 --- /dev/null +++ b/frontend/src/components/dialog/department-detail-widget/department-group.js @@ -0,0 +1,151 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Loading from '../../../components/loading'; +import { gettext, isOrgContext } from '../../../utils/constants'; + +const ItemPropTypes = { + department: PropTypes.object, + departments: PropTypes.array, + getMembers: PropTypes.func.isRequired, + setCurrent: PropTypes.func.isRequired, + toggleExpanded: PropTypes.func.isRequired, + currentDepartment: PropTypes.object, + allMembersClick: PropTypes.bool, +}; + +class Item extends Component { + + getMembers = (e) => { + e.stopPropagation(); + const { department } = this.props; + this.props.getMembers(department.id); + this.props.setCurrent(department); + }; + + toggleExpanded = (e) => { + e.stopPropagation(); + this.props.toggleExpanded(this.props.department.id, !this.props.department.isExpanded); + }; + + renderSubDepartments = () => { + const { departments } = this.props; + return ( +
+ {departments.map((department, index) => { + if (department.parent_group_id !== this.props.department.id) return null; + return ( + + ); + })} +
+ ); + }; + + render() { + const { department, currentDepartment, allMembersClick } = this.props; + const isCurrent = !allMembersClick && currentDepartment.id === department.id; + const { hasChild, isExpanded } = department; + return ( + <> +
+ {hasChild && + + + } + {department.name} +
+ {(isExpanded && hasChild) && this.renderSubDepartments()} + + ); + } +} + +Item.propTypes = ItemPropTypes; + + +const DepartmentGroupPropTypes = { + departments: PropTypes.array.isRequired, + getMembers: PropTypes.func.isRequired, + setCurrent: PropTypes.func.isRequired, + currentDepartment: PropTypes.object.isRequired, + loading: PropTypes.bool, + departmentsTree: PropTypes.array, +}; + +class DepartmentGroup extends Component { + + constructor(props) { + super(props); + this.state = { + allMembersClick: !!isOrgContext + }; + } + + toggleExpanded = (id, state) => { + let departments = this.props.departmentsTree.slice(0); + let index = departments.findIndex(item => item.id === id); + departments[index].isExpanded = state; + this.setState({ departments }); + }; + + getMembers = (department_id) => { + this.props.getMembers(department_id); + this.setState({ allMembersClick: false }); + }; + + render() { + const { loading } = this.props; + let departments = this.props.departmentsTree; + if (loading) { + return (); + } + const { allMembersClick } = this.state; + return ( +
+
+ {isOrgContext && +
+ + {gettext('All users')} +
+ } + {departments.length > 0 && departments.map((department, index) => { + if (department.parent_group_id !== -1) return null; + return ( + + ); + })} +
+
+ ); + } +} + +DepartmentGroup.propTypes = DepartmentGroupPropTypes; + +export default DepartmentGroup; diff --git a/frontend/src/components/dialog/manage-members-dialog.js b/frontend/src/components/dialog/manage-members-dialog.js index cae1d1e94d9..cea7a016314 100644 --- a/frontend/src/components/dialog/manage-members-dialog.js +++ b/frontend/src/components/dialog/manage-members-dialog.js @@ -10,20 +10,23 @@ import '../../css/manage-members-dialog.css'; const propTypes = { groupID: PropTypes.string, isOwner: PropTypes.bool.isRequired, - toggleManageMembersDialog: PropTypes.func.isRequired + toggleManageMembersDialog: PropTypes.func, + toggleDepartmentDetailDialog: PropTypes.func, }; class ManageMembersDialog extends React.Component { render() { - const { groupID, isOwner, toggleManageMembersDialog: toggle } = this.props; + const { groupID, isOwner } = this.props; return ( - - {gettext('Manage group members')} + + {gettext('Manage group members')} diff --git a/frontend/src/components/dialog/share-to-user.js b/frontend/src/components/dialog/share-to-user.js index acedfd6e1e5..2a913b86e9e 100644 --- a/frontend/src/components/dialog/share-to-user.js +++ b/frontend/src/components/dialog/share-to-user.js @@ -1,12 +1,14 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { gettext, isPro, cloudMode, isOrgContext } from '../../utils/constants'; import { Button } from 'reactstrap'; -import { gettext, isPro } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; import toaster from '../toast'; import UserSelect from '../user-select'; import SharePermissionEditor from '../select-editor/share-permission-editor'; +import DepartmentDetailDialog from './department-detail-dialog'; import '../../css/invitations.css'; import '../../css/share-to-user.css'; @@ -167,7 +169,9 @@ class ShareToUser extends React.Component { errorMsg: [], permission: 'rw', sharedItems: [], - isWiki: this.props.repoType === 'wiki' + isWiki: this.props.repoType === 'wiki', + tmpUserList: [], + isShowDepartmentDetailDialog: false }; this.options = []; this.permissions = []; @@ -198,7 +202,16 @@ class ShareToUser extends React.Component { let repoID = this.props.repoID; seafileAPI.listSharedItems(repoID, path, 'user').then((res) => { if (res.data.length !== 0) { - this.setState({ sharedItems: res.data }); + let tmpUserList = res.data.map(item => { + return { + 'email': item.user_info.name, + 'name': item.user_info.nickname, + 'avatar_url': item.user_info.avatar_url, + 'contact_email': item.user_info.contact_email, + 'permission': item.permission + }; + }); + this.setState({ sharedItems: res.data, tmpUserList: tmpUserList }); } }).catch(error => { let errMessage = Utils.getErrorMsg(error); @@ -341,7 +354,88 @@ class ShareToUser extends React.Component { this.setState({ sharedItems: sharedItems }); }; + toggleDepartmentDetailDialog = () => { + this.setState({ isShowDepartmentDetailDialog: !this.state.isShowDepartmentDetailDialog }); + }; + + addUserShares = (membersSelectedObj) => { + let path = this.props.itemPath; + let repoID = this.props.repoID; + let users = Object.keys(membersSelectedObj); + + if (this.props.isGroupOwnedRepo) { + seafileAPI.shareGroupOwnedRepoToUser(repoID, this.state.permission, users, path).then(res => { + let errorMsg = []; + if (res.data.failed.length > 0) { + for (let i = 0 ; i < res.data.failed.length ; i++) { + errorMsg[i] = res.data.failed[i]; + } + } + // todo modify api + let items = res.data.success.map(item => { + let sharedItem = { + 'user_info': { 'nickname': item.user_name, 'name': item.user_email }, + 'permission': item.permission, + 'share_type': 'user', + }; + return sharedItem; + }); + this.setState({ + errorMsg: errorMsg, + sharedItems: this.state.sharedItems.concat(items), + selectedOption: null, + permission: 'rw', + }); + this.refs.userSelect.clearSelect(); + }).catch(error => { + if (error.response) { + let message = gettext('Library can not be shared to owner.'); + let errMessage = []; + errMessage.push(message); + this.setState({ + errorMsg: errMessage, + selectedOption: null, + }); + } + }); + } else { + seafileAPI.shareFolder(repoID, path, 'user', this.state.permission, users).then(res => { + let errorMsg = []; + if (res.data.failed.length > 0) { + for (let i = 0 ; i < res.data.failed.length ; i++) { + errorMsg[i] = res.data.failed[i]; + } + } + this.setState({ + errorMsg: errorMsg, + sharedItems: this.state.sharedItems.concat(res.data.success), + selectedOption: null, + permission: 'rw', + }); + this.refs.userSelect.clearSelect(); + }).catch(error => { + if (error.response) { + let message = gettext('Library can not be shared to owner.'); + let errMessage = []; + errMessage.push(message); + this.setState({ + errorMsg: errMessage, + selectedOption: null, + }); + } + }); + } + this.toggleDepartmentDetailDialog(); + }; + render() { + let showDeptBtn = true; + if (window.app.config.lang !== 'zh-cn') { + showDeptBtn = false; + } + if (cloudMode && !isOrgContext) { + showDeptBtn = false; + } let { sharedItems } = this.state; const thead = ( @@ -353,18 +447,28 @@ class ShareToUser extends React.Component { ); return ( - +
{thead}
- +
+ + {showDeptBtn && + + + } +
+ {this.state.isShowDepartmentDetailDialog && + + }
-
+ ); } } diff --git a/frontend/src/components/list-and-add-group-members.js b/frontend/src/components/list-and-add-group-members.js index 9ce695e89f4..af0f2e1bcf0 100644 --- a/frontend/src/components/list-and-add-group-members.js +++ b/frontend/src/components/list-and-add-group-members.js @@ -2,7 +2,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { Button, InputGroup, InputGroupText, Input } from 'reactstrap'; import { Utils } from '../utils/utils'; -import { gettext } from '../utils/constants'; +import { gettext, cloudMode, isOrgContext } from '../utils/constants'; import { seafileAPI } from '../utils/seafile-api'; import UserSelect from './user-select'; import toaster from './toast'; @@ -10,6 +10,8 @@ import Loading from './loading'; import GroupMembers from './group-members'; const propTypes = { + toggleManageMembersDialog: PropTypes.func, + toggleDepartmentDetailDialog: PropTypes.func, groupID: PropTypes.string, isOwner: PropTypes.bool.isRequired }; @@ -159,12 +161,24 @@ class ManageMembersDialog extends React.Component { }); }; + onClickDeptBtn = () => { + this.props.toggleManageMembersDialog(); + this.props.toggleDepartmentDetailDialog(); + }; + render() { const { isLoading, hasNextPage, groupMembers, keyword, membersFound, searchActive } = this.state; + let showDeptBtn = true; + if (window.app.config.lang !== 'zh-cn') { + showDeptBtn = false; + } + if (cloudMode && !isOrgContext) { + showDeptBtn = false; + } return (

{gettext('Add group member')}

@@ -176,6 +190,9 @@ class ManageMembersDialog extends React.Component { isMulti={true} className="add-members-select" /> + {showDeptBtn && + + } {this.state.selectedOption ? : diff --git a/frontend/src/css/group-departments.css b/frontend/src/css/group-departments.css new file mode 100644 index 00000000000..fb94813804e --- /dev/null +++ b/frontend/src/css/group-departments.css @@ -0,0 +1,171 @@ +.department-dialog .department-dialog-content { + padding: 0; + min-height: 30rem; + display: flex; + overflow: hidden; + flex-wrap: nowrap; + align-content: space-between; + justify-content: space-between; + flex-direction: row; +} + +.department-dialog .department-dialog-content>div { + max-height: calc(100vh - 120px); + overflow-y: auto; +} + +.department-dialog-content .department-dialog-group { + flex: 0 0 30%; + padding: 1rem; + border-right: 1px solid #eee; +} + +.department-dialog-content .department-dialog-group .tr-highlight .dtable-icon-groups { + padding-right: 10px; + color: #ffffff; +} + +.department-dialog-content .department-dialog-group .dtable-icon-groups { + padding-right: 10px; + color: #9c9c9c; +} + +.department-dialog-content .department-dialog-member { + display: flex; + flex: 0 0 35%; + border-right: 1px solid #eee; +} + +.department-dialog-content .department-dialog-member-selected { + display: flex; + flex: 0 0 35%; + border-right: 1px solid #eee; + flex-direction: column; + justify-content: space-between; +} + +.department-dialog-content .department-dialog-member-selected .modal-footer { + border-top: none; +} + +.department-dialog-content .department-dialog-member-selected .dtable-icon-cancel { + cursor: pointer; + color: #959595; +} + +.department-dialog-content .department-dialog-group .group-item { + cursor: pointer; + padding: 5px; + border-radius: 5px; +} + +.department-dialog-content .department-dialog-group .group-item:hover { + background-color: #f5f5f5; +} + +.department-dialog-content .department-dialog-group .group-item.tr-highlight:hover, +.department-dialog-content .department-dialog-group .tr-highlight { + background-color: #FF8000; + color: #ffffff; +} + +.department-dialog-member-head { + display: flex; + padding: 0 0 12px 0; + justify-content: space-between; +} + +.department-dialog-member-head .department-name { + font-size: 0.8125rem; + color: #9c9c9c; +} + +.department-dialog-member-head .select-all { + cursor: pointer; + font-size: 0.8125rem; + color: #ea7500;; +} + +.department-dialog-member-head .select-all-disable { + font-size: 0.8125rem; + color: rgb(248, 205, 160); +} + +.department-dialog-member-table td, +.department-dialog-member-head td { + border: none; + text-align: left; + padding: 0; +} + +.department-dialog-member-table { + display: block; + text-align: center; + max-height: calc(100% - 32px); + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +.department-dialog-member-table tr { + display: table; + width: 100%; + table-layout: fixed; + height: 36px; +} + +.department-dialog-member-table .sf3-font-help { + color: #999; +} + +.department-dialog-member .empty-tip { + border: none; + background-color: transparent; +} + +.department-dialog-member .empty-tip .no-items-img-tip { + margin-bottom: 10px; +} + +.department-dialog-member .empty-tip h2 { + color: #999; + font-weight: normal; + font-size: 1rem; +} + +.department-dialog-content .avatar { + width: 24px; + height: 24px; + line-height: 24px; +} + +.department-dialog-content tr td:first-child { + padding-left: 16px; +} + +.department-dialog-member-table tr td:first-child { + padding-bottom: 2px; +} + +.department-dialog-member-table tr td .dtable-icon-use-help { + color: #bdbdbd; +} + +.department-dialog-member-table tr td .dtable-icon-use-help:hover { + color: #888; +} + +.tooltip-inner { + font-size: 13px; + font-weight: lighter; + text-align: justify; + color: #FFF; + background-color: #303133; +} + +.department-dialog-member-selected tr td:last-child { + padding-right: 16px; +} + +.department-dialog-member-selected .modal-footer .btn { + min-width: 80px; +} diff --git a/frontend/src/css/manage-members-dialog.css b/frontend/src/css/manage-members-dialog.css index feb68f2e065..2f5b1ec0abb 100644 --- a/frontend/src/css/manage-members-dialog.css +++ b/frontend/src/css/manage-members-dialog.css @@ -18,6 +18,7 @@ .add-members { display: flex; justify-content: space-between; + position: relative; } .add-members .add-members-select { @@ -32,6 +33,21 @@ margin-top: 10px; } +.group-manage-members-dialog .add-members .toggle-detail-btn { + position: absolute; + top: 6px; + right: 90px; + border-left: 1px solid #ccc; + padding-left: 9px; + font-size: 18px; + cursor: pointer; + color: #999; +} + +.group-manage-members-dialog .add-members .toggle-detail-btn:hover { + color: #666; +} + .group-manage-members-dialog .search-group-members { color: #999; font-size: 14px; diff --git a/frontend/src/css/share-to-user.css b/frontend/src/css/share-to-user.css index f6f8c77b780..1ed4faf70ba 100644 --- a/frontend/src/css/share-to-user.css +++ b/frontend/src/css/share-to-user.css @@ -16,3 +16,42 @@ font-size: 1rem; font-weight: 500; } + +.share-link-container .share-user-avatar { + width: 25px; + height: 25px; + border-radius: 50%; +} + +.share-link-container .share-link-tip { + background-color: #f6eddf; + margin: -1rem -1.5rem 1rem; + padding: 0.5rem 1rem; +} + + +.share-link-container .add-members { + display: flex; + justify-content: space-between; + position: relative; +} + +.share-link-container .add-members .reviewer-select { + width: 300px; + max-width: calc(100% - 10px); +} + +.share-link-container .add-members .toggle-detail-btn { + position: absolute; + top: 6px; + right: 20px; + border-left: 1px solid #ccc; + padding-left: 9px; + font-size: 18px; + color: #999; + cursor: pointer; +} + +.user-select.user-select-right-btn .true__value-container { + padding-right: 40px; +} diff --git a/frontend/src/models/department.js b/frontend/src/models/department.js new file mode 100644 index 00000000000..ec45adc87b2 --- /dev/null +++ b/frontend/src/models/department.js @@ -0,0 +1,10 @@ +export default class Department { + constructor(obj) { + this.id = obj.id || null; + this.name = obj.name || null; + this.owner = obj.owner || null; + this.created_at = obj.created_at || null; + this.parent_group_id = obj.parent_group_id || null; + this.quota = obj.quota || null; + } +} diff --git a/frontend/src/pages/groups/group-view.js b/frontend/src/pages/groups/group-view.js index 16bdcb9734c..0104c273448 100644 --- a/frontend/src/pages/groups/group-view.js +++ b/frontend/src/pages/groups/group-view.js @@ -17,6 +17,7 @@ import RenameGroupDialog from '../../components/dialog/rename-group-dialog'; import TransferGroupDialog from '../../components/dialog/transfer-group-dialog'; import ImportMembersDialog from '../../components/dialog/import-members-dialog'; import ManageMembersDialog from '../../components/dialog/manage-members-dialog'; +import DepartmentDetailDialog from '../../components/dialog/department-detail-dialog'; import LeaveGroupDialog from '../../components/dialog/leave-group-dialog'; import SharedRepoListView from '../../components/shared-repo-list-view/shared-repo-list-view'; import SortOptionsDialog from '../../components/dialog/sort-options'; @@ -56,6 +57,7 @@ class GroupView extends React.Component { libraryType: 'group', isCreateRepoDialogShow: false, isDepartmentGroup: false, + isShowDepartmentDetailDialog: false, showGroupDropdown: false, showGroupMembersPopover: false, showRenameGroupDialog: false, @@ -406,6 +408,12 @@ class GroupView extends React.Component { }); }; + toggleDepartmentDetailDialog = () => { + this.setState({ + isShowDepartmentDetailDialog: !this.state.isShowDepartmentDetailDialog + }); + }; + render() { const { isLoading, repoList, errMessage, emptyTip, @@ -558,6 +566,16 @@ class GroupView extends React.Component { groupID={this.props.groupID} onGroupChanged={this.props.onGroupChanged} isOwner={this.state.isOwner} + toggleDepartmentDetailDialog={this.toggleDepartmentDetailDialog} + /> + } + {this.state.isShowDepartmentDetailDialog && + } {this.state.isLeaveGroupDialogOpen && diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 3b5263f07fc..f87c3cf59ff 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -35,6 +35,8 @@ export const appAvatarURL = window.app.config.avatarURL; export const faviconPath = window.app.config.faviconPath; export const loginBGPath = window.app.config.loginBGPath; export const enableRepoAutoDel = window.app.config.enableRepoAutoDel; +export const cloudMode = window.app.pageOptions.cloudMode; +export const isOrgContext = window.app.pageOptions.isOrgContext; // pageOptions export const trashReposExpireDays = window.app.pageOptions.trashReposExpireDays; diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js index 82b655239e0..050fb732297 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -94,6 +94,16 @@ class SeafileAPI { return this.req.get(url); } + listAddressBookDepartments() { + const url = this.server + '/api/v2.1/address-book/departments/'; + return this.req.get(url); + } + + listAddressBookDepartmentMembers(department_id) { + const url = this.server + '/api/v2.1/address-book/departments/' + department_id + '/members/'; + return this.req.get(url); + } + listGroups(withRepos = false) { let options = { with_repos: withRepos ? 1 : 0 }; const url = this.server + '/api/v2.1/groups/'; diff --git a/seahub/api2/endpoints/address_book/departments.py b/seahub/api2/endpoints/address_book/departments.py new file mode 100644 index 00000000000..9f71ef84851 --- /dev/null +++ b/seahub/api2/endpoints/address_book/departments.py @@ -0,0 +1,131 @@ +import logging + +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from seaserv import seafile_api, ccnet_api + +from seahub.api2.utils import api_error +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.authentication import TokenAuthentication +from seahub.avatar.templatetags.avatar_tags import api_avatar_url +from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email + +from seahub.utils import is_org_context +from seahub.utils.timeutils import timestamp_to_isoformat_timestr + +logger = logging.getLogger(__name__) + + +def get_address_book_group_memeber_info(group_member_obj, avatar_size=80): + email = group_member_obj.user_name + avatar_url, is_default, date_uploaded = api_avatar_url(email, avatar_size) + is_admin = bool(group_member_obj.is_staff) + role = 'Admin' if is_admin else 'Member' + member_info = { + 'email': email, + "name": email2nickname(email), + "contact_email": email2contact_email(email), + "avatar_url": avatar_url, + "is_admin": is_admin, + "role": role, + } + + return member_info + + +def address_book_group_to_dict(group): + + if isinstance(group, int): + group = ccnet_api.get_group(group) + + return { + "id": group.id, + "name": group.group_name, + "owner": group.creator_name, + "created_at": timestamp_to_isoformat_timestr(group.timestamp), + "parent_group_id": group.parent_group_id, + "quota": seafile_api.get_group_quota(group.id), + } + + +class AddressBookDepartments(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAuthenticated,) + + def get(self, request): + """ List sub groups of a group in address book. + """ + if is_org_context(request): + org_id = request.user.org.org_id + groups = ccnet_api.get_org_top_groups(org_id) + else: + groups = ccnet_api.get_top_groups(including_org=False) + + all_groups = [] + try: + for g in groups: + all_groups.extend(ccnet_api.get_descendants_groups(g.id)) + + return_results = [] + for group in all_groups: + return_results.append(address_book_group_to_dict(group)) + except Exception as e: + logger.error(e) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') + + return Response({ + 'departments': return_results + }) + + +class AddressBookDepartmentMembers(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAuthenticated,) + + def _check_department_permission(self, org_id, department_id): + ''' + check if the department belongs to the org + ''' + # TODO + # owner = "%s@seafile_group" % department_id + # ws = Workspaces.objects.get_workspace_by_owner(owner) + # if ws and ws.org_id == org_id: + # return True + return False + + def get(self, request, department_id): + """ List members of a group in address book. + """ + + try: + department_id = int(department_id) + except Exception: + return api_error(status.HTTP_400_BAD_REQUEST, 'Department id invalid') + + org_id = request.user.org.org_id if request.user.org else None + if org_id and not self._check_department_permission(org_id, department_id): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + return_results = [] + members = ccnet_api.get_group_members(department_id) + for m in members: + member_info = get_address_book_group_memeber_info(m) + # filter empty-user from bug that made an empty-group-owner when creating department + if m.user_name == '': + continue + return_results.append(member_info) + except Exception as e: + logger.error(e) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') + + return Response({ + 'members': return_results + }) diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index bc06191f988..400c06a4b23 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -54,7 +54,10 @@ useGoFileserver: {% if USE_GO_FILESERVER %} true {% else %} false {% endif %}, serviceURL: '{{ service_url }}', seafileVersion: '{{ seafile_version }}', - avatarURL: '{{ avatar_url }}' + avatarURL: '{{ avatar_url }}', + cloudMode: {% if cloud_mode %} true {% else %} false {% endif %}, + isOrgContext: {% if org is not None %} true {% else %} false {% endif %}, + }, pageOptions: { csrfToken: "{{ csrf_token }}", diff --git a/seahub/urls.py b/seahub/urls.py index c9c72912883..111ec8e2a6d 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -31,6 +31,8 @@ from seahub.api2.endpoints.groups import Groups, Group from seahub.api2.endpoints.all_groups import AllGroups from seahub.api2.endpoints.departments import Departments +from seahub.api2.endpoints.address_book.departments import AddressBookDepartments, \ + AddressBookDepartmentMembers from seahub.api2.endpoints.shareable_groups import ShareableGroups from seahub.api2.endpoints.group_libraries import GroupLibraries, GroupLibrary @@ -340,7 +342,13 @@ re_path(r'^api/v2.1/search-file/$', SearchFile.as_view(), name='api-v2.1-search-file'), # departments - re_path(r'api/v2.1/departments/$', Departments.as_view(), name='api-v2.1-all-departments'), + re_path(r'^api/v2.1/departments/$', Departments.as_view(), name='api-v2.1-all-departments'), + re_path(r'^api/v2.1/address-book/departments/$', + AddressBookDepartments.as_view(), + name='api-v2.1-address-book-groups-departments'), + re_path(r'^api/v2.1/address-book/departments/(?P\d+)/members/$', + AddressBookDepartmentMembers.as_view(), + name='api-v2.1-address-book-groups-department-members'), ## user::groups re_path(r'^api/v2.1/all-groups/$', AllGroups.as_view(), name='api-v2.1-all-groups'),