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 && +
+ + + + + + + + + + + + + {membersList.map((item, index) => { + return ( + + ); + })} + +
{gettext('Name')}{' '}{sortByName && sortIcon}{gettext('Role')}{' '}{sortByRole && sortIcon}{gettext('Contact email')}{/* Operations */}
+
+ } + {!isMembersListLoading && membersList.length === 0 && + + } + + } + + {(activeNav === 'repos' && repos.length > 0) && +
+ + + + + + + + + + + {repos.map((repo, index) => { + return ( + + ); + })} + +
{gettext('Name')}{gettext('Size')}
+
+ } + {(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 */