mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-18 08:16:07 +00:00
@@ -0,0 +1,45 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
const propTypes = {
|
||||
formActionURL: PropTypes.string.isRequired,
|
||||
csrfToken: PropTypes.string.isRequired,
|
||||
toggle: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class ConfirmDisconnectDingtalk extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.form = React.createRef();
|
||||
}
|
||||
|
||||
disconnect = () => {
|
||||
this.form.current.submit();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formActionURL, csrfToken, toggle} = this.props;
|
||||
return (
|
||||
<Modal centered={true} isOpen={true} toggle={toggle}>
|
||||
<ModalHeader toggle={toggle}>{gettext('Disconnect')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure you want to disconnect?')}</p>
|
||||
<form ref={this.form} className="d-none" method="post" action={formActionURL}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
</form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.disconnect}>{gettext('Disconnect')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDisconnectDingtalk.propTypes = propTypes;
|
||||
|
||||
export default ConfirmDisconnectDingtalk;
|
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import Loading from '../loading';
|
||||
|
||||
const propTypes = {
|
||||
importDepartmentDialogToggle: PropTypes.func.isRequired,
|
||||
onImportDepartmentSubmit: PropTypes.func.isRequired,
|
||||
departmentsCount: PropTypes.number.isRequired,
|
||||
membersCount: PropTypes.number.isRequired,
|
||||
departmentName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
class ImportDingtalkDepartmentDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isLoading : false,
|
||||
};
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
this.props.importDepartmentDialogToggle(null);
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.onImportDepartmentSubmit();
|
||||
this.setState({ isLoading : true });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { departmentsCount, membersCount, departmentName } = this.props;
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>
|
||||
<span>{'导入部门 '}</span><span className="op-target" title={departmentName}>{departmentName}</span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{'将要导入 '}<strong>{departmentsCount}</strong>{' 个部门,其中包括 '}<strong>{membersCount}</strong>{' 个成员'}</p>
|
||||
{this.state.isLoading && <Loading/>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.toggle}>{'取消'}</Button>
|
||||
<Button color="primary" onClick={this.handleSubmit}>{'导入'}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportDingtalkDepartmentDialog.propTypes = propTypes;
|
||||
|
||||
export default ImportDingtalkDepartmentDialog;
|
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
import ModalPortal from '../modal-portal';
|
||||
import ConfirmDisconnectDingtalk from '../dialog/confirm-disconnect-dingtalk';
|
||||
|
||||
const {
|
||||
csrfToken,
|
||||
langCode,
|
||||
socialConnectedDingtalk,
|
||||
socialNextPage
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class SocialLoginDintalk extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isConfirmDialogOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
confirmDisconnect = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
isConfirmDialogOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({
|
||||
isConfirmDialogOpen: !this.state.isConfirmDialogOpen
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="setting-item" id="social-auth">
|
||||
<h3 className="setting-item-heading">{gettext('Social Login')}</h3>
|
||||
<p className="mb-2">{langCode == 'zh-cn' ? '钉钉': 'Dingtalk'}</p>
|
||||
{socialConnectedDingtalk ?
|
||||
<a href="#" className="btn btn-outline-primary" onClick={this.confirmDisconnect}>{gettext('Disconnect')}</a> :
|
||||
<a href={`${siteRoot}dingtalk/connect/?next=${encodeURIComponent(socialNextPage)}`} className="btn btn-outline-primary">{gettext('Connect')}</a>
|
||||
}
|
||||
</div>
|
||||
{this.state.isConfirmDialogOpen && (
|
||||
<ModalPortal>
|
||||
<ConfirmDisconnectDingtalk
|
||||
formActionURL={`${siteRoot}dingtalk/disconnect/?next=${encodeURIComponent(socialNextPage)}`}
|
||||
csrfToken={csrfToken}
|
||||
toggle={this.toggleDialog}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SocialLoginDintalk;
|
87
frontend/src/css/dingtalk-departments.css
Normal file
87
frontend/src/css/dingtalk-departments.css
Normal file
@@ -0,0 +1,87 @@
|
||||
.cur-view-content {
|
||||
position: relative;
|
||||
}
|
||||
.dir-content-main {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 75%;
|
||||
overflow-y: hidden;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.dir-content-main:hover {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dir-content-main table td {
|
||||
line-height: 2rem;
|
||||
}
|
||||
.dir-content-main .empty-tip {
|
||||
box-shadow: none;
|
||||
}
|
||||
.dir-content-main .empty-tip img {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
.dir-content-nav {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 24%;
|
||||
}
|
||||
.dir-content-nav:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
.dir-content-resize {
|
||||
position: absolute;
|
||||
left: 25%;
|
||||
height: 100%;
|
||||
width: 1%;
|
||||
border-left: 1px solid #eee;
|
||||
}
|
||||
.department-children {
|
||||
padding-left: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
.tree-node-inner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.tree-node-inner i {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 0.3rem;
|
||||
color: silver;
|
||||
}
|
||||
.tree-node-inner-hover {
|
||||
background-color: #FFEFB2;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tree-node-hight-light {
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
background-color: #feac74 !important;
|
||||
}
|
||||
.tree-node-hight-light i {
|
||||
color: #fff;
|
||||
}
|
||||
.tree-node-hight-light .attr-action-icon,
|
||||
.tree-node-hight-light .attr-action-icon:focus,
|
||||
.tree-node-hight-light .attr-action-icon:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
.tree-node-icon {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
.tree-node-text {
|
||||
width: calc(100% - 2.5rem);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 24px;
|
||||
}
|
||||
.tree-view {
|
||||
padding: 12px 12px 12px 0;
|
||||
flex: 1 1;
|
||||
}
|
355
frontend/src/pages/sys-admin/dingtalk-departments.js
Normal file
355
frontend/src/pages/sys-admin/dingtalk-departments.js
Normal file
@@ -0,0 +1,355 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Button } from 'reactstrap';
|
||||
import _ from 'lodash';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { gettext, siteRoot, isPro } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../../components/toast';
|
||||
import Account from '../../components/common/account';
|
||||
import { DingtalkDepartmentMembersList, DingtalkDepartmentsTreePanel } from './dingtalk';
|
||||
import ImportDingtalkDepartmentDialog from '../../components/dialog/import-dingtalk-department-dialog';
|
||||
|
||||
import '../../css/work-weixin-departments.css';
|
||||
import '../../css/dingtalk-departments.css';
|
||||
|
||||
class DingtalkDepartments extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isTreeLoading: true,
|
||||
isMembersListLoading: true,
|
||||
departmentsTree: [],
|
||||
checkedDepartmentId: 0,
|
||||
membersTempObj: {},
|
||||
membersList: [],
|
||||
newUsersTempObj: {},
|
||||
isCheckedAll: false,
|
||||
canCheckUserIds: [],
|
||||
isImportDepartmentDialogShow: false,
|
||||
importDepartment: null,
|
||||
importDepartmentChildrenCount: 0,
|
||||
importDepartmentMembersCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
getDepartmentsTree = (list) => {
|
||||
let childIds = [];
|
||||
let parentIds = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (childIds.indexOf(list[i].id) === -1) {
|
||||
childIds.push(list[i].id);
|
||||
}
|
||||
if (parentIds.indexOf(list[i].parentid) === -1) {
|
||||
parentIds.push(list[i].parentid);
|
||||
}
|
||||
}
|
||||
let intersection = parentIds.filter((v) => {
|
||||
return childIds.indexOf(v) !== -1;
|
||||
});
|
||||
let rootIds = parentIds.concat(intersection).filter((v) => {
|
||||
return parentIds.indexOf(v) === -1 || intersection.indexOf(v) === -1;
|
||||
});
|
||||
let cloneData = _.cloneDeep(list);
|
||||
return cloneData.filter(father => {
|
||||
let branchArr = cloneData.filter(child => father.id === child.parentid);
|
||||
branchArr.length > 0 ? father.children = branchArr : '';
|
||||
return rootIds.indexOf(father.parentid) !== -1;
|
||||
});
|
||||
};
|
||||
|
||||
getDingtalkDepartmentsList = (departmentID) => {
|
||||
seafileAPI.adminListDingtalkDepartments(departmentID).then((res) => {
|
||||
if (!departmentID) {
|
||||
let departmentsTree = this.getDepartmentsTree(res.data.department);
|
||||
this.setState({
|
||||
isTreeLoading: false,
|
||||
departmentsTree: departmentsTree,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
importDepartmentChildrenCount: res.data.department.length,
|
||||
importDepartmentMembersCount: this.state.membersTempObj[departmentID].length,
|
||||
});
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.handleError(error);
|
||||
this.setState({
|
||||
isTreeLoading: false,
|
||||
isMembersListLoading: false,
|
||||
});
|
||||
if (error.response && error.response.status === 403) {
|
||||
window.location = siteRoot + 'sys/useradmin/';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getDingtalkDepartmentMembersList = (department_id) => {
|
||||
this.setState({
|
||||
isMembersListLoading: true,
|
||||
});
|
||||
seafileAPI.adminListDingtalkDepartmentMembers(department_id).then((res) => {
|
||||
let membersTempObj = this.state.membersTempObj;
|
||||
membersTempObj[department_id] = res.data.userlist;
|
||||
let canCheckUserIds = this.getCanCheckUserIds(res.data.userlist);
|
||||
this.setState({
|
||||
membersTempObj: membersTempObj,
|
||||
membersList: res.data.userlist,
|
||||
isMembersListLoading: false,
|
||||
canCheckUserIds: canCheckUserIds,
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({isMembersListLoading: false});
|
||||
this.handleError(error);
|
||||
});
|
||||
}
|
||||
getCanCheckUserIds = (membersList) => {
|
||||
let userIds = [];
|
||||
membersList.forEach((member) => {
|
||||
if (!member.email) userIds.push(member.userid);
|
||||
});
|
||||
return userIds;
|
||||
};
|
||||
|
||||
onChangeDepartment = (department_id) => {
|
||||
this.setState({
|
||||
newUsersTempObj: {},
|
||||
isCheckedAll: false,
|
||||
checkedDepartmentId: department_id,
|
||||
});
|
||||
if (!(department_id in this.state.membersTempObj)) {
|
||||
this.getDingtalkDepartmentMembersList(department_id);
|
||||
} else {
|
||||
let canCheckUserIds = this.getCanCheckUserIds(this.state.membersTempObj[department_id]);
|
||||
this.setState({
|
||||
membersList: this.state.membersTempObj[department_id],
|
||||
canCheckUserIds: canCheckUserIds,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onUserChecked = (user) => {
|
||||
if (this.state.canCheckUserIds.indexOf(user.userid) !== -1) {
|
||||
let newUsersTempObj = this.state.newUsersTempObj;
|
||||
if (user.userid in newUsersTempObj) {
|
||||
delete newUsersTempObj[user.userid];
|
||||
if (this.state.isCheckedAll) {
|
||||
this.setState({ isCheckedAll: false });
|
||||
}
|
||||
} else {
|
||||
newUsersTempObj[user.userid] = user;
|
||||
if (Object.keys(newUsersTempObj).length === this.state.canCheckUserIds.length) {
|
||||
this.setState({ isCheckedAll: true });
|
||||
}
|
||||
}
|
||||
this.setState({ newUsersTempObj: newUsersTempObj });
|
||||
}
|
||||
};
|
||||
|
||||
onAllUsersChecked = () => {
|
||||
this.setState({
|
||||
isCheckedAll: !this.state.isCheckedAll,
|
||||
}, () => {
|
||||
if (this.state.isCheckedAll) {
|
||||
let newUsersTempObj = {};
|
||||
let newUsersTempList = this.state.membersList.filter(user => {
|
||||
return this.state.canCheckUserIds.indexOf(user.userid) !== -1;
|
||||
});
|
||||
for (let i = 0; i < newUsersTempList.length; i++) {
|
||||
newUsersTempObj[newUsersTempList[i].userid] = newUsersTempList[i];
|
||||
}
|
||||
this.setState({ newUsersTempObj: newUsersTempObj });
|
||||
} else {
|
||||
this.setState({ newUsersTempObj: {} });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { newUsersTempObj } = this.state;
|
||||
if (JSON.stringify(newUsersTempObj) === '{}') return;
|
||||
let userList = [];
|
||||
for (let i in newUsersTempObj) {
|
||||
userList.push(newUsersTempObj[i]);
|
||||
}
|
||||
if (userList.length === 0) {
|
||||
toaster.danger('未选择成员', {duration: 3});
|
||||
return;
|
||||
}
|
||||
seafileAPI.adminAddDingtalkUsersBatch(userList).then((res) => {
|
||||
this.setState({
|
||||
newUsersTempObj: {},
|
||||
isCheckedAll: false,
|
||||
});
|
||||
if (res.data.success) {
|
||||
this.handleSubmitSuccess(res.data.success);
|
||||
}
|
||||
if (res.data.failed) {
|
||||
const fails= res.data.failed;
|
||||
for (let i = 0; i < fails.length; i++) {
|
||||
toaster.danger(fails[i].name + ' ' + fails[i].error_msg, {duration: 3});
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
handleSubmitSuccess = (success) => {
|
||||
let { membersTempObj, membersList, canCheckUserIds } = this.state;
|
||||
for (let i = 0; i < success.length; i++) {
|
||||
let { userid, name, email } = success[i];
|
||||
toaster.success(name + ' 成功导入', {duration: 1});
|
||||
// refresh all temp
|
||||
if (canCheckUserIds.indexOf(userid) !== -1) {
|
||||
canCheckUserIds.splice(canCheckUserIds.indexOf(userid), 1);
|
||||
}
|
||||
for (let j = 0; j < membersList.length; j++) {
|
||||
if (membersList[j].userid === userid) {
|
||||
membersList[j].email = email;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let departmentId in membersTempObj) {
|
||||
for (let k = 0; k < membersTempObj[departmentId].length; k++) {
|
||||
if (membersTempObj[departmentId][k].userid === userid) {
|
||||
membersTempObj[departmentId][k].email = email;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
membersTempObj: membersTempObj,
|
||||
membersList: membersList,
|
||||
canCheckUserIds: canCheckUserIds,
|
||||
});
|
||||
}
|
||||
|
||||
importDepartmentDialogToggle = (importDepartment) => {
|
||||
this.setState({
|
||||
isImportDepartmentDialogShow: !this.state.isImportDepartmentDialogShow,
|
||||
importDepartment: importDepartment,
|
||||
}, () => {
|
||||
if (importDepartment) {
|
||||
this.getDingtalkDepartmentsList(importDepartment.id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onImportDepartmentSubmit = () => {
|
||||
let importDepartment = this.state.importDepartment;
|
||||
if (!importDepartment) return;
|
||||
seafileAPI.adminImportDingtalkDepartment(importDepartment.id).then((res) => {
|
||||
this.setState({
|
||||
isMembersListLoading: true,
|
||||
checkedDepartmentId: importDepartment.id,
|
||||
membersTempObj: {},
|
||||
membersList: [],
|
||||
newUsersTempObj: {},
|
||||
isCheckedAll: false,
|
||||
canCheckUserIds: [],
|
||||
});
|
||||
this.getDingtalkDepartmentMembersList(importDepartment.id);
|
||||
this.importDepartmentDialogToggle(null);
|
||||
if (res.data.success) {
|
||||
this.handleImportDepartmentSubmitSuccess(res.data.success);
|
||||
}
|
||||
if (res.data.failed) {
|
||||
this.handleImportDepartmentSubmitFailed(res.data.failed);
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
};
|
||||
|
||||
handleImportDepartmentSubmitSuccess = (successes) => {
|
||||
for (let i = 0, len = successes.length; i < len; i++) {
|
||||
let success = successes[i];
|
||||
let successMsg = success.type === 'department' ? '部门 ' + success.department_name + ' 导入成功' : success.api_user_name + ' 导入成功' ;
|
||||
toaster.success(successMsg, { duration: 3 });
|
||||
}
|
||||
};
|
||||
|
||||
handleImportDepartmentSubmitFailed = (fails) => {
|
||||
for (let i = 0, len = fails.length; i < len; i++) {
|
||||
let fail = fails[i];
|
||||
let failName = fail.type === 'department' ? fail.department_name : fail.api_user_name;
|
||||
toaster.danger(failName + ' ' + fail.msg, { duration: 3} );
|
||||
}
|
||||
};
|
||||
|
||||
handleError = (error) => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getDingtalkDepartmentsList(null);
|
||||
}
|
||||
|
||||
renderNav() {
|
||||
const btnClass = 'btn btn-secondary operation-item ';
|
||||
return (
|
||||
<div className="main-panel-north border-left-show">
|
||||
<div className="cur-view-toolbar">
|
||||
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
|
||||
<Button className={btnClass + 'my-1 d-md-none'} onClick={this.onSubmit}>{'导入用户'}</Button>
|
||||
<Button className={btnClass + 'hidden-md-up'} onClick={this.onSubmit}>{'导入用户'}</Button>
|
||||
</div>
|
||||
<div className="common-toolbar">
|
||||
<Account isAdminPanel={true}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isImportDepartmentDialogShow, isTreeLoading, importDepartment, importDepartmentChildrenCount, importDepartmentMembersCount } = this.state;
|
||||
let canImportDepartment = !!(isPro && isImportDepartmentDialogShow && !isTreeLoading && importDepartment);
|
||||
return (
|
||||
<Fragment>
|
||||
{this.renderNav()}
|
||||
<div className="main-panel-center">
|
||||
<div className="cur-view-container">
|
||||
<div className="cur-view-path">
|
||||
<h3 className="sf-heading">{'钉钉集成'}</h3>
|
||||
</div>
|
||||
<div className="cur-view-content d-flex flex-row">
|
||||
<DingtalkDepartmentsTreePanel
|
||||
departmentsTree={this.state.departmentsTree}
|
||||
isTreeLoading={this.state.isTreeLoading}
|
||||
onChangeDepartment={this.onChangeDepartment}
|
||||
checkedDepartmentId={this.state.checkedDepartmentId}
|
||||
importDepartmentDialogToggle={this.importDepartmentDialogToggle}
|
||||
/>
|
||||
<div className="dir-content-resize"></div>
|
||||
<DingtalkDepartmentMembersList
|
||||
isMembersListLoading={this.state.isMembersListLoading}
|
||||
membersList={this.state.membersList}
|
||||
checkedDepartmentId={this.state.checkedDepartmentId}
|
||||
newUsersTempObj={this.state.newUsersTempObj}
|
||||
onUserChecked={this.onUserChecked}
|
||||
onAllUsersChecked={this.onAllUsersChecked}
|
||||
isCheckedAll={this.state.isCheckedAll}
|
||||
canCheckUserIds={this.state.canCheckUserIds}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{canImportDepartment &&
|
||||
<ImportDingtalkDepartmentDialog
|
||||
importDepartmentDialogToggle={this.importDepartmentDialogToggle}
|
||||
onImportDepartmentSubmit={this.onImportDepartmentSubmit}
|
||||
departmentsCount={importDepartmentChildrenCount}
|
||||
membersCount={importDepartmentMembersCount}
|
||||
departmentName={importDepartment.name}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DingtalkDepartments;
|
@@ -0,0 +1,84 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Table } from 'reactstrap';
|
||||
import { siteRoot } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
|
||||
const DingtalkDepartmentMembersListPropTypes = {
|
||||
isMembersListLoading: PropTypes.bool.isRequired,
|
||||
membersList: PropTypes.array.isRequired,
|
||||
newUsersTempObj: PropTypes.object.isRequired,
|
||||
checkedDepartmentId: PropTypes.number.isRequired,
|
||||
onUserChecked: PropTypes.func.isRequired,
|
||||
onAllUsersChecked: PropTypes.func.isRequired,
|
||||
isCheckedAll: PropTypes.bool.isRequired,
|
||||
canCheckUserIds: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
class DingtalkDepartmentMembersList extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { newUsersTempObj, checkedDepartmentId, isMembersListLoading, canCheckUserIds } = this.props;
|
||||
const membersList = this.props.membersList.map((member, index) => {
|
||||
let avatar = member.avatar;
|
||||
if (member.avatar.length > 0) {
|
||||
avatar = member.avatar;
|
||||
} else {
|
||||
avatar = siteRoot + 'media/avatars/default.png';
|
||||
}
|
||||
return (
|
||||
<tr key={checkedDepartmentId.toString() + member.userid}>
|
||||
<td>
|
||||
{!member.email &&
|
||||
<input type="checkbox" className="vam" onChange={() => this.props.onUserChecked(member)}
|
||||
checked={(member.userid in newUsersTempObj) ? 'checked' : ''}></input>}
|
||||
</td>
|
||||
<td><img className="avatar" src={avatar} alt=""></img></td>
|
||||
<td>{member.name}</td>
|
||||
<td>{member.mobile}</td>
|
||||
<td>{member.contact_email}</td>
|
||||
<td>{member.email && <i className="sf2-icon-tick"></i>}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="dir-content-main">
|
||||
{isMembersListLoading && <Loading/>}
|
||||
{!isMembersListLoading && this.props.membersList.length > 0 &&
|
||||
<Table hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">
|
||||
{canCheckUserIds.length > 0 &&
|
||||
<input type="checkbox" className="vam" checked={this.props.isCheckedAll}
|
||||
onChange={() => this.props.onAllUsersChecked()}></input>}
|
||||
</th>
|
||||
<th width="10%"></th>
|
||||
<th width="20%">{'名称'}</th>
|
||||
<th width="20%">{'手机号'}</th>
|
||||
<th width="30%">{'邮箱'}</th>
|
||||
<th width="15%">{'已添加'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{membersList}</tbody>
|
||||
</Table>
|
||||
}
|
||||
{!isMembersListLoading && this.props.membersList.length === 0 &&
|
||||
<div className="message empty-tip text-center">
|
||||
<img src={`${siteRoot}media/img/member-list-empty-2x.png`} alt=""/>
|
||||
<h4>{'成员列表为空'}</h4>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DingtalkDepartmentMembersList.propTypes = DingtalkDepartmentMembersListPropTypes;
|
||||
|
||||
export default DingtalkDepartmentMembersList;
|
@@ -0,0 +1,147 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||
import { gettext, isPro } from '../../../utils/constants';
|
||||
|
||||
const DingtalkDepartmentsTreeNodePropTypes = {
|
||||
index: PropTypes.number,
|
||||
department: PropTypes.object.isRequired,
|
||||
isChildrenShow: PropTypes.bool.isRequired,
|
||||
onChangeDepartment: PropTypes.func.isRequired,
|
||||
checkedDepartmentId: PropTypes.number.isRequired,
|
||||
importDepartmentDialogToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class DingtalkDepartmentsTreeNode extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isChildrenShow: false,
|
||||
dropdownOpen: false,
|
||||
active: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleChildren = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.setState({
|
||||
isChildrenShow: !this.state.isChildrenShow,
|
||||
});
|
||||
};
|
||||
|
||||
dropdownToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
this.setState({ dropdownOpen: !this.state.dropdownOpen });
|
||||
};
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({ active: true });
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
if (this.state.dropdownOpen) return;
|
||||
this.setState({ active: false });
|
||||
};
|
||||
|
||||
importDepartmentDialogToggle = (depart) => {
|
||||
this.setState({ active: false });
|
||||
this.props.importDepartmentDialogToggle(depart);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.index === 0) {
|
||||
this.setState({ isChildrenShow: true });
|
||||
this.props.onChangeDepartment(this.props.department.id);
|
||||
}
|
||||
}
|
||||
|
||||
renderTreeNodes = (departmentsTree) => {
|
||||
if (departmentsTree.length > 0) {
|
||||
return departmentsTree.map((department) => {
|
||||
return (
|
||||
<DingtalkDepartmentsTreeNode
|
||||
key={department.id}
|
||||
department={department}
|
||||
isChildrenShow={this.state.isChildrenShow}
|
||||
onChangeDepartment={this.props.onChangeDepartment}
|
||||
checkedDepartmentId={this.props.checkedDepartmentId}
|
||||
importDepartmentDialogToggle={this.importDepartmentDialogToggle}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
changeDept = (departmentID) => {
|
||||
const { department, checkedDepartmentId } = this.props;
|
||||
this.props.onChangeDepartment(departmentID);
|
||||
if (checkedDepartmentId === department.id && !this.state.isChildrenShow) {
|
||||
this.setState({ isChildrenShow: true });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isChildrenShow, department, checkedDepartmentId } = this.props;
|
||||
let toggleClass = classNames({
|
||||
'folder-toggle-icon fa fa-caret-down': department.children && this.state.isChildrenShow,
|
||||
'folder-toggle-icon fa fa-caret-right': department.children && !this.state.isChildrenShow,
|
||||
});
|
||||
let nodeInnerClass = classNames({
|
||||
'tree-node-inner': true,
|
||||
'tree-node-inner-hover': this.state.active,
|
||||
'tree-node-hight-light': checkedDepartmentId === department.id
|
||||
});
|
||||
return (
|
||||
<Fragment>
|
||||
{isChildrenShow &&
|
||||
<div
|
||||
className={nodeInnerClass}
|
||||
onClick={() => this.changeDept(department.id)}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<span className="tree-node-icon" onClick={(e) => this.toggleChildren(e)}>
|
||||
<i className={toggleClass}></i>
|
||||
</span>
|
||||
<span className="tree-node-text">{department.name}</span>
|
||||
{isPro &&
|
||||
<Dropdown
|
||||
isOpen={this.state.dropdownOpen}
|
||||
toggle={(e) => this.dropdownToggle(e)}
|
||||
direction="down"
|
||||
style={this.state.active ? {} : { opacity: 0 }}
|
||||
>
|
||||
<DropdownToggle
|
||||
tag='i'
|
||||
className='fa fa-ellipsis-v cursor-pointer attr-action-icon'
|
||||
title={gettext('More Operations')}
|
||||
data-toggle="dropdown"
|
||||
aria-expanded={this.state.dropdownOpen}
|
||||
>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu className="drop-list" right={true}>
|
||||
<DropdownItem
|
||||
onClick={this.importDepartmentDialogToggle.bind(this, department)}
|
||||
id={department.id}
|
||||
>{'导入部门'}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{this.state.isChildrenShow &&
|
||||
<div className="department-children">
|
||||
{department.children && this.renderTreeNodes(department.children)}
|
||||
</div>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DingtalkDepartmentsTreeNode.propTypes = DingtalkDepartmentsTreeNodePropTypes;
|
||||
|
||||
export default DingtalkDepartmentsTreeNode;
|
@@ -0,0 +1,51 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Loading from '../../../components/loading';
|
||||
import DingtalkDepartmentsTreeNode from './dingtalk-departments-tree-node';
|
||||
|
||||
const DingtalkDepartmentsTreePanelPropTypes = {
|
||||
isTreeLoading: PropTypes.bool.isRequired,
|
||||
departmentsTree: PropTypes.array.isRequired,
|
||||
onChangeDepartment: PropTypes.func.isRequired,
|
||||
checkedDepartmentId: PropTypes.number.isRequired,
|
||||
importDepartmentDialogToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class DingtalkDepartmentsTreePanel extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { departmentsTree } = this.props;
|
||||
return (
|
||||
<div className="dir-content-nav">
|
||||
<div className="tree-view tree">
|
||||
{this.props.isTreeLoading ?
|
||||
<Loading/> :
|
||||
<div className="tree-node">
|
||||
{departmentsTree.length > 0 && departmentsTree.map((department, index) => {
|
||||
return (
|
||||
<DingtalkDepartmentsTreeNode
|
||||
key={department.id}
|
||||
index={index}
|
||||
department={department}
|
||||
isChildrenShow={true}
|
||||
onChangeDepartment={this.props.onChangeDepartment}
|
||||
checkedDepartmentId={this.props.checkedDepartmentId}
|
||||
importDepartmentDialogToggle={this.props.importDepartmentDialogToggle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DingtalkDepartmentsTreePanel.propTypes = DingtalkDepartmentsTreePanelPropTypes;
|
||||
|
||||
export default DingtalkDepartmentsTreePanel;
|
5
frontend/src/pages/sys-admin/dingtalk/index.js
Normal file
5
frontend/src/pages/sys-admin/dingtalk/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import DingtalkDepartmentMembersList from './dingtalk-department-members-list';
|
||||
import DingtalkDepartmentsTreePanel from './dingtalk-departments-tree-panel';
|
||||
import DingtalkDepartmentsTreeNode from './dingtalk-departments-tree-node';
|
||||
|
||||
export { DingtalkDepartmentMembersList, DingtalkDepartmentsTreePanel, DingtalkDepartmentsTreeNode };
|
@@ -68,6 +68,7 @@ import Notifications from './notifications/notifications';
|
||||
import FileScanRecords from './file-scan-records';
|
||||
import VirusScanRecords from './virus-scan-records';
|
||||
import WorkWeixinDepartments from './work-weixin-departments';
|
||||
import DingtalkDepartments from './dingtalk-departments';
|
||||
|
||||
import Invitations from './invitations/invitations';
|
||||
import StatisticFile from './statistic/statistic-file';
|
||||
@@ -258,6 +259,11 @@ class SysAdmin extends React.Component {
|
||||
currentTab={currentTab}
|
||||
tabItemClick={this.tabItemClick}
|
||||
/>
|
||||
<DingtalkDepartments
|
||||
path={siteRoot + 'sys/dingtalk'}
|
||||
currentTab={currentTab}
|
||||
tabItemClick={this.tabItemClick}
|
||||
/>
|
||||
<AbuseReports path={siteRoot + 'sys/abuse-reports'} />
|
||||
</Router>
|
||||
</MainPanel>
|
||||
|
@@ -5,7 +5,7 @@ import Logo from '../../components/logo';
|
||||
import { gettext, siteRoot, isPro, otherPermission, canViewSystemInfo, canViewStatistic,
|
||||
canConfigSystem, canManageLibrary, canManageUser, canManageGroup, canViewUserLog,
|
||||
canViewAdminLog, constanceEnabled, multiTenancy, multiInstitution, sysadminExtraEnabled,
|
||||
enableGuestInvitation, enableTermsAndConditions, enableFileScan, enableWorkWeixin,
|
||||
enableGuestInvitation, enableTermsAndConditions, enableFileScan, enableWorkWeixin, enableDingtalk,
|
||||
enableShareLinkReportAbuse } from '../../utils/constants';
|
||||
|
||||
const propTypes = {
|
||||
@@ -259,6 +259,20 @@ class SidePanel extends React.Component {
|
||||
</Link>
|
||||
</li>
|
||||
}
|
||||
|
||||
{otherPermission && enableDingtalk &&
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className={`nav-link ellipsis ${this.getActiveClass('dingtalk')}`}
|
||||
to={siteRoot + 'sys/dingtalk/'}
|
||||
onClick={() => this.props.tabItemClick('dingtalk')}
|
||||
>
|
||||
<span className="sf3-font-enterprise-dingtalk sf3-font" aria-hidden="true"></span>
|
||||
<span className="nav-text">{'钉钉集成'}</span>
|
||||
</Link>
|
||||
</li>
|
||||
}
|
||||
|
||||
{otherPermission && enableShareLinkReportAbuse &&
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
|
3074
frontend/src/seafile-api.js
Normal file
3074
frontend/src/seafile-api.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import ListInAddressBook from './components/user-settings/list-in-address-book';
|
||||
import EmailNotice from './components/user-settings/email-notice';
|
||||
import TwoFactorAuthentication from './components/user-settings/two-factor-auth';
|
||||
import SocialLogin from './components/user-settings/social-login';
|
||||
import SocialLoginDingtalk from './components/user-settings/social-login-dingtalk';
|
||||
import DeleteAccount from './components/user-settings/delete-account';
|
||||
|
||||
import './css/toolbar.css';
|
||||
@@ -22,12 +23,13 @@ import './css/search.css';
|
||||
|
||||
import './css/user-settings.css';
|
||||
|
||||
const {
|
||||
const {
|
||||
canUpdatePassword, passwordOperationText,
|
||||
enableAddressBook,
|
||||
enableWebdavSecret,
|
||||
twoFactorAuthEnabled,
|
||||
enableWechatWork,
|
||||
enableDingtalk,
|
||||
enableDeleteAccount
|
||||
} = window.app.pageOptions;
|
||||
|
||||
@@ -44,6 +46,7 @@ class Settings extends React.Component {
|
||||
{show: isPro, href: '#email-notice', text: gettext('Email Notification')},
|
||||
{show: twoFactorAuthEnabled, href: '#two-factor-auth', text: gettext('Two-Factor Authentication')},
|
||||
{show: enableWechatWork, href: '#social-auth', text: gettext('Social Login')},
|
||||
{show: enableDingtalk, href: '#social-auth', text: gettext('Social Login')},
|
||||
{show: enableDeleteAccount, href: '#del-account', text: gettext('Delete Account')},
|
||||
];
|
||||
|
||||
@@ -62,7 +65,7 @@ class Settings extends React.Component {
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
updateUserInfo = (data) => {
|
||||
seafileAPI.updateUserInfo(data).then((res) => {
|
||||
this.setState({
|
||||
@@ -127,12 +130,13 @@ class Settings extends React.Component {
|
||||
</div>
|
||||
}
|
||||
{enableWebdavSecret && <WebdavPassword />}
|
||||
{enableAddressBook && this.state.userInfo &&
|
||||
{enableAddressBook && this.state.userInfo &&
|
||||
<ListInAddressBook userInfo={this.state.userInfo} updateUserInfo={this.updateUserInfo} />}
|
||||
<LanguageSetting />
|
||||
{isPro && <EmailNotice />}
|
||||
{twoFactorAuthEnabled && <TwoFactorAuthentication />}
|
||||
{enableWechatWork && <SocialLogin />}
|
||||
{enableDingtalk && <SocialLoginDingtalk />}
|
||||
{enableDeleteAccount && <DeleteAccount />}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -128,6 +128,7 @@ export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admi
|
||||
export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : '';
|
||||
export const otherPermission = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.other_permission : '';
|
||||
export const enableWorkWeixin = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin : '';
|
||||
export const enableDingtalk = window.sysadmin ? window.sysadmin.pageOptions.enable_dingtalk : '';
|
||||
export const enableSysAdminViewRepo = window.sysadmin ? window.sysadmin.pageOptions.enableSysAdminViewRepo : '';
|
||||
export const haveLDAP = window.sysadmin ? window.sysadmin.pageOptions.haveLDAP : '';
|
||||
export const enableShareLinkReportAbuse = window.sysadmin ? window.sysadmin.pageOptions.enable_share_link_report_abuse : '';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import cookie from 'react-cookies';
|
||||
import { SeafileAPI } from 'seafile-js';
|
||||
import { SeafileAPI } from '../seafile-api';
|
||||
import { siteRoot } from './constants';
|
||||
|
||||
let seafileAPI = new SeafileAPI();
|
||||
|
Reference in New Issue
Block a user