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.avatar_url}) |
+ {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.avatar_url}) |
+ {member.name} |
+
+
+
+ {tip}
+
+ |
+
+ );
+ }
+ return (
+
+
+
+ |
+ ![]({member.avatar_url}) |
+ {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'),