diff --git a/frontend/src/components/dialog/sysadmin-dialog/add-department-v2-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/add-department-v2-dialog.js
new file mode 100644
index 0000000000..177dfd9fb1
--- /dev/null
+++ b/frontend/src/components/dialog/sysadmin-dialog/add-department-v2-dialog.js
@@ -0,0 +1,99 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
+import toaster from '../../toast';
+import { gettext } from '../../../utils/constants';
+import { Utils, validateName } from '../../../utils/utils';
+import { systemAdminAPI } from '../../../utils/system-admin-api';
+
+const propTypes = {
+ parentNode: PropTypes.object,
+ addDepartment: PropTypes.func,
+ toggle: PropTypes.func,
+ setRootNode: PropTypes.func
+};
+
+class AddDepartmentV2Dialog extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ departName: '',
+ isSubmitBtnActive: false
+ };
+ }
+
+ onKeyDown = (e) => {
+ if (e.key === 'Enter') {
+ this.handleSubmit();
+ e.preventDefault();
+ }
+ };
+
+ handleChange = (e) => {
+ this.setState({
+ departName: e.target.value
+ }, () => {
+ this.setState({ isSubmitBtnActive: !!this.state.departName.trim() });
+ });
+ };
+
+ handleSubmit = () => {
+ let response = validateName(this.state.departName.trim());
+ if (!response.isValid) {
+ this.setState({ errMessage: response.message });
+ return;
+ }
+ const { parentNode } = this.props;
+ const parentNodeId = parentNode ? parentNode.id : -1;
+ systemAdminAPI.sysAdminAddNewDepartment(parentNodeId, this.state.departName.trim()).then((res) => {
+ if (parentNode) {
+ this.props.addDepartment(parentNode, res.data);
+ } else {
+ this.props.setRootNode(res.data);
+ }
+ this.props.toggle();
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ render() {
+ const { parentNode } = this.props;
+ const { isSubmitBtnActive } = this.state;
+ let title;
+ if (parentNode) {
+ title = gettext('Add department at') + ' ' + parentNode.name;
+ } else {
+ title = gettext('Create top department');
+ }
+ return (
+
+ {title}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+AddDepartmentV2Dialog.propTypes = propTypes;
+
+export default AddDepartmentV2Dialog;
diff --git a/frontend/src/components/dialog/sysadmin-dialog/delete-department-v2-confirm-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/delete-department-v2-confirm-dialog.js
new file mode 100644
index 0000000000..865ab3193a
--- /dev/null
+++ b/frontend/src/components/dialog/sysadmin-dialog/delete-department-v2-confirm-dialog.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Modal, ModalBody, ModalFooter, ModalHeader, Button } from 'reactstrap';
+import { gettext } from '../../../utils/constants';
+
+const propTypes = {
+ node: PropTypes.object,
+ toggle: PropTypes.func,
+ onDelete: PropTypes.func
+};
+
+class DeleteDepartmentV2ConfirmDialog extends React.Component {
+ render() {
+ const { node, toggle } = this.props;
+ return (
+
+
+ {gettext('Delete department')}
+
+
+ {gettext('Are you sure to delete')}{' '}{node.name} ?
+
+
+
+
+
+
+ );
+ }
+}
+
+DeleteDepartmentV2ConfirmDialog.propTypes = propTypes;
+
+export default DeleteDepartmentV2ConfirmDialog;
diff --git a/frontend/src/components/dialog/sysadmin-dialog/rename-department-v2-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/rename-department-v2-dialog.js
new file mode 100644
index 0000000000..a6bd3de015
--- /dev/null
+++ b/frontend/src/components/dialog/sysadmin-dialog/rename-department-v2-dialog.js
@@ -0,0 +1,87 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
+import toaster from '../../toast';
+import { gettext } from '../../../utils/constants';
+import { Utils, validateName } from '../../../utils/utils';
+import { systemAdminAPI } from '../../../utils/system-admin-api';
+
+const propTypes = {
+ node: PropTypes.object,
+ renameDepartment: PropTypes.func,
+ toggle: PropTypes.func
+};
+
+class RenameDepartmentV2Dialog extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ departName: props.node.name,
+ isSubmitBtnActive: false
+ };
+ }
+
+ onKeyDown = (e) => {
+ if (e.key === 'Enter') {
+ this.handleSubmit();
+ e.preventDefault();
+ }
+ };
+
+ handleChange = (e) => {
+ const value = e.target.value;
+ this.setState({
+ departName: value
+ }, () => {
+ this.setState({ isSubmitBtnActive: !!this.state.departName.trim() });
+ });
+ };
+
+ handleSubmit = () => {
+ let response = validateName(this.state.departName.trim());
+ if (!response.isValid) {
+ this.setState({ errMessage: response.message });
+ return;
+ }
+ const { node } = this.props;
+ systemAdminAPI.sysAdminRenameDepartment(node.id, this.state.departName.trim()).then((res) => {
+ this.props.renameDepartment(node, res.data);
+ this.props.toggle();
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ render() {
+ const { isSubmitBtnActive } = this.state;
+ return (
+
+ {gettext('Rename')}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+RenameDepartmentV2Dialog.propTypes = propTypes;
+
+export default RenameDepartmentV2Dialog;
diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-depart-member-v2-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-depart-member-v2-dialog.js
new file mode 100644
index 0000000000..0ca0ae4b64
--- /dev/null
+++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-depart-member-v2-dialog.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
+import { gettext } from '../../../utils/constants';
+import { systemAdminAPI } from '../../../utils/system-admin-api';
+import { Utils } from '../../../utils/utils';
+import UserSelect from '../../user-select';
+
+export default class AddDepartMemberV2Dialog extends React.Component {
+
+ static propTypes = {
+ toggle: PropTypes.func.isRequired,
+ nodeId: PropTypes.number.isRequired,
+ onMemberChanged: PropTypes.func.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ selectedOptions: [],
+ errMessage: '',
+ };
+ }
+
+ handleSelectChange = (options) => {
+ this.setState({ selectedOptions: options });
+ };
+
+ handleSubmit = () => {
+ const emails = this.state.selectedOptions.map(option => option.email);
+ if (emails.length === 0) return;
+ this.setState({ errMessage: '' });
+ systemAdminAPI.sysAdminAddGroupMember(this.props.nodeId, emails).then((res) => {
+ this.setState({ selectedOptions: [] });
+ if (res.data.failed.length > 0) {
+ this.setState({ errMessage: res.data.failed[0].error_msg });
+ }
+ if (res.data.success.length > 0) {
+ this.props.onMemberChanged();
+ this.props.toggle();
+ }
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ this.setState({ errMessage });
+ });
+ };
+
+ render() {
+ return (
+
+ {gettext('Add member')}
+
+
+ {this.state.errMessage && {this.state.errMessage}
}
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/frontend/src/css/add-user-to-departments.css b/frontend/src/css/add-user-to-departments.css
new file mode 100644
index 0000000000..6e65c97d48
--- /dev/null
+++ b/frontend/src/css/add-user-to-departments.css
@@ -0,0 +1,152 @@
+.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 35%;
+ 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: #999;
+}
+
+.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 65%;
+ 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: #ED7109;
+ 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: #666;
+}
+
+.department-dialog-member-head .select-all {
+ cursor: pointer;
+ font-size: 0.8125rem;
+ color: #ED7109;
+}
+
+.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-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/admin-common.css b/frontend/src/css/admin-common.css
new file mode 100644
index 0000000000..d968562179
--- /dev/null
+++ b/frontend/src/css/admin-common.css
@@ -0,0 +1,244 @@
+.org-admin .info-item-heading,
+.sys-admin .info-item-heading {
+ font-size: 15px;
+ font-weight: normal;
+ padding-bottom: 0.2em;
+ border-bottom: 1px solid #eee;
+ margin: 24px 0 0.7em;
+ color: inherit;
+}
+
+.org-admin .side-nav-con svg.multicolor-icon,
+.sys-admin .side-nav-con svg.multicolor-icon {
+ font-size: 20px;
+ width: 2rem;
+ color: #999;
+}
+
+.org-admin .side-nav-con .active [class^='multicolor-icon'],
+.sys-admin .side-nav-con .active [class^='multicolor-icon'] {
+ color: #fff;
+}
+
+.org-admin .side-nav-title,
+.sys-admin .side-nav-title {
+ color: #666666;
+ padding: 0 4px;
+ font-size: 15px;
+}
+
+.org-admin .main-panel .heading,
+.sys-admin .main-panel .heading {
+ height: 40px;
+ padding: 0 16px;
+ background: #f9f9f9;
+ font-size: 1rem;
+ font-weight: normal;
+ line-height: 40px;
+ margin: 0;
+ border-bottom: 1px solid #eee;
+}
+
+.org-admin .main-panel .nav .nav-item .nav-link,
+.sys-admin .main-panel .nav .nav-item .nav-link {
+ font-size: 16px;
+ border-bottom: 2px solid transparent;
+}
+
+.org-admin .main-panel .nav .nav-item .nav-link.active,
+.sys-admin .main-panel .nav .nav-item .nav-link.active {
+ color: #ED7109;
+ text-decoration: none;
+ border-bottom: 2px solid #ED7109;
+}
+
+.org-admin .main-panel .cur-view-path:after,
+.sys-admin .main-panel .cur-view-path:after {
+ display: none;
+}
+
+.org-admin .text-secondary,
+.sys-admin .text-secondary {
+ color: #999 !important;
+}
+
+.org-admin .side-nav-con .nav .nav-item,
+.sys-admin .side-nav-con .nav .nav-item {
+ font-size: 15px;
+}
+
+.org-admin .side-nav-con .nav .nav-item .nav-link,
+.sys-admin .side-nav-con .nav .nav-item .nav-link {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 36px;
+}
+
+.org-table-icon {
+ padding-left: 10px;
+}
+
+.org-info-content {
+ padding: 0rem 1rem;
+ background-color: #f5f5f5;
+ flex: 1 1;
+ overflow: auto;
+}
+
+.org-info-content .info-header-content {
+ height: 106px;
+}
+
+.info-header-content .info-header-id,
+.info-header-content .info-header-name {
+ width: calc(50% - 5px);
+ background-color: #fff;
+ border-radius: 5px;
+ border: 1px solid #e4e4e4;
+}
+
+.info-header-content .info-header-id img,
+.info-header-content .info-header-name img {
+ width: 46px;
+ height: 46px;
+ margin: 30px 0 0 30px;
+}
+
+.info-header-content .id-content,
+.info-header-content .name-content {
+ width: 70%;
+ height: 50%;
+ margin: 28px 0 0 15px;
+}
+
+.info-header-content .id-content p,
+.info-header-content .name-content p {
+ font-size: 16px;
+ font-weight: 500;
+}
+
+.info-header-content .id-content span,
+.info-header-content .name-content span {
+ font-size: 14px;
+}
+
+.org-info-content .info-user-content {
+ height: 180px;
+ background: #fff;
+ border-radius: 5px;
+ border: 1px solid #e4e4e4;
+}
+
+.info-user-content .user-content-detail {
+ flex-direction: column;
+ width: calc(100% / 3);
+}
+
+.info-user-content .user-content-detail p:first-child {
+ font-size: 16px;
+ font-weight: 500;
+ margin-top: 45px;
+}
+
+.info-user-content .user-content-detail p:last-child {
+ font-size: 36px;
+ font-weight: 500;
+}
+
+.org-info-content .used-storage-content {
+ height: 180px;
+}
+
+.used-storage-content .used-space {
+ width: calc(50% - 5px);
+ flex-direction: column;
+ background-color: #fff;
+ border-radius: 5px;
+ border: 1px solid #e4e4e4;
+}
+
+.used-storage-content .used-space p:first-child {
+ font-size: 16px;
+ font-weight: 500;
+ margin: 30px 0 0 30px;
+}
+
+.used-storage-content .used-space p:nth-child(2) {
+ font-size: 30px;
+ font-weight: 500;
+ margin: 5px 0 0 30px;
+}
+
+.used-storage-content .used-space span {
+ font-size: 13px;
+ font-weight: 400;
+ color: #aaa;
+ margin-left: 30px;
+}
+
+/* mobile */
+@media (max-width: 767px) {
+ .org-info-content .info-header-content {
+ width: 100%;
+ height: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ }
+
+ .info-header-content .info-header-name,
+ .info-header-content .info-header-id {
+ width: 100%;
+ height: 100%;
+ display: inline-flex;
+ background-color: #fff;
+ border-radius: 5px;
+ }
+
+ .info-header-content .info-header-id {
+ margin-top: 10px;
+ }
+
+ .info-header-content .info-header-id img,
+ .info-header-content .info-header-name img {
+ margin: 30px 0 30px 30px;
+ }
+
+ .org-info-content .used-storage-content {
+ height: auto;
+ flex-direction: column;
+ }
+
+ .used-storage-content .used-space {
+ width: 100%;
+ }
+
+ .used-storage-content .used-space span {
+ margin-bottom: 30px;
+ }
+}
+
+/* Process bar style */
+.org-info-content .am-progress-outer {
+ height: 6px !important;
+ background: #eee;
+ margin-left: 30px;
+ width: 86%;
+ border-radius: 10px;
+ margin-top: 1.25rem;
+}
+
+.org-info-content .am-progress-bar {
+ height: 6px !important;
+ border-radius: 5px;
+ background-color: #ED7109;
+}
+
+.dtable-icon-use-help {
+ color: #bdbdbd;
+}
+
+.dtable-icon-use-help:hover {
+ color: #888;
+}
diff --git a/frontend/src/css/system-departments-v2.css b/frontend/src/css/system-departments-v2.css
new file mode 100644
index 0000000000..b7992e04fe
--- /dev/null
+++ b/frontend/src/css/system-departments-v2.css
@@ -0,0 +1,131 @@
+.cur-view-content {
+ position: relative;
+}
+
+.departments-tree-panel {
+ width: 25%;
+ padding: 8px;
+ border-right: 1px solid #eee;
+ height: 100%;
+ overflow: auto;
+}
+
+.departments-tree-panel .departments-v2-hight-light {
+ color: #fff;
+ border-radius: 4px;
+ background-color: #ED7109 !important;
+}
+
+.departments-tree-panel .departments-v2-hight-light i {
+ color: #fff;
+}
+
+.top-department-button-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.departments-v2-tree-item {
+ position: relative;
+ display: flex;
+ height: 24px;
+}
+
+.departments-v2-tree-item:hover {
+ background-color: #ffefb2;
+ border-radius: 0.25rem;
+ cursor: pointer;
+}
+
+.departments-v2-tree-item .departments-v2-tree-icon {
+ width: 24px;
+ text-align: center;
+ color: #b0b0b0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.departments-v2-tree-item .departments-v2-tree-node-text {
+ flex: 1;
+ padding-right: 4px;
+ line-height: 24px;
+ font-size: 14px;
+}
+
+.departments-v2-tree-item .department-dropdown-menu {
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #b0b0b0;
+}
+
+.departments-v2-tree-item .department-action-icon {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #666666;
+}
+
+.departments-v2-tree-item .department-action-icon:hover {
+ color: #212529;
+}
+
+.departments-tree-panel .department-children {
+ padding-left: 1rem;
+ position: relative;
+}
+
+.department-content-main {
+ flex: 1;
+ height: 100%;
+ overflow-y: hidden;
+}
+
+.department-content-main:hover {
+ overflow-y: auto;
+}
+
+.department-content-main table td {
+ line-height: 2rem;
+}
+
+.department-content-main .department-content-main-name {
+ height: 40px;
+ padding: 0 16px;
+ background: #f9f9f9;
+ font-size: 1rem;
+ line-height: 40px;
+ margin: 0;
+ border-bottom: 1px solid #eee;
+}
+
+.department-content-main .create-group-info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 200px;
+}
+
+.department-content-main .cur-view-content .table {
+ margin-bottom: 6rem;
+}
+
+.department-content-main .cur-view-content .sort-dirent {
+ transform: scale(0.8);
+ font-size: 12px;
+ cursor: pointer;
+ display: inline-block;
+}
+
+.department-content-main table .dtable-icon-check-mark {
+ color: #21bc2e;
+}
+
+/* 顶部的样式:背景色和边界去掉,和 seatable 保持一致 */
diff --git a/frontend/src/pages/org-admin/index.js b/frontend/src/pages/org-admin/index.js
index bfa78e2560..c85d4b3172 100644
--- a/frontend/src/pages/org-admin/index.js
+++ b/frontend/src/pages/org-admin/index.js
@@ -44,6 +44,7 @@ import OrgSubscription from './org-subscription';
import '../../css/layout.css';
import '../../css/toolbar.css';
+import '../../css/admin-common.css';
class Org extends React.Component {
constructor(props) {
@@ -94,7 +95,7 @@ class Org extends React.Component {
render() {
let { isSidePanelClosed, currentTab } = this.state;
return (
-
+
diff --git a/frontend/src/pages/sys-admin/departments-v2/department-node.js b/frontend/src/pages/sys-admin/departments-v2/department-node.js
new file mode 100644
index 0000000000..47175b51cc
--- /dev/null
+++ b/frontend/src/pages/sys-admin/departments-v2/department-node.js
@@ -0,0 +1,38 @@
+class DepartmentNode {
+
+ constructor(props) {
+ this.id = props.id || '';
+ this.name = props.name || '';
+ this.children = props.children || [];
+ this.parentNode = props.parentNode || null;
+ this.orgId = props.orgId || '';
+ }
+
+ findNodeById(nodeId) {
+ if (this.id === nodeId) return this;
+ const s = [...this.children];
+ while (s.length > 0) {
+ const node = s.shift();
+ if (node.id === nodeId) return node;
+ s.push(...node.children);
+ }
+ }
+
+ addChildren(nodes) {
+ this.children.push(...nodes);
+ }
+
+ setChildren(nodes) {
+ this.children = nodes;
+ }
+
+ hasChildren() {
+ return this.children.length > 0;
+ }
+
+ deleteChildById(nodeId) {
+ this.children = this.children.filter(nodeItem => nodeItem.id !== nodeId);
+ }
+}
+
+export default DepartmentNode;
diff --git a/frontend/src/pages/sys-admin/departments-v2/departments-v2-members-item.js b/frontend/src/pages/sys-admin/departments-v2/departments-v2-members-item.js
new file mode 100644
index 0000000000..26ad635575
--- /dev/null
+++ b/frontend/src/pages/sys-admin/departments-v2/departments-v2-members-item.js
@@ -0,0 +1,124 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@gatsbyjs/reach-router';
+import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
+import RoleSelector from '../../../components/single-selector';
+import { gettext, siteRoot } from '../../../utils/constants';
+import { getRoleOptions } from './role-status-utils';
+
+const propTypes = {
+ isItemFreezed: PropTypes.bool,
+ member: PropTypes.object,
+ setMemberStaff: PropTypes.func,
+ deleteMember: PropTypes.func,
+ unfreezeItem: PropTypes.func,
+ freezeItem: PropTypes.func,
+ toggleItemFreezed: PropTypes.func,
+};
+
+class DepartmentsV2MembersItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ dropdownOpenEmail: '',
+ isShowDropdownMenu: false,
+ isItemMenuShow: false,
+ };
+ this.roles = ['Admin', 'Member'];
+ }
+
+ onMouseEnter = () => {
+ if (!this.props.isItemFreezed) {
+ this.setState({ isShowDropdownMenu: true });
+ }
+ };
+
+ onMouseLeave = () => {
+ if (!this.props.isItemFreezed) {
+ this.setState({ isShowDropdownMenu: false });
+ }
+ };
+
+ setMemberStaff = (role) => {
+ this.props.setMemberStaff(this.props.member.email, role.value === 'Admin');
+ };
+
+ deleteMember = (e) => {
+ e.stopPropagation();
+ const { member } = this.props;
+ this.props.deleteMember(member.email);
+ };
+
+ toggleDropdownMenu = () => {
+ this.setState({
+ isItemMenuShow: !this.state.isItemMenuShow
+ }, () => {
+ if (this.state.isItemMenuShow && typeof(this.props.freezeItem) === 'function') {
+ this.props.freezeItem();
+ } else if (!this.state.isItemMenuShow && typeof(this.props.unfreezeItem) === 'function') {
+ this.props.unfreezeItem();
+ }
+ });
+ };
+
+ translateRole = (role) => {
+ if (role === 'Admin') {
+ return gettext('Admin');
+ } else if (role === 'Member') {
+ return gettext('Default member');
+ }
+ };
+
+ render() {
+ const { member, freezeItem, unfreezeItem } = this.props;
+ const { isShowDropdownMenu, isItemMenuShow } = this.state;
+ const currentRole = member.role;
+ const options = getRoleOptions(this.roles) || [];
+ const option = options.find(item => item.value === currentRole) || {};
+
+ return (
+
+  |
+
+ {member.name}
+ |
+
+ { freeze ? freezeItem() : unfreezeItem(); }}
+ />
+ |
+ {member.contact_email} |
+
+ {isShowDropdownMenu &&
+
+
+
+ {gettext('Delete')}
+
+
+ }
+ |
+
+ );
+ }
+}
+
+DepartmentsV2MembersItem.propTypes = propTypes;
+
+export default DepartmentsV2MembersItem;
diff --git a/frontend/src/pages/sys-admin/departments-v2/departments-v2-members-list.js b/frontend/src/pages/sys-admin/departments-v2/departments-v2-members-list.js
new file mode 100644
index 0000000000..42331187ad
--- /dev/null
+++ b/frontend/src/pages/sys-admin/departments-v2/departments-v2-members-list.js
@@ -0,0 +1,224 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Table } from 'reactstrap';
+import Loading from '../../../components/loading';
+import EmptyTip from '../../../components/empty-tip';
+import { gettext } from '../../../utils/constants';
+import DepartmentsV2MembersItem from './departments-v2-members-item';
+import RepoItem from '../departments/repo-item';
+import ModalPortal from '../../../components/modal-portal';
+import DeleteRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-repo-dialog';
+
+const propTypes = {
+ rootNodes: PropTypes.array,
+ checkedDepartmentId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ membersList: PropTypes.array,
+ isMembersListLoading: PropTypes.bool,
+ setMemberStaff: PropTypes.func,
+ sortItems: PropTypes.func,
+ sortOrder: PropTypes.string,
+ sortBy: PropTypes.string,
+ deleteMember: PropTypes.func,
+ getRepos: PropTypes.func,
+};
+
+class DepartmentsV2MembersList extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isItemFreezed: false,
+ activeNav: 'members',
+ repos: [],
+ deletedRepo: {},
+ showDeleteRepoDialog: false,
+ };
+ }
+
+ componentDidMount() {
+ this.getRepos(this.props.checkedDepartmentId);
+ }
+
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (this.props.checkedDepartmentId !== nextProps.checkedDepartmentId || this.props.isAddNewRepo !== nextProps.isAddNewRepo) {
+ this.getRepos(nextProps.checkedDepartmentId);
+ }
+ }
+
+ showDeleteRepoDialog = (repo) => {
+ this.setState({
+ showDeleteRepoDialog: true,
+ deletedRepo: repo,
+ });
+ };
+
+ toggleCancel = () => {
+ this.setState({
+ showDeleteRepoDialog: false,
+ deletedRepo: {},
+ });
+ };
+
+ onRepoChanged = () => {
+ this.getRepos(this.props.checkedDepartmentId);
+ };
+
+ freezeItem = () => {
+ this.setState({ isItemFreezed: true });
+ };
+
+ unfreezeItem = () => {
+ this.setState({ isItemFreezed: false });
+ };
+
+ toggleItemFreezed = () => {
+ this.setState({ isItemFreezed: !this.state.isItemFreezed });
+ };
+
+ getDepartmentName = () => {
+ const { rootNodes, checkedDepartmentId } = this.props;
+ if (!rootNodes) return '';
+ let name = '';
+ let arr = [...rootNodes];
+ while (!name && arr.length > 0) {
+ let curr = arr.shift();
+ if (curr.id === checkedDepartmentId) {
+ name = curr.name;
+ } else if (curr.children && curr.children.length > 0) {
+ arr.push(...curr.children);
+ }
+ }
+ return name;
+ };
+
+ sortByName = (e) => {
+ e.preventDefault();
+ const sortBy = 'name';
+ let { sortOrder } = this.props;
+ sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
+ this.props.sortItems(sortBy, sortOrder);
+ };
+
+ sortByRole = (e) => {
+ e.preventDefault();
+ const sortBy = 'role';
+ let { sortOrder } = this.props;
+ sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
+ this.props.sortItems(sortBy, sortOrder);
+ };
+
+ changeActiveNav = (activeNav) => {
+ this.setState({ activeNav });
+ };
+
+ getRepos = (id) => {
+ this.props.getRepos(id, (repos) => {
+ this.setState({ repos });
+ });
+ };
+
+ render() {
+ const { activeNav, repos } = this.state;
+ const { membersList, isMembersListLoading, sortBy, sortOrder } = this.props;
+ const sortByName = sortBy === 'name';
+ const sortByRole = sortBy === 'role';
+ const sortIcon = ;
+
+ return (
+
+
{this.getDepartmentName()}
+
+
+
+ -
+ this.changeActiveNav('members')}>{gettext('Members')}
+
+ -
+ this.changeActiveNav('repos')}>{gettext('Libraries')}
+
+
+
+
+ {activeNav === 'members' &&
+ <>
+ {isMembersListLoading &&
}
+ {!isMembersListLoading && membersList.length > 0 &&
+
+
+
+
+
+ |
+ {gettext('Name')}{' '}{sortByName && sortIcon} |
+ {gettext('Role')}{' '}{sortByRole && sortIcon} |
+ {gettext('Contact email')} |
+ {/* Operations */} |
+
+
+
+ {membersList.map((item, index) => {
+ return (
+
+ );
+ })}
+
+
+
+ }
+ {!isMembersListLoading && membersList.length === 0 &&
+
+ }
+ >
+ }
+
+ {(activeNav === 'repos' && repos.length > 0) &&
+
+
+
+
+ |
+ {gettext('Name')} |
+ {gettext('Size')} |
+ |
+
+
+
+ {repos.map((repo, index) => {
+ return (
+
+ );
+ })}
+
+
+
+ }
+ {(activeNav === 'repos' && repos.length === 0) &&
+
+ }
+ {this.state.showDeleteRepoDialog && (
+
+
+
+ )}
+
+ );
+ }
+}
+
+DepartmentsV2MembersList.propTypes = propTypes;
+
+export default DepartmentsV2MembersList;
diff --git a/frontend/src/pages/sys-admin/departments-v2/departments-v2-tree-node.js b/frontend/src/pages/sys-admin/departments-v2/departments-v2-tree-node.js
new file mode 100644
index 0000000000..62144758a7
--- /dev/null
+++ b/frontend/src/pages/sys-admin/departments-v2/departments-v2-tree-node.js
@@ -0,0 +1,226 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
+import { gettext } from '../../../utils/constants';
+
+const departmentsV2TreeNodePropTypes = {
+ node: PropTypes.object,
+ checkedDepartmentId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ listSubDepartments: PropTypes.func,
+ onChangeDepartment: PropTypes.func,
+ toggleAddDepartment: PropTypes.func,
+ toggleAddLibrary: PropTypes.func,
+ toggleAddMembers: PropTypes.func,
+ toggleRename: PropTypes.func,
+ toggleDelete: PropTypes.func
+};
+
+class DepartmentsV2TreeNode extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isShowTreeIcon: true,
+ isChildrenShow: false,
+ dropdownOpen: false,
+ active: false
+ };
+ }
+
+ componentDidMount() {
+ const { node } = this.props;
+ if (node.id === -1) {
+ this.listSubDepartments();
+ }
+ }
+
+ toggleChildren = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this.state.isChildrenShow) {
+ this.setState({ isChildrenShow: false });
+ return;
+ }
+ this.listSubDepartments();
+ };
+
+ listSubDepartments = () => {
+ const { node } = this.props;
+ this.props.listSubDepartments(node.id, (childrenNodes) => {
+ if (Array.isArray(childrenNodes) && childrenNodes.length === 0) {
+ this.setState({ isShowTreeIcon: false });
+ }
+ this.setState({ isChildrenShow: true });
+ });
+ };
+
+ 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 });
+ };
+
+ renderTreeNodes = (nodes) => {
+ if (nodes.length > 0) {
+ return nodes.map((node) => {
+ return (
+
+ );
+ });
+ }
+ };
+
+ changeDept = (nodeId) => {
+ const { node, checkedDepartmentId } = this.props;
+ const { isChildrenShow } = this.state;
+ if (checkedDepartmentId !== node.id) {
+ this.props.onChangeDepartment(nodeId);
+ }
+ if (checkedDepartmentId === node.id) {
+ if (isChildrenShow) {
+ this.setState({ isChildrenShow: false });
+ return;
+ }
+ this.listSubDepartments();
+ }
+ };
+
+ toggleAddDepartment = (node) => {
+ this.props.toggleAddDepartment(node);
+ };
+
+ toggleAddMembers = (node) => {
+ this.props.toggleAddMembers(node);
+ };
+
+ toggleRename = (node) => {
+ this.props.toggleRename(node);
+ };
+
+ toggleDelete = (node) => {
+ this.props.toggleDelete(node);
+ };
+
+ render() {
+ const { node, checkedDepartmentId } = this.props;
+ const { isChildrenShow, dropdownOpen, active } = this.state;
+ let nodeInnerClass = classNames({
+ 'departments-v2-tree-item': true,
+ 'departments-v2-hight-light': checkedDepartmentId === node.id
+ });
+ return (
+
+ this.changeDept(node.id)}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}
+ >
+ {this.state.isShowTreeIcon ?
+ this.toggleChildren(e)}>
+
+
+ :
+
+ }
+ {node.name}
+ {active && node.id !== 'other_users' &&
+ this.dropdownToggle(e)}
+ direction="down"
+ className="department-dropdown-menu"
+ >
+
+
+
+
+
+ {gettext('Add sub-department')}
+
+
+ {gettext('Add Library')}
+
+ {node.id !== -1 && (
+
+
+ {gettext('Add members')}
+
+
+ {gettext('Rename')}
+
+
+ {gettext('Delete')}
+
+
+ {`${gettext('Department ID')} : ${node.id}`}
+
+
+ )}
+
+
+ }
+
+ {this.state.isChildrenShow &&
+
+ {node.children && this.renderTreeNodes(node.children)}
+
+ }
+
+ );
+ }
+}
+
+DepartmentsV2TreeNode.propTypes = departmentsV2TreeNodePropTypes;
+
+export default DepartmentsV2TreeNode;
diff --git a/frontend/src/pages/sys-admin/departments-v2/departments-v2-tree-panel.js b/frontend/src/pages/sys-admin/departments-v2/departments-v2-tree-panel.js
new file mode 100644
index 0000000000..b20d6dda39
--- /dev/null
+++ b/frontend/src/pages/sys-admin/departments-v2/departments-v2-tree-panel.js
@@ -0,0 +1,45 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import DepartmentsV2TreeNode from './departments-v2-tree-node';
+
+const DepartmentV2TreePanelPropTypes = {
+ rootNodes: PropTypes.array,
+ checkedDepartmentId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ onChangeDepartment: PropTypes.func,
+ listSubDepartments: PropTypes.func,
+ toggleAddDepartment: PropTypes.func,
+ toggleAddLibrary: PropTypes.func,
+ toggleAddMembers: PropTypes.func,
+ toggleRename: PropTypes.func,
+ toggleDelete: PropTypes.func
+};
+
+class DepartmentV2TreePanel extends Component {
+ render() {
+ const { rootNodes, checkedDepartmentId } = this.props;
+ return (
+
+ {rootNodes.map(rootNode => {
+ return (
+
+ );
+ })}
+
+ );
+ }
+}
+
+DepartmentV2TreePanel.propTypes = DepartmentV2TreePanelPropTypes;
+
+export default DepartmentV2TreePanel;
diff --git a/frontend/src/pages/sys-admin/departments-v2/departments-v2.js b/frontend/src/pages/sys-admin/departments-v2/departments-v2.js
new file mode 100644
index 0000000000..9355dc4b59
--- /dev/null
+++ b/frontend/src/pages/sys-admin/departments-v2/departments-v2.js
@@ -0,0 +1,394 @@
+import React, { Fragment } from 'react';
+import { Button } from 'reactstrap';
+import toaster from '../../../components/toast';
+import { gettext } from '../../../utils/constants';
+import Account from '../../../components/common/account';
+import DepartmentNode from './department-node';
+import DepartmentV2TreePanel from './departments-v2-tree-panel';
+import { systemAdminAPI } from '../../../utils/system-admin-api';
+import DepartmentsV2MembersList from './departments-v2-members-list';
+import AddDepartmentV2Dialog from '../../../components/dialog/sysadmin-dialog/add-department-v2-dialog';
+import AddDepartMemberV2Dialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-depart-member-v2-dialog';
+import RenameDepartmentV2Dialog from '../../../components/dialog/sysadmin-dialog/rename-department-v2-dialog';
+import DeleteDepartmentV2ConfirmDialog from '../../../components/dialog/sysadmin-dialog/delete-department-v2-confirm-dialog';
+import AddRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-repo-dialog';
+import Loading from '../../../components/loading';
+import { Utils } from '../../../utils/utils';
+
+import '../../../css/system-departments-v2.css';
+
+class DepartmentsV2 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ const localSortItems = localStorage.getItem('departments-members-sort-items') || {};
+ this.state = {
+ rootNodes: [],
+ checkedDepartmentId: -1,
+ operateNode: null,
+ isAddDepartmentDialogShow: false,
+ isAddMembersDialogShow: false,
+ isRenameDepartmentDialogShow: false,
+ isDeleteDepartmentDialogShow: false,
+ isShowAddRepoDialog: false,
+ membersList: [],
+ isTopDepartmentLoading: false,
+ isMembersListLoading: false,
+ sortBy: localSortItems.sort_by || 'name', // 'name' or 'role'
+ sortOrder: localSortItems.sort_order || 'asc', // 'asc' or 'desc',
+ };
+ }
+
+ componentDidMount() {
+ this.setState({ isTopDepartmentLoading: true }, () => {
+ systemAdminAPI.sysAdminListAllDepartments().then(res => {
+ const department_list = res.data.data;
+ if (department_list && department_list.length > 0) {
+ const rootNodes = department_list.map(item => {
+ const node = new DepartmentNode({
+ id: item.id,
+ name: item.name,
+ orgId: item.org_id,
+ });
+ return node;
+ });
+ this.setState({
+ rootNodes,
+ checkedDepartmentId: rootNodes[0].id,
+ });
+ this.loadDepartmentMembers(rootNodes[0].id);
+ }
+ this.setState({ isTopDepartmentLoading: false });
+ });
+ });
+ }
+
+ onChangeDepartment = (nodeId) => {
+ this.setState({ checkedDepartmentId: nodeId }, this.loadDepartmentMembers(nodeId));
+ };
+
+ loadDepartmentMembers = (nodeId) => {
+ if (nodeId === -1) {
+ this.setState({
+ isMembersListLoading: false,
+ membersList: [],
+ });
+ return;
+ }
+ this.setState({ isMembersListLoading: true });
+ systemAdminAPI.sysAdminListGroupMembers(nodeId, 0, 100).then(res => {
+ this.setState({
+ membersList: res.data.members,
+ isMembersListLoading: false
+ });
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ listSubDepartments = (nodeId, cb) => {
+ const { rootNodes } = this.state;
+ let node = null;
+ rootNodes.forEach(rootNode => {
+ if (!node) {
+ node = rootNode.findNodeById(nodeId);
+ }
+ });
+ if (!node) return;
+ systemAdminAPI.sysAdminGetDepartmentInfo(nodeId).then(res => {
+ const childrenNodes = res.data.groups.map(department => new DepartmentNode({
+ id: department.id,
+ name: department.name,
+ parentGroupId: department.parent_group_id,
+ orgId: department.org_id,
+ parentNode: node,
+ }));
+ node.setChildren(childrenNodes);
+ cb && cb(childrenNodes);
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ getRepos = (nodeId, cb) => {
+ systemAdminAPI.sysAdminListGroupRepos(nodeId).then(res => {
+ cb && cb(res.data.libraries);
+ }).catch(error => {
+ if (error.response && error.response.status === 404) {
+ cb && cb(null);
+ return;
+ }
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ toggleAddLibrary = (node) => {
+ this.setState({
+ operateNode: node,
+ isShowAddRepoDialog: !this.state.isShowAddRepoDialog
+ });
+ };
+
+ toggleAddDepartment = (node) => {
+ this.setState({ operateNode: node, isAddDepartmentDialogShow: !this.state.isAddDepartmentDialogShow });
+ };
+
+ toggleAddMembers = (node) => {
+ this.setState({ operateNode: node, isAddMembersDialogShow: !this.state.isAddMembersDialogShow });
+ };
+
+ toggleRename = (node) => {
+ this.setState({ operateNode: node, isRenameDepartmentDialogShow: !this.state.isRenameDepartmentDialogShow });
+ };
+
+ toggleDelete = (node) => {
+ this.setState({ operateNode: node, isDeleteDepartmentDialogShow: !this.state.isDeleteDepartmentDialogShow });
+ };
+
+ addDepartment = (parentNode, department) => {
+ parentNode.addChildren([new DepartmentNode({
+ id: department.id,
+ name: department.name,
+ parentNode: parentNode,
+ orgId: department.org_id,
+ })]);
+ };
+
+ setRootNode = (department) => {
+ const newRootNode = new DepartmentNode({
+ id: department.id,
+ name: department.name,
+ orgId: department.org_id,
+ });
+ const rootNodes = this.state.rootNodes.slice(0);
+ rootNodes.push(newRootNode);
+ this.setState({
+ rootNodes: rootNodes,
+ checkedDepartmentId: newRootNode.id,
+ });
+ this.loadDepartmentMembers(newRootNode.id);
+ };
+
+ renameDepartment = (node, department) => {
+ node.id = department.id;
+ node.name = department.name;
+ };
+
+ onDelete = () => {
+ const { operateNode, checkedDepartmentId } = this.state;
+ systemAdminAPI.sysAdminDeleteDepartment(operateNode.id).then(() => {
+ this.toggleDelete();
+ if (operateNode.parentNode) {
+ operateNode.parentNode.deleteChildById(operateNode.id);
+ if (operateNode.id === checkedDepartmentId && operateNode.parentNode.id !== -1) {
+ this.onChangeDepartment(operateNode.parentNode.id);
+ }
+ } else {
+ let rootNodes = this.state.rootNodes.slice(0);
+ let rootIndex = rootNodes.findIndex(node => node.id === operateNode.id);
+ rootNodes.splice(rootIndex, 1);
+ this.setState({
+ rootNodes,
+ checkedDepartmentId: rootNodes[0].id,
+ });
+ this.loadDepartmentMembers(rootNodes[0].id);
+ }
+ }).catch(error => {
+ if (error.response && error.response.status === 400) {
+ toaster.danger(error.response.data.error_msg);
+ return;
+ }
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ onMemberChanged = () => {
+ const { checkedDepartmentId, operateNode } = this.state;
+ if (checkedDepartmentId && operateNode && checkedDepartmentId !== operateNode.id) return;
+ this.loadDepartmentMembers(operateNode.id);
+ };
+
+ setMemberStaff = (email, isAdmin) => {
+ const { checkedDepartmentId, membersList } = this.state;
+ systemAdminAPI.sysAdminUpdateGroupMemberRole(checkedDepartmentId, email, isAdmin).then(res => {
+ const member = res.data;
+ const newMembersList = membersList.map(memberItem => {
+ if (memberItem.email === email) return member;
+ return memberItem;
+ });
+ this.setState({ membersList: newMembersList });
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ deleteMember = (email) => {
+ const { checkedDepartmentId, membersList } = this.state;
+ systemAdminAPI.sysAdminDeleteGroupMember(checkedDepartmentId, email).then(() => {
+ const newMembersList = membersList.filter(memberItem => memberItem.email !== email);
+ this.setState({ membersList: newMembersList });
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ sortMembers = (items, sortBy, sortOrder) => {
+ let comparator;
+ switch (`${sortBy}-${sortOrder}`) {
+ case 'name-asc':
+ comparator = function (a, b) {
+ var result = Utils.compareTwoWord(a.name, b.name);
+ return result;
+ };
+ break;
+ case 'name-desc':
+ comparator = function (a, b) {
+ var result = Utils.compareTwoWord(a.name, b.name);
+ return -result;
+ };
+ break;
+ case 'role-asc':
+ comparator = function (a, b) {
+ return a.is_staff && !b.is_staff ? -1 : 1;
+ };
+ break;
+ case 'role-desc':
+ comparator = function (a, b) {
+ return a.is_staff && !b.is_staff ? 1 : -1;
+ };
+ break;
+ default:
+ comparator = function () {
+ return true;
+ };
+ }
+ items.sort((a, b) => {
+ return comparator(a, b);
+ });
+ return items;
+ };
+
+ sortItems = (sortBy, sortOrder) => {
+ localStorage.setItem('departments-members-sort-items', { sort_by: sortBy, sort_order: sortOrder });
+ this.setState({
+ sortBy: sortBy,
+ sortOrder: sortOrder,
+ membersList: this.sortMembers(this.state.membersList, sortBy, sortOrder),
+ });
+ };
+
+
+ render() {
+ const { rootNodes, operateNode, checkedDepartmentId, isAddDepartmentDialogShow, isAddMembersDialogShow,
+ membersList, isMembersListLoading, isTopDepartmentLoading, isRenameDepartmentDialogShow,
+ isDeleteDepartmentDialogShow, sortBy, sortOrder } = this.state;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{gettext('Departments')}
+
+ {isTopDepartmentLoading &&
}
+ {(!isTopDepartmentLoading && rootNodes.length > 0) &&
+ <>
+
+
+ >
+ }
+ {(!isTopDepartmentLoading && rootNodes.length === 0) &&
+
+
+
+ }
+
+
+
+ {isAddMembersDialogShow &&
+
+ }
+ {isRenameDepartmentDialogShow &&
+
+ }
+ {isDeleteDepartmentDialogShow &&
+
+ }
+ {isAddDepartmentDialogShow &&
+
+ }
+ {this.state.isShowAddRepoDialog && (
+ {this.setState({ isAddNewRepo: !this.state.isAddNewRepo });}}
+ groupID={String(operateNode.id)}
+ />
+ )}
+
+ );
+ }
+
+}
+
+export default DepartmentsV2;
diff --git a/frontend/src/pages/sys-admin/departments-v2/role-status-utils.js b/frontend/src/pages/sys-admin/departments-v2/role-status-utils.js
new file mode 100644
index 0000000000..f3d5112f73
--- /dev/null
+++ b/frontend/src/pages/sys-admin/departments-v2/role-status-utils.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import { gettext } from '../../../utils/constants';
+
+const getRoleOptions = (roles) => {
+ return Array.isArray(roles) && roles.map(role => ({
+ value: role,
+ text: translateRole(role),
+ label: (
+
+ {translateRole(role)}
+
+ ),
+ }));
+};
+
+const translateRole = (role) => {
+ switch (role) {
+ case 'Admin':
+ return gettext('Admin');
+ case 'Member':
+ return gettext('Member');
+ case 'default':
+ return gettext('Default');
+ case 'guest':
+ return gettext('Guest');
+ case 'default_admin':
+ return gettext('Default admin');
+ case 'system_admin':
+ return gettext('System admin');
+ case 'daily_admin':
+ return gettext('Daily admin');
+ case 'audit_admin':
+ return gettext('Audit admin');
+ default:
+ return role;
+ }
+};
+
+export { getRoleOptions };
diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js
index 963ee69074..9522038352 100644
--- a/frontend/src/pages/sys-admin/index.js
+++ b/frontend/src/pages/sys-admin/index.js
@@ -45,11 +45,7 @@ import SearchGroups from './groups/search-groups';
import GroupRepos from './groups/group-repos';
import GroupMembers from './groups/group-members';
-import Departments from './departments/departments';
-import DepartmentList from './departments/department-list';
-import SubDepartments from './departments/sub-departments';
-import DepartmentMembers from './departments/department-members';
-import DepartmentLibraries from './departments/department-libraries';
+import DepartmentsV2 from './departments-v2/departments-v2';
import ShareLinks from './links/share-links';
import UploadLinks from './links/upload-links';
@@ -89,6 +85,7 @@ import AbuseReports from './abuse-reports';
import '../../css/layout.css';
import '../../css/toolbar.css';
+import '../../css/admin-common.css';
class SysAdmin extends React.Component {
constructor(props) {
@@ -209,7 +206,7 @@ class SysAdmin extends React.Component {
};
return (
-
+
-
-
-
-
-
-
+
diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css
index 0d4244a025..e3c726fe9b 100644
--- a/media/css/seahub_react.css
+++ b/media/css/seahub_react.css
@@ -1092,6 +1092,7 @@ a.table-sort-op:hover {
.dropdown-item {
height: 32px;
padding: 0.25rem 1rem;
+ line-height: 1.5;
cursor: pointer;
color: #212529;
font-size: 14px;
@@ -1490,15 +1491,15 @@ a.table-sort-op:hover {
}
.rotate-90 {
- transform: rotate(90deg);
+ transform: rotate(90deg) !important;
}
.rotate-180 {
- transform: rotate(180deg);
+ transform: rotate(180deg) !important;
}
.rotate-270 {
- transform: rotate(270deg);
+ transform: rotate(270deg) !important;
}
/* All triangle icon use 12px */