1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-06 17:33:18 +00:00

[system admin - departments] fixup & improvements (#7350)

- improved the UX for 'member item'
    - added paginator for the member list
    - fixed sorting members by 'role'
    - fixed member name link
    - added confirm dialog for 'delete member'
    - fixed displaying error msgs for adding multiple members
    - removed 'store the sort way in localStorage'
    - removed 'v2' from code & file names
    - cleaned up files. removed files for the previous version, and etc.
    - fixed 'delete repo'
    - improved the scroll of the main content area (make the department
      name bar & the tabs bar fixed)
    - fixup for mobile
This commit is contained in:
llj
2025-01-10 22:03:31 +08:00
committed by GitHub
parent e22eb47f2d
commit 55afd3689c
23 changed files with 780 additions and 1833 deletions

View File

@@ -47,7 +47,7 @@ class DeleteRepoDialog extends React.Component {
const propTypes = {
repo: PropTypes.object.isRequired,
toggle: PropTypes.func.isRequired,
groupID: PropTypes.string,
groupID: PropTypes.number,
onRepoChanged: PropTypes.func.isRequired
};

View File

@@ -100,9 +100,8 @@ class Paginator extends Component {
<span className="sf3-font sf3-font-down rotate-270 d-inline-block"></span>
</button>
<Dropdown isOpen={this.state.isMenuShow} toggle={this.toggleOperationMenu} direction="up" className="paginator-dropdown">
<Dropdown isOpen={this.state.isMenuShow} toggle={this.toggleOperationMenu} direction="up" className="paginator-dropdown ml-6">
<DropdownToggle
className="ml-6"
data-toggle="dropdown"
aria-expanded={this.state.isMenuShow}
onClick={this.toggleOperationMenu}

View File

@@ -84,11 +84,6 @@
.department-content-main {
flex: 1;
height: 100%;
overflow-y: hidden;
}
.department-content-main:hover {
overflow-y: auto;
}
.department-content-main table td {
@@ -113,10 +108,6 @@
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;

View File

@@ -51,10 +51,6 @@ class Department extends React.Component {
this.setState({ isItemFreezed: false });
};
toggleItemFreezed = () => {
this.setState({ isItemFreezed: !this.state.isItemFreezed });
};
getCurrentDepartment = () => {
const { rootNodes, checkedDepartmentId } = this.props;
if (!rootNodes) return {};
@@ -116,7 +112,7 @@ class Department extends React.Component {
const currentDepartment = this.getCurrentDepartment();
return (
<div className="department-content-main">
<div className="department-content-main d-flex flex-column">
<div className="department-content-main-name">
{currentDepartment.name}
<Dropdown
@@ -180,7 +176,6 @@ class Department extends React.Component {
setMemberStaff={this.props.setMemberStaff}
unfreezeItem={this.unfreezeItem}
freezeItem={this.freezeItem}
toggleItemFreezed={this.toggleItemFreezed}
isItemFreezed={this.state.isItemFreezed}
/>
);

View File

@@ -13,7 +13,6 @@ const propTypes = {
deleteMember: PropTypes.func,
unfreezeItem: PropTypes.func,
freezeItem: PropTypes.func,
toggleItemFreezed: PropTypes.func,
};
class DepartmentsV2MembersItem extends React.Component {
@@ -87,7 +86,6 @@ class DepartmentsV2MembersItem extends React.Component {
});
const currentSelectedOption = this.roleOptions.filter(item => item.isSelected)[0];
return (
<>
<tr className={`departments-members-item ${highlighted ? 'tr-highlight' : ''}`} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>

View File

@@ -1,124 +0,0 @@
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 (
<tr className="departments-members-item" key={member.email} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td><img className="avatar" src={member.avatar_url} alt=""></img></td>
<td className='text-truncate'>
<Link to={`${siteRoot}sys/users/${encodeURIComponent(member.email)}/`}>{member.name}</Link>
</td>
<td>
<RoleSelector
isDropdownToggleShown={isShowDropdownMenu}
currentSelectedOption={option}
options={options}
selectOption={this.setMemberStaff}
toggleItemFreezed={(freeze) => { freeze ? freezeItem() : unfreezeItem(); }}
/>
</td>
<td>{member.contact_email}</td>
<td>
{isShowDropdownMenu &&
<Dropdown
isOpen={isItemMenuShow}
toggle={this.toggleDropdownMenu}
direction="down"
>
<DropdownToggle
tag='a'
role="button"
className='attr-action-icon sf3-font sf3-font-more-vertical'
title={gettext('More operations')}
aria-label={gettext('More operations')}
data-toggle="dropdown"
/>
<DropdownMenu right={true}>
<DropdownItem key='delete' onClick={this.deleteMember}>{gettext('Delete')}</DropdownItem>
</DropdownMenu>
</Dropdown>
}
</td>
</tr>
);
}
}
DepartmentsV2MembersItem.propTypes = propTypes;
export default DepartmentsV2MembersItem;

View File

@@ -1,257 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Table, Dropdown, DropdownToggle } 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';
import DepartmentNodeMenu from './departments-node-dropdown-menu';
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,
dropdownOpen: 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 });
};
getCurrentDepartment = () => {
const { rootNodes, checkedDepartmentId } = this.props;
if (!rootNodes) return {};
let currentDepartment = {};
let arr = [...rootNodes];
while (!currentDepartment.id && arr.length > 0) {
let curr = arr.shift();
if (curr.id === checkedDepartmentId) {
currentDepartment = curr;
} else if (curr.children && curr.children.length > 0) {
arr.push(...curr.children);
}
}
return currentDepartment;
};
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 });
});
};
dropdownToggle = (e) => {
e.stopPropagation();
this.setState({ dropdownOpen: !this.state.dropdownOpen });
};
render() {
const { activeNav, repos } = this.state;
const { membersList, isMembersListLoading, sortBy, sortOrder } = this.props;
const sortByName = sortBy === 'name';
const sortByRole = sortBy === 'role';
const sortIcon = <span className={`sort-dirent sf3-font sf3-font-down ${sortOrder === 'asc' ? 'rotate-180' : ''}`}></span>;
const currentDepartment = this.getCurrentDepartment();
return (
<div className="department-content-main">
<div className="department-content-main-name">
{currentDepartment.name}
<Dropdown
isOpen={this.state.dropdownOpen}
toggle={(e) => this.dropdownToggle(e)}
direction="down"
className="department-dropdown-menu"
>
<DropdownToggle
tag='span'
role="button"
className='sf3-font-down sf3-font ml-1 sf-dropdown-toggle'
title={gettext('More operations')}
aria-label={gettext('More operations')}
data-toggle="dropdown"
/>
<DepartmentNodeMenu
node={currentDepartment}
toggleAddDepartment={this.props.toggleAddDepartment}
toggleAddLibrary={this.props.toggleAddLibrary}
toggleAddMembers={this.props.toggleAddMembers}
toggleRename={this.props.toggleRename}
toggleDelete={this.props.toggleDelete}
/>
</Dropdown>
</div>
<div className="cur-view-path tab-nav-container">
<ul className="nav">
<li className="nav-item">
<span className={`nav-link ${activeNav === 'members' ? 'active' : ''}`} onClick={() => this.changeActiveNav('members')}>{gettext('Members')}</span>
</li>
<li className="nav-item">
<span className={`nav-link ${activeNav === 'repos' ? 'active' : ''}`} onClick={() => this.changeActiveNav('repos')}>{gettext('Libraries')}</span>
</li>
</ul>
</div>
{activeNav === 'members' &&
<>
{isMembersListLoading && <Loading />}
{!isMembersListLoading && membersList.length > 0 &&
<div className='cur-view-content'>
<Table hover>
<thead>
<tr>
<th width="60px"></th>
<th width="25%" onClick={this.sortByName}>{gettext('Name')}{' '}{sortByName && sortIcon}</th>
<th width="23%" onClick={this.sortByRole}>{gettext('Role')}{' '}{sortByRole && sortIcon}</th>
<th width="35%">{gettext('Contact email')}</th>
<th width="calc(17% - 60px)">{/* Operations */}</th>
</tr>
</thead>
<tbody>
{membersList.map((item, index) => {
return (
<DepartmentsV2MembersItem
key={index}
member={item}
deleteMember={this.props.deleteMember}
setMemberStaff={this.props.setMemberStaff}
unfreezeItem={this.unfreezeItem}
freezeItem={this.freezeItem}
toggleItemFreezed={this.toggleItemFreezed}
isItemFreezed={this.state.isItemFreezed}
/>
);
})}
</tbody>
</Table>
</div>
}
{!isMembersListLoading && membersList.length === 0 &&
<EmptyTip text={gettext('No members')} />
}
</>
}
{(activeNav === 'repos' && repos.length > 0) &&
<div className="cur-view-content">
<table>
<thead>
<tr>
<th width="5%"></th>
<th width="50%">{gettext('Name')}</th>
<th width="30%">{gettext('Size')}</th>
<th width="15%"></th>
</tr>
</thead>
<tbody>
{repos.map((repo, index) => {
return (
<RepoItem key={index} repo={repo} showDeleteRepoDialog={this.showDeleteRepoDialog} />
);
})}
</tbody>
</table>
</div>
}
{(activeNav === 'repos' && repos.length === 0) &&
<EmptyTip text={gettext('No libraries')} />
}
{this.state.showDeleteRepoDialog && (
<ModalPortal>
<DeleteRepoDialog
toggle={this.toggleCancel}
onRepoChanged={this.onRepoChanged}
repo={this.state.deletedRepo}
groupID={this.props.checkedDepartmentId}
/>
</ModalPortal>
)}
</div>
);
}
}
DepartmentsV2MembersList.propTypes = propTypes;
export default DepartmentsV2MembersList;

View File

@@ -1,406 +0,0 @@
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);
if (rootNodes.length === 0) {
this.setState({
rootNodes,
checkedDepartmentId: -1
});
} else {
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 (
<Fragment>
<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>
<div className="operation">
<button className='btn btn-secondary operation-item' title={gettext('New Department')} onClick={() => {this.toggleAddDepartment(null);}}>{gettext('New Department')}
</button>
</div>
</div>
<div className="common-toolbar">
<Account isAdminPanel={true}/>
</div>
</div>
<div className="main-panel-center">
<div className="cur-view-container">
<h2 className="heading">{gettext('Departments')}</h2>
<div className="cur-view-content d-flex flex-row p-0">
{isTopDepartmentLoading && <Loading/>}
{(!isTopDepartmentLoading && rootNodes.length > 0) &&
<>
<DepartmentV2TreePanel
rootNodes={rootNodes}
checkedDepartmentId={checkedDepartmentId}
onChangeDepartment={this.onChangeDepartment}
listSubDepartments={this.listSubDepartments}
toggleAddDepartment={this.toggleAddDepartment}
toggleAddLibrary={this.toggleAddLibrary}
toggleAddMembers={this.toggleAddMembers}
toggleRename={this.toggleRename}
toggleDelete={this.toggleDelete}
/>
<DepartmentsV2MembersList
rootNodes={rootNodes}
checkedDepartmentId={checkedDepartmentId}
membersList={membersList}
isMembersListLoading={isMembersListLoading}
isAddNewRepo={this.state.isAddNewRepo}
setMemberStaff={this.setMemberStaff}
deleteMember={this.deleteMember}
sortItems={this.sortItems}
sortBy={sortBy}
sortOrder={sortOrder}
getRepos={this.getRepos}
deleteGroup={this.deleteGroup}
createGroup={this.createGroup}
toggleAddDepartment={this.toggleAddDepartment}
toggleAddLibrary={this.toggleAddLibrary}
toggleAddMembers={this.toggleAddMembers}
toggleRename={this.toggleRename}
toggleDelete={this.toggleDelete}
/>
</>
}
{(!isTopDepartmentLoading && rootNodes.length === 0) &&
<div className="top-department-button-container h-100 w-100">
<Button onClick={this.toggleAddDepartment.bind(this, null)}>
{gettext('Enable departments feature')}
</Button>
</div>
}
</div>
</div>
</div>
{isAddMembersDialogShow &&
<AddDepartMemberV2Dialog
toggle={this.toggleAddMembers}
nodeId={operateNode.id}
onMemberChanged={this.onMemberChanged}
/>
}
{isRenameDepartmentDialogShow &&
<RenameDepartmentV2Dialog
node={operateNode}
toggle={this.toggleRename}
renameDepartment={this.renameDepartment}
/>
}
{isDeleteDepartmentDialogShow &&
<DeleteDepartmentV2ConfirmDialog
node={operateNode}
toggle={this.toggleDelete}
onDelete={this.onDelete}
/>
}
{isAddDepartmentDialogShow &&
<AddDepartmentV2Dialog
parentNode={operateNode}
toggle={this.toggleAddDepartment}
addDepartment={this.addDepartment}
setRootNode={this.setRootNode}
/>
}
{this.state.isShowAddRepoDialog && (
<AddRepoDialog
toggle={this.toggleAddLibrary}
onAddNewRepo={() => {this.setState({ isAddNewRepo: !this.state.isAddNewRepo });}}
groupID={String(operateNode.id)}
/>
)}
</Fragment>
);
}
}
export default DepartmentsV2;

View File

@@ -1,39 +0,0 @@
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: (
<div className="label-container">
<span>{translateRole(role)}</span>
</div>
),
}));
};
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 };

View File

@@ -1,119 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import { systemAdminAPI } from '../../../utils/system-admin-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import ModalPortal from '../../../components/modal-portal';
import DeleteRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-repo-dialog';
import { gettext, lang } from '../../../utils/constants';
import RepoItem from './repo-item';
import Department from './department';
import EmptyTip from '../../../components/empty-tip';
import '../../../css/org-department-item.css';
dayjs.locale(lang);
const DepartmentDetailPropTypes = {
groupID: PropTypes.string,
};
class DepartmentDetail extends React.Component {
constructor(props) {
super(props);
this.state = {
repos: [],
deletedRepo: {},
showDeleteRepoDialog: false
};
}
componentDidMount() {
const { groupID } = this.props;
this.listGroupRepo(groupID);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.groupID !== nextProps.groupID) {
this.listGroupRepo(nextProps.groupID);
}
}
listGroupRepo = (groupID) => {
systemAdminAPI.sysAdminListGroupRepos(groupID).then(res => {
this.setState({ repos: res.data.libraries });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
toggleCancel = () => {
this.setState({
showDeleteRepoDialog: false
});
};
onRepoChanged = () => {
this.listGroupRepo(this.props.groupID);
};
showDeleteRepoDialog = (repo) => {
this.setState({ showDeleteRepoDialog: true, deletedRepo: repo });
};
onAddNewRepo = (newRepo) => {
const { repos } = this.state;
repos.unshift(newRepo);
this.setState({ repos });
};
render() {
const { repos } = this.state;
const { groupID } = this.props;
return (
<Fragment>
<Department groupID={groupID} currentItem="repos" onAddNewRepo={this.onAddNewRepo}>
{repos.length > 0 ?
<div className="cur-view-content">
<table>
<thead>
<tr>
<th width="5%"></th>
<th width="50%">{gettext('Name')}</th>
<th width="30%">{gettext('Size')}</th>
<th width="15%"></th>
</tr>
</thead>
<tbody>
{repos.map((repo, index) => {
return (
<RepoItem key={index} repo={repo} showDeleteRepoDialog={this.showDeleteRepoDialog} />
);
})}
</tbody>
</table>
</div>
: <EmptyTip text={gettext('No libraries')} />
}
</Department>
{this.state.showDeleteRepoDialog && (
<ModalPortal>
<DeleteRepoDialog
toggle={this.toggleCancel}
onRepoChanged={this.onRepoChanged}
repo={this.state.deletedRepo}
groupID={groupID}
/>
</ModalPortal>
)}
</Fragment>
);
}
}
DepartmentDetail.propTypes = DepartmentDetailPropTypes;
export default DepartmentDetail;

View File

@@ -1,150 +0,0 @@
import React, { Fragment } from 'react';
import dayjs from 'dayjs';
import { systemAdminAPI } from '../../../utils/system-admin-api';
import MainPanelTopbar from '../main-panel-topbar';
import ModalPortal from '../../../components/modal-portal';
import AddDepartDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-department-dialog';
import { gettext, lang } from '../../../utils/constants';
import GroupItem from './group-item';
import EmptyTip from '../../../components/empty-tip';
import '../../../css/org-department-item.css';
dayjs.locale(lang);
class DepartmentList extends React.Component {
constructor(props) {
super(props);
this.state = {
groups: null,
groupID: '',
isShowAddDepartDialog: false,
isItemFreezed: false
};
}
componentDidMount() {
this.listDepartments();
}
onFreezedItem = () => {
this.setState({ isItemFreezed: true });
};
onUnfreezedItem = () => {
this.setState({ isItemFreezed: false });
};
listDepartments = () => {
systemAdminAPI.sysAdminListAllDepartments().then(res => {
this.setState({ groups: res.data.data });
});
};
toggleAddDepartDialog = () => {
this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog });
};
onDepartmentNameChanged = (dept) => {
this.setState({
groups: this.state.groups.map(item => {
if (item.id == dept.id) {
item.name = dept.name;
}
return item;
})
});
};
onAddNewDepartment = (newDepartment) => {
const { groups } = this.state;
groups.unshift(newDepartment);
this.setState({
groups: groups
});
};
onDeleteDepartment = (id) => {
const { groups } = this.state;
this.setState({
groups: groups.filter((item) => item.id != id)
});
};
onSetDepartmentQuota = (target) => {
const { groups } = this.state;
this.setState({
groups: groups.map((item) => {
if (item.id == target.id) {
item.quota = target.quota;
}
return item;
})
});
};
render() {
const groups = this.state.groups;
return (
<Fragment>
<MainPanelTopbar {...this.props}>
<button className='btn btn-secondary operation-item' title={gettext('New Department')} onClick={this.toggleAddDepartDialog}>{gettext('New Department')}
</button>
{this.state.isShowAddDepartDialog && (
<ModalPortal>
<AddDepartDialog
onAddNewDepartment={this.onAddNewDepartment}
groupID={this.state.groupID}
toggle={this.toggleAddDepartDialog}
/>
</ModalPortal>
)}
</MainPanelTopbar>
<div className="main-panel-center flex-row h-100">
<div className="cur-view-container o-auto">
<div className="cur-view-path">
<div className="fleft">
<h3 className="sf-heading">{gettext('Departments')}</h3>
</div>
</div>
<div className="cur-view-content pb-8">
{groups && groups.length > 0 ?
<table className='mb-8'>
<thead>
<tr>
<th width="40%">{gettext('Name')}</th>
<th width="25%">{gettext('Created At')}</th>
<th width="20%">{gettext('Quota')}</th>
<th width="15%"></th>
</tr>
</thead>
<tbody>
{groups.map((group, index) => {
return (
<Fragment key={group.id}>
<GroupItem
group={group}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
onDepartmentNameChanged={this.onDepartmentNameChanged}
onDeleteDepartment={this.onDeleteDepartment}
onSetDepartmentQuota={this.onSetDepartmentQuota}
/>
</Fragment>
);
})}
</tbody>
</table>
:
<EmptyTip text={gettext('No departments')}/>
}
</div>
</div>
</div>
</Fragment>
);
}
}
export default DepartmentList;

View File

@@ -1,181 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import Paginator from '../../../components/paginator';
import { systemAdminAPI } from '../../../utils/system-admin-api';
import { Utils } from '../../../utils/utils';
import ModalPortal from '../../../components/modal-portal';
import DeleteMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-member-dialog';
import { gettext, lang } from '../../../utils/constants';
import MemberItem from './member-item';
import Department from './department';
import EmptyTip from '../../../components/empty-tip';
import '../../../css/org-department-item.css';
dayjs.locale(lang);
const DepartmentMembersPropTypes = {
groupID: PropTypes.string,
};
class DepartmentMembers extends React.Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false,
members: [],
membersErrorMsg: '',
currentPageInfo: {
},
currentPage: 1,
perPage: 100,
deletedMember: {},
showDeleteMemberDialog: false
};
}
componentDidMount() {
let urlParams = (new URL(window.location)).searchParams;
const { currentPage, perPage } = this.state;
this.setState({
perPage: parseInt(urlParams.get('per_page') || perPage),
currentPage: parseInt(urlParams.get('page') || currentPage)
}, () => {
const { groupID } = this.props;
this.listMembers(groupID, this.state.currentPage, this.state.perPage);
});
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.groupID !== nextProps.groupID) {
this.listMembers(nextProps.groupID, this.state.currentPage, this.state.perPage);
}
}
listMembers = (groupID, page, perPage) => {
systemAdminAPI.sysAdminListGroupMembers(groupID, page, perPage).then((res) => {
this.setState({
members: res.data.members,
currentPageInfo: res.data.page_info
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
this.setState({ membersErrorMsg: errMessage });
});
};
getPreviousPageList = () => {
this.listMembers(this.props.groupID, this.state.currentPageInfo.current_page - 1, this.state.perPage);
};
getNextPageList = () => {
this.listMembers(this.props.groupID, this.state.currentPageInfo.current_page + 1, this.state.perPage);
};
resetPerPage = (perPage) => {
this.setState({
perPage: perPage
}, () => {
this.listMembers(this.props.groupID, 1, perPage);
});
};
toggleCancel = () => {
this.setState({
showDeleteMemberDialog: false,
});
};
onMemberChanged = () => {
this.listMembers(this.props.groupID, this.state.currentPageInfo.current_page, this.state.perPage);
};
toggleItemFreezed = (isFreezed) => {
this.setState({ isItemFreezed: isFreezed });
};
showDeleteMemberDialog = (member) => {
this.setState({ showDeleteMemberDialog: true, deletedMember: member });
};
onAddNewMembers = (newMembers) => {
const { members } = this.state;
members.unshift(...newMembers);
this.setState({ members });
};
render() {
const { members, membersErrorMsg } = this.state;
const { groupID } = this.props;
return (
<Fragment>
<Department
groupID={groupID}
currentItem="members"
onAddNewMembers={this.onAddNewMembers}
>
<div className="cur-view-content">
{membersErrorMsg ? <p className="error text-center">{membersErrorMsg}</p> :
members.length == 0 ?
<EmptyTip text={gettext('No members')}/> :
<Fragment>
<table>
<thead>
<tr>
<th width="5%"></th>
<th width="50%">{gettext('Name')}</th>
<th width="15%">{gettext('Role')}</th>
<th width="30%"></th>
</tr>
</thead>
<tbody>
{members.map((member, index) => {
return (
<Fragment key={index}>
<MemberItem
member={member}
showDeleteMemberDialog={this.showDeleteMemberDialog}
isItemFreezed={this.state.isItemFreezed}
onMemberChanged={this.onMemberChanged}
toggleItemFreezed={this.toggleItemFreezed}
groupID={groupID}
/>
</Fragment>
);
})}
</tbody>
</table>
{this.state.currentPageInfo &&
<Paginator
gotoPreviousPage={this.getPreviousPageList}
gotoNextPage={this.getNextPageList}
currentPage={this.state.currentPageInfo.current_page}
hasNextPage={this.state.currentPageInfo.has_next_page}
curPerPage={this.state.perPage}
resetPerPage={this.resetPerPage}
/>
}
</Fragment>
}
</div>
</Department>
{this.state.showDeleteMemberDialog && (
<ModalPortal>
<DeleteMemberDialog
toggle={this.toggleCancel}
onMemberChanged={this.onMemberChanged}
member={this.state.deletedMember}
groupID={groupID}
/>
</ModalPortal>
)}
</Fragment>
);
}
}
DepartmentMembers.propTypes = DepartmentMembersPropTypes;
export default DepartmentMembers;

View File

@@ -1,29 +1,33 @@
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import { Link } from '@gatsbyjs/reach-router';
import { systemAdminAPI } from '../../../utils/system-admin-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import MainPanelTopbar from '../main-panel-topbar';
import { Table, Dropdown, DropdownToggle } from 'reactstrap';
import Paginator from '../../../components/paginator';
import Loading from '../../../components/loading';
import EmptyTip from '../../../components/empty-tip';
import { gettext } from '../../../utils/constants';
import MemberItem from './member-item';
import RepoItem from './repo-item';
import ModalPortal from '../../../components/modal-portal';
import AddDepartmentDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-department-dialog';
import RenameDepartmentDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-rename-department-dialog';
import AddMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-member-dialog';
import AddRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-repo-dialog';
import { siteRoot, gettext, lang } from '../../../utils/constants';
import DeleteRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-repo-dialog';
import DepartmentNodeMenu from './departments-node-dropdown-menu';
import '../../../css/org-department-item.css';
dayjs.locale(lang);
const DepartmentDetailPropTypes = {
groupID: PropTypes.string,
currentItem: PropTypes.string.isRequired,
onAddNewDepartment: PropTypes.func,
onAddNewMembers: PropTypes.func,
onAddNewRepo: PropTypes.func,
children: PropTypes.object
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,
getPreviousPageList: PropTypes.func,
getNextPageList: PropTypes.func,
resetPerPage: PropTypes.func,
currentPageInfo: PropTypes.object,
perPage: PropTypes.number,
};
class Department extends React.Component {
@@ -31,161 +35,239 @@ class Department extends React.Component {
constructor(props) {
super(props);
this.state = {
groupName: '',
ancestorGroups: [],
isShowAddDepartmentDialog: false,
isShowAddMemberDialog: false,
isShowRenameDepartmentDialog: false,
isShowAddRepoDialog: false
isItemFreezed: false,
activeNav: 'members',
repos: [],
deletedRepo: {},
showDeleteRepoDialog: false,
dropdownOpen: false,
};
this.navItems = [
{ name: 'subDepartments', urlPart: '/', text: gettext('Sub-departments') },
{ name: 'members', urlPart: '/members/', text: gettext('Members') },
{ name: 'repos', urlPart: '/libraries/', text: gettext('Libraries') }
];
}
componentDidMount() {
const groupID = this.props.groupID;
this.getDepartmentInfo(groupID);
this.getRepos(this.props.checkedDepartmentId);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.groupID !== nextProps.groupID) {
this.getDepartmentInfo(nextProps.groupID);
if (this.props.checkedDepartmentId !== nextProps.checkedDepartmentId || this.props.isAddNewRepo !== nextProps.isAddNewRepo) {
this.getRepos(nextProps.checkedDepartmentId);
}
}
getDepartmentInfo = (groupID) => {
systemAdminAPI.sysAdminGetDepartmentInfo(groupID, true).then(res => {
showDeleteRepoDialog = (repo) => {
this.setState({
ancestorGroups: res.data.ancestor_groups,
groupName: res.data.name,
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
showDeleteRepoDialog: true,
deletedRepo: repo,
});
};
onDepartmentNameChanged = (dept) => {
toggleCancel = () => {
this.setState({
groupName: dept.name
showDeleteRepoDialog: false,
deletedRepo: {},
});
};
toggleRenameDepartmentDialog = () => {
this.setState({ isShowRenameDepartmentDialog: !this.state.isShowRenameDepartmentDialog });
onRepoChanged = () => {
this.getRepos(this.props.checkedDepartmentId);
};
toggleAddRepoDialog = () => {
this.setState({ isShowAddRepoDialog: !this.state.isShowAddRepoDialog });
freezeItem = () => {
this.setState({ isItemFreezed: true });
};
toggleAddMemberDialog = () => {
this.setState({ isShowAddMemberDialog: !this.state.isShowAddMemberDialog });
unfreezeItem = () => {
this.setState({ isItemFreezed: false });
};
toggleAddDepartmentDialog = () => {
this.setState({ isShowAddDepartmentDialog: !this.state.isShowAddDepartmentDialog });
getCurrentDepartment = () => {
const { rootNodes, checkedDepartmentId } = this.props;
if (!rootNodes) return {};
let currentDepartment = {};
let arr = [...rootNodes];
while (!currentDepartment.id && arr.length > 0) {
let curr = arr.shift();
if (curr.id === checkedDepartmentId) {
currentDepartment = curr;
} else if (curr.children && curr.children.length > 0) {
arr.push(...curr.children);
}
}
return currentDepartment;
};
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 });
});
};
dropdownToggle = (e) => {
e.stopPropagation();
this.setState({ dropdownOpen: !this.state.dropdownOpen });
};
render() {
const { groupID, currentItem } = this.props;
const { groupName } = this.state;
const topBtn = 'btn btn-secondary operation-item';
const topbarChildren = (
<Fragment>
{groupID &&
<Fragment>
<button className={topBtn} title={gettext('Rename Department')} onClick={this.toggleRenameDepartmentDialog}>{gettext('Rename Department')}</button>
{currentItem == 'subDepartments' && <button className={topBtn} title={gettext('New Sub-department')} onClick={this.toggleAddDepartmentDialog}>{gettext('New Sub-department')}</button>}
{currentItem == 'members' && <button className={topBtn} title={gettext('Add Member')} onClick={this.toggleAddMemberDialog}>{gettext('Add Member')}</button>}
{currentItem == 'repos' && <button className={topBtn} onClick={this.toggleAddRepoDialog} title={gettext('New Library')}>{gettext('New Library')}</button>}
</Fragment>
}
{this.state.isShowRenameDepartmentDialog && (
<ModalPortal>
<RenameDepartmentDialog
groupID={groupID}
name={groupName}
toggle={this.toggleRenameDepartmentDialog}
onDepartmentNameChanged={this.onDepartmentNameChanged}
/>
</ModalPortal>
)}
{this.state.isShowAddMemberDialog && (
<ModalPortal>
<AddMemberDialog
toggle={this.toggleAddMemberDialog}
onAddNewMembers={this.props.onAddNewMembers}
groupID={groupID}
/>
</ModalPortal>
)}
{this.state.isShowAddRepoDialog && (
<ModalPortal>
<AddRepoDialog
toggle={this.toggleAddRepoDialog}
onAddNewRepo={this.props.onAddNewRepo}
groupID={groupID}
/>
</ModalPortal>
)}
{this.state.isShowAddDepartmentDialog && (
<ModalPortal>
<AddDepartmentDialog
onAddNewDepartment={this.props.onAddNewDepartment}
parentGroupID={groupID}
toggle={this.toggleAddDepartmentDialog}
/>
</ModalPortal>
)}
</Fragment>
);
const { activeNav, repos } = this.state;
const { membersList, isMembersListLoading, sortBy, sortOrder } = this.props;
const sortByName = sortBy === 'name';
const sortByRole = sortBy === 'role';
const sortIcon = <span className={`sort-dirent sf3-font sf3-font-down ${sortOrder === 'asc' ? 'rotate-180' : ''}`}></span>;
const currentDepartment = this.getCurrentDepartment();
return (
<Fragment>
<MainPanelTopbar {...this.props}>
{topbarChildren}
</MainPanelTopbar>
<div className="main-panel-center flex-row h-100">
<div className="cur-view-container o-auto">
<div className="cur-view-path">
<div className="fleft">
<h3 className="sf-heading">
{groupID ?
<Link to={siteRoot + 'sys/departments/'}>{gettext('Departments')}</Link>
: <span>{gettext('Departments')}</span>
}
{this.state.ancestorGroups.map(ancestor => {
let newHref = siteRoot + 'sys/departments/' + ancestor.id + '/';
return <span key={ancestor.id}>{' / '}<Link to={newHref}>{ancestor.name}</Link></span>;
})}
{groupID && <span>{' / '}{groupName}</span>}
</h3>
</div>
<div className="department-content-main d-flex flex-column">
<div className="department-content-main-name">
{currentDepartment.name}
<Dropdown
isOpen={this.state.dropdownOpen}
toggle={(e) => this.dropdownToggle(e)}
direction="down"
className="department-dropdown-menu"
>
<DropdownToggle
tag='span'
role="button"
className='sf3-font-down sf3-font ml-1 sf-dropdown-toggle'
title={gettext('More operations')}
aria-label={gettext('More operations')}
data-toggle="dropdown"
/>
<DepartmentNodeMenu
node={currentDepartment}
toggleAddDepartment={this.props.toggleAddDepartment}
toggleAddLibrary={this.props.toggleAddLibrary}
toggleAddMembers={this.props.toggleAddMembers}
toggleRename={this.props.toggleRename}
toggleDelete={this.props.toggleDelete}
/>
</Dropdown>
</div>
<ul className="nav border-bottom mx-4">
{this.navItems.map((item, index) => {
return (
<li className="nav-item mr-2" key={index}>
<Link to={`${siteRoot}sys/departments/${groupID}${item.urlPart}`} className={`nav-link ${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
<div className="cur-view-path tab-nav-container">
<ul className="nav">
<li className="nav-item">
<span className={`nav-link ${activeNav === 'members' ? 'active' : ''}`} onClick={() => this.changeActiveNav('members')}>{gettext('Members')}</span>
</li>
<li className="nav-item">
<span className={`nav-link ${activeNav === 'repos' ? 'active' : ''}`} onClick={() => this.changeActiveNav('repos')}>{gettext('Libraries')}</span>
</li>
</ul>
</div>
{activeNav === 'members' && (
<div className='cur-view-content'>
{isMembersListLoading
? <Loading />
: membersList.length == 0
? <EmptyTip text={gettext('No members')} />
: (
<div className="w-xs-250">
<Table>
<thead>
<tr>
<th width="10%"></th>
<th width="25%" onClick={this.sortByName}>{gettext('Name')}{' '}{sortByName && sortIcon}</th>
<th width="25%" onClick={this.sortByRole}>{gettext('Role')}{' '}{sortByRole && sortIcon}</th>
<th width="30%">{gettext('Contact email')}</th>
<th width="10%">{/* Operations */}</th>
</tr>
</thead>
<tbody>
{membersList.map((item, index) => {
return (
<MemberItem
key={index}
member={item}
deleteMember={this.props.deleteMember}
setMemberStaff={this.props.setMemberStaff}
unfreezeItem={this.unfreezeItem}
freezeItem={this.freezeItem}
isItemFreezed={this.state.isItemFreezed}
/>
);
})}
</ul>
{this.props.children}
</tbody>
</Table>
{this.props.currentPageInfo &&
<Paginator
gotoPreviousPage={this.props.getPreviousPageList}
gotoNextPage={this.props.getNextPageList}
currentPage={this.props.currentPageInfo.current_page}
hasNextPage={this.props.currentPageInfo.has_next_page}
curPerPage={this.props.perPage}
resetPerPage={this.props.resetPerPage}
noURLUpdate={true}
/>
}
</div>
)}
</div>
)}
{activeNav === 'repos' && (
<div className="cur-view-content">
{repos.length == 0
? <EmptyTip text={gettext('No libraries')} />
: (
<>
<table>
<thead>
<tr>
<th width="5%"></th>
<th width="50%">{gettext('Name')}</th>
<th width="30%">{gettext('Size')}</th>
<th width="15%"></th>
</tr>
</thead>
<tbody>
{repos.map((repo, index) => {
return (
<RepoItem key={index} repo={repo} showDeleteRepoDialog={this.showDeleteRepoDialog} />
);
})}
</tbody>
</table>
{this.state.showDeleteRepoDialog && (
<ModalPortal>
<DeleteRepoDialog
toggle={this.toggleCancel}
onRepoChanged={this.onRepoChanged}
repo={this.state.deletedRepo}
groupID={this.props.checkedDepartmentId}
/>
</ModalPortal>
)}
</>
)
}
</div>
)}
</div>
</Fragment>
);
}
}
Department.propTypes = DepartmentDetailPropTypes;
Department.propTypes = propTypes;
export default Department;

View File

@@ -1,8 +1,8 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import DepartmentsV2TreeNode from './departments-v2-tree-node';
import DepartmentTreeNode from './tree-node';
const DepartmentV2TreePanelPropTypes = {
const DepartmentsTreePanelPropTypes = {
rootNodes: PropTypes.array,
checkedDepartmentId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
onChangeDepartment: PropTypes.func,
@@ -14,14 +14,14 @@ const DepartmentV2TreePanelPropTypes = {
toggleDelete: PropTypes.func
};
class DepartmentV2TreePanel extends Component {
class DepartmentsTreePanel extends Component {
render() {
const { rootNodes, checkedDepartmentId } = this.props;
return (
<div className="departments-tree-panel">
{rootNodes.map(rootNode => {
return (
<DepartmentsV2TreeNode
<DepartmentTreeNode
key={rootNode.id}
node={rootNode}
checkedDepartmentId={checkedDepartmentId}
@@ -40,6 +40,6 @@ class DepartmentV2TreePanel extends Component {
}
}
DepartmentV2TreePanel.propTypes = DepartmentV2TreePanelPropTypes;
DepartmentsTreePanel.propTypes = DepartmentsTreePanelPropTypes;
export default DepartmentV2TreePanel;
export default DepartmentsTreePanel;

View File

@@ -1,19 +1,428 @@
import React from 'react';
import PropTypes from 'prop-types';
import '../../../css/org-department-item.css';
import React, { Fragment } from 'react';
import { Button } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import { systemAdminAPI } from '../../../utils/system-admin-api';
import Loading from '../../../components/loading';
import toaster from '../../../components/toast';
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 DepartmentNode from './department-node';
import DepartmentsTreePanel from './departments-tree-panel';
import Department from './department';
import MainPanelTopbar from '../main-panel-topbar';
import '../../../css/system-departments-v2.css';
class Departments extends React.Component {
render() {
return (
<div className="h-100 org-departments">
{this.props.children}
</div>
);
}
constructor(props) {
super(props);
this.state = {
rootNodes: [],
checkedDepartmentId: -1,
operateNode: null,
isAddDepartmentDialogShow: false,
isAddMembersDialogShow: false,
isRenameDepartmentDialogShow: false,
isDeleteDepartmentDialogShow: false,
isShowAddRepoDialog: false,
membersList: [],
isTopDepartmentLoading: false,
isMembersListLoading: false,
sortBy: 'name', // 'name' or 'role'
sortOrder: 'asc', // 'asc' or 'desc',
currentPageInfo: null,
currentPage: 1,
perPage: 100
};
}
Departments.propTypes = {
children: PropTypes.any.isRequired,
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 });
const { currentPage, perPage } = this.state;
systemAdminAPI.sysAdminListGroupMembers(nodeId, currentPage, perPage).then(res => {
const { members, page_info } = res.data;
this.setState({
membersList: members,
currentPageInfo: page_info,
isMembersListLoading: false
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
getPreviousPageList = () => {
const { checkedDepartmentId, currentPageInfo } = this.state;
const { current_page } = currentPageInfo;
this.setState({ currentPage: current_page - 1 }, () => {
this.loadDepartmentMembers(checkedDepartmentId);
});
};
getNextPageList = () => {
const { checkedDepartmentId, currentPageInfo } = this.state;
const { current_page } = currentPageInfo;
this.setState({ currentPage: current_page + 1 }, () => {
this.loadDepartmentMembers(checkedDepartmentId);
});
};
resetPerPage = (perPage) => {
this.setState({
perPage: perPage,
currentPage: 1
}, () => {
const { checkedDepartmentId } = this.state;
this.loadDepartmentMembers(checkedDepartmentId);
});
};
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);
if (rootNodes.length === 0) {
this.setState({
rootNodes,
checkedDepartmentId: -1
});
} else {
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.role == 'Admin' ? -1 : 1;
};
break;
case 'role-desc':
comparator = function (a, b) {
return a.role == 'Admin' ? 1 : -1;
};
break;
// no default
}
items.sort((a, b) => {
return comparator(a, b);
});
return items;
};
sortItems = (sortBy, 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 (
<Fragment>
<MainPanelTopbar {...this.props}>
<button className='btn btn-secondary operation-item' title={gettext('New Department')} onClick={() => {this.toggleAddDepartment(null);}}>{gettext('New Department')}</button>
</MainPanelTopbar>
<div className="main-panel-center">
<div className="cur-view-container">
<h2 className="heading">{gettext('Departments')}</h2>
<div className="cur-view-content d-flex flex-row p-0">
{isTopDepartmentLoading && <Loading/>}
{(!isTopDepartmentLoading && rootNodes.length > 0) &&
<>
<DepartmentsTreePanel
rootNodes={rootNodes}
checkedDepartmentId={checkedDepartmentId}
onChangeDepartment={this.onChangeDepartment}
listSubDepartments={this.listSubDepartments}
toggleAddDepartment={this.toggleAddDepartment}
toggleAddLibrary={this.toggleAddLibrary}
toggleAddMembers={this.toggleAddMembers}
toggleRename={this.toggleRename}
toggleDelete={this.toggleDelete}
/>
<Department
rootNodes={rootNodes}
checkedDepartmentId={checkedDepartmentId}
membersList={membersList}
isMembersListLoading={isMembersListLoading}
isAddNewRepo={this.state.isAddNewRepo}
setMemberStaff={this.setMemberStaff}
deleteMember={this.deleteMember}
sortItems={this.sortItems}
sortBy={sortBy}
sortOrder={sortOrder}
getRepos={this.getRepos}
deleteGroup={this.deleteGroup}
createGroup={this.createGroup}
toggleAddDepartment={this.toggleAddDepartment}
toggleAddLibrary={this.toggleAddLibrary}
toggleAddMembers={this.toggleAddMembers}
toggleRename={this.toggleRename}
toggleDelete={this.toggleDelete}
getPreviousPageList={this.getPreviousPageList}
getNextPageList={this.getNextPageList}
resetPerPage={this.resetPerPage}
currentPageInfo={this.state.currentPageInfo}
perPage={this.state.perPage}
/>
</>
}
{(!isTopDepartmentLoading && rootNodes.length === 0) &&
<div className="top-department-button-container h-100 w-100">
<Button onClick={this.toggleAddDepartment.bind(this, null)}>
{gettext('Enable departments feature')}
</Button>
</div>
}
</div>
</div>
</div>
{isAddMembersDialogShow &&
<AddDepartMemberV2Dialog
toggle={this.toggleAddMembers}
nodeId={operateNode.id}
onMemberChanged={this.onMemberChanged}
/>
}
{isRenameDepartmentDialogShow &&
<RenameDepartmentV2Dialog
node={operateNode}
toggle={this.toggleRename}
renameDepartment={this.renameDepartment}
/>
}
{isDeleteDepartmentDialogShow &&
<DeleteDepartmentV2ConfirmDialog
node={operateNode}
toggle={this.toggleDelete}
onDelete={this.onDelete}
/>
}
{isAddDepartmentDialogShow &&
<AddDepartmentV2Dialog
parentNode={operateNode}
toggle={this.toggleAddDepartment}
addDepartment={this.addDepartment}
setRootNode={this.setRootNode}
/>
}
{this.state.isShowAddRepoDialog && (
<AddRepoDialog
toggle={this.toggleAddLibrary}
onAddNewRepo={() => {this.setState({ isAddNewRepo: !this.state.isAddNewRepo });}}
groupID={String(operateNode.id)}
/>
)}
</Fragment>
);
}
}
export default Departments;

View File

@@ -1,170 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Link } from '@gatsbyjs/reach-router';
import { Utils } from '../../../utils/utils';
import { siteRoot, gettext } from '../../../utils/constants';
import OpMenu from '../../../components/dialog/op-menu';
import ModalPortal from '../../../components/modal-portal';
import RenameDepartmentDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-rename-department-dialog';
import DeleteDepartmentDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-department-dialog';
import SetGroupQuotaDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-set-group-quota-dialog';
const GroupItemPropTypes = {
isItemFreezed: PropTypes.bool.isRequired,
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired,
group: PropTypes.object.isRequired,
onDepartmentNameChanged: PropTypes.func.isRequired,
onDeleteDepartment: PropTypes.func.isRequired,
onSetDepartmentQuota: PropTypes.func.isRequired
};
dayjs.extend(relativeTime);
class GroupItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpIconShown: false,
highlight: false,
isSetQuotaDialogOpen: false,
isDeleteDialogOpen: false,
isRenameDialogOpen: false
};
}
handleMouseOver = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: true,
highlight: true
});
}
};
handleMouseOut = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: false,
highlight: false
});
}
};
onUnfreezedItem = () => {
this.setState({
highlight: false,
isOpIconShow: false
});
this.props.onUnfreezedItem();
};
translateOperations = (item) => {
let translateResult = '';
switch (item) {
case 'Rename':
translateResult = gettext('Rename');
break;
case 'Delete':
translateResult = gettext('Delete');
break;
default:
break;
}
return translateResult;
};
onMenuItemClick = (operation) => {
switch (operation) {
case 'Rename':
this.toggleRenameDialog();
break;
case 'Delete':
this.toggleDeleteDialog();
break;
default:
break;
}
};
toggleRenameDialog = () => {
this.setState({
isRenameDialogOpen: !this.state.isRenameDialogOpen
});
};
toggleDeleteDialog = () => {
this.setState({
isDeleteDialogOpen: !this.state.isDeleteDialogOpen
});
};
toggleSetQuotaDialog = () => {
this.setState({
isSetQuotaDialogOpen: !this.state.isSetQuotaDialogOpen
});
};
render() {
const { group } = this.props;
const { highlight, isOpIconShown, isRenameDialogOpen, isDeleteDialogOpen, isSetQuotaDialogOpen } = this.state;
const newHref = siteRoot + 'sys/departments/' + group.id + '/';
return (
<Fragment>
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<td><Link to={newHref}>{group.name}</Link></td>
<td>{dayjs(group.created_at).fromNow()}</td>
<td>
{Utils.bytesToSize(group.quota)}{' '}
<span onClick={this.toggleSetQuotaDialog} title={gettext('Edit')} className={`sf3-font sf3-font-rename attr-action-icon ${highlight ? '' : 'vh'}`}></span>
</td>
<td>
{isOpIconShown &&
<OpMenu
operations={['Rename', 'Delete']}
translateOperations={this.translateOperations}
onMenuItemClick={this.onMenuItemClick}
onFreezedItem={this.props.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
}
</td>
</tr>
{isDeleteDialogOpen && (
<ModalPortal>
<DeleteDepartmentDialog
group={group}
onDeleteDepartment={this.props.onDeleteDepartment}
toggle={this.toggleDeleteDialog}
/>
</ModalPortal>
)}
{isSetQuotaDialogOpen && (
<ModalPortal>
<SetGroupQuotaDialog
groupID={group.id}
onSetQuota={this.props.onSetDepartmentQuota}
toggle={this.toggleSetQuotaDialog}
/>
</ModalPortal>
)}
{isRenameDialogOpen && (
<RenameDepartmentDialog
groupID={group.id}
name={group.name}
toggle={this.toggleRenameDialog}
onDepartmentNameChanged={this.props.onDepartmentNameChanged}
/>
)}
</Fragment>
);
}
}
GroupItem.propTypes = GroupItemPropTypes;
export default GroupItem;

View File

@@ -1,19 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { systemAdminAPI } from '../../../utils/system-admin-api';
import { Utils } from '../../../utils/utils';
import { gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import RoleSelector from '../../../components/single-selector';
import UserLink from '../user-link';
import { gettext, siteRoot } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
const MemberItemPropTypes = {
groupID: PropTypes.string,
member: PropTypes.object.isRequired,
isItemFreezed: PropTypes.bool.isRequired,
onMemberChanged: PropTypes.func.isRequired,
showDeleteMemberDialog: PropTypes.func.isRequired,
toggleItemFreezed: PropTypes.func.isRequired,
const propTypes = {
isItemFreezed: PropTypes.bool,
member: PropTypes.object,
setMemberStaff: PropTypes.func,
deleteMember: PropTypes.func,
unfreezeItem: PropTypes.func,
freezeItem: PropTypes.func,
};
class MemberItem extends React.Component {
@@ -21,7 +20,8 @@ class MemberItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false
highlighted: false,
isItemMenuShow: false,
};
this.roleOptions = [
{ value: 'Admin', text: gettext('Admin'), isSelected: false },
@@ -30,58 +30,119 @@ class MemberItem extends React.Component {
}
onMouseEnter = () => {
if (this.props.isItemFreezed) return;
this.setState({ highlight: true });
if (!this.props.isItemFreezed) {
this.setState({ highlighted: true });
}
};
onMouseLeave = () => {
if (this.props.isItemFreezed) return;
this.setState({ highlight: false });
if (!this.props.isItemFreezed) {
this.setState({ highlighted: false });
}
};
onChangeUserRole = (roleOption) => {
let isAdmin = roleOption.value === 'Admin' ? true : false;
systemAdminAPI.sysAdminUpdateGroupMemberRole(this.props.groupID, this.props.member.email, isAdmin).then((res) => {
this.props.onMemberChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
setMemberStaff = (role) => {
this.props.setMemberStaff(this.props.member.email, role.value === 'Admin');
};
deleteMember = () => {
const { member } = this.props;
this.props.deleteMember(member.email);
};
toggleDropdownMenu = () => {
this.setState({
highlight: false
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();
}
});
};
toggleItemFreezed = (freezed) => {
if (freezed) {
this.props.freezeItem();
} else {
this.props.unfreezeItem();
this.setState({ highlighted: false });
}
};
toggleDeleteMemberDialog = () => {
this.setState({
isDeleteMemberDialogOpen: !this.state.isDeleteMemberDialogOpen
});
};
render() {
const member = this.props.member;
const highlight = this.state.highlight;
if (member.role === 'Owner') return null;
const { member } = this.props;
const { highlighted, isItemMenuShow, isDeleteMemberDialogOpen } = this.state;
this.roleOptions = this.roleOptions.map(item => {
item.isSelected = item.value == member.role;
return item;
});
const currentSelectedOption = this.roleOptions.filter(item => item.isSelected)[0];
return (
<tr className={highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td><img src={member.avatar_url} alt="member-header" width="24" className="avatar"/></td>
<td><UserLink email={member.email} name={member.name} /></td>
<>
<tr className={`departments-members-item ${highlighted ? 'tr-highlight' : ''}`} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td><img className="avatar" src={member.avatar_url} alt="" /></td>
<td className='text-truncate'>
<a href={`${siteRoot}sys/users/${encodeURIComponent(member.email)}/`}>{member.name}</a>
</td>
<td>
<RoleSelector
isDropdownToggleShown={highlight}
isDropdownToggleShown={highlighted}
currentSelectedOption={currentSelectedOption}
options={this.roleOptions}
selectOption={this.onChangeUserRole}
toggleItemFreezed={this.props.toggleItemFreezed}
selectOption={this.setMemberStaff}
toggleItemFreezed={this.toggleItemFreezed}
/>
</td>
<td className="cursor-pointer text-center" onClick={this.props.showDeleteMemberDialog.bind(this, member)}>
<span className={`sf2-icon-x3 action-icon ${highlight ? '' : 'vh'}`} title="Delete"></span>
<td>{member.contact_email}</td>
<td>
{highlighted &&
<Dropdown
isOpen={isItemMenuShow}
toggle={this.toggleDropdownMenu}
direction="down"
>
<DropdownToggle
tag='a'
role="button"
className='attr-action-icon sf3-font sf3-font-more-vertical'
title={gettext('More operations')}
aria-label={gettext('More operations')}
data-toggle="dropdown"
/>
<DropdownMenu right={true}>
<DropdownItem onClick={this.toggleDeleteMemberDialog}>{gettext('Delete')}</DropdownItem>
</DropdownMenu>
</Dropdown>
}
</td>
</tr>
{isDeleteMemberDialogOpen && (
<CommonOperationConfirmationDialog
title={gettext('Delete Member')}
message={
gettext('Are you sure you want to delete {placeholder} ?')
.replace('{placeholder}', '<span class="op-target">' + Utils.HTMLescape(member.name) + '</span>')
}
executeOperation={this.deleteMember}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteMemberDialog}
/>
)}
</>
);
}
}
MemberItem.propTypes = MemberItemPropTypes;
MemberItem.propTypes = propTypes;
export default MemberItem;

View File

@@ -34,15 +34,16 @@ class RepoItem extends React.Component {
let iconUrl = Utils.getLibIconUrl(repo);
return (
<tr className={highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td><img src={iconUrl} width="24" alt={gettext('icon')}/></td>
{ enableSysAdminViewRepo ?
<td><a href={`${siteRoot}sys/libraries/${repo.repo_id}/${encodeURIComponent(repoName)}/`}>{repoName}</a></td>
:
<td>{repoName}</td>
<td><img src={iconUrl} width="24" alt="" /></td>
<td>
{enableSysAdminViewRepo
? <a href={`${siteRoot}sys/libraries/${repo.repo_id}/${encodeURIComponent(repoName)}/`}>{repoName}</a>
: <span>{repoName}</span>
}
</td>
<td>{Utils.bytesToSize(repo.size)}</td>
<td className="cursor-pointer text-center" onClick={this.props.showDeleteRepoDialog.bind(this, repo)}>
<span className={`sf3-font-delete1 sf3-font op-icon ${highlight ? '' : 'vh'}`} title="Delete"></span>
<span className={`sf3-font-delete1 sf3-font op-icon ${highlight ? '' : 'vh'}`} title={gettext('Delete')}></span>
</td>
</tr>
);

View File

@@ -1,143 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import { systemAdminAPI } from '../../../utils/system-admin-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import { gettext, lang } from '../../../utils/constants';
import GroupItem from './group-item';
import Department from './department';
import EmptyTip from '../../../components/empty-tip';
import '../../../css/org-department-item.css';
dayjs.locale(lang);
const SubDepartmentsPropTypes = {
groupID: PropTypes.string
};
class SubDepartments extends React.Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false,
groups: []
};
}
componentDidMount() {
this.listSubDepartments(this.props.groupID);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.groupID !== nextProps.groupID) {
this.listSubDepartments(nextProps.groupID);
}
}
listSubDepartments = (groupID) => {
systemAdminAPI.sysAdminGetDepartmentInfo(groupID, true).then(res => {
this.setState({ groups: res.data.groups });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
onSubDepartmentNameChanged = (dept) => {
this.setState({
groups: this.state.groups.map(item => {
if (item.id == dept.id) {
item.name = dept.name;
}
return item;
})
});
};
onFreezedItem = () => {
this.setState({ isItemFreezed: true });
};
onUnfreezedItem = () => {
this.setState({ isItemFreezed: false });
};
onAddNewDepartment = (newDepartment) => {
const { groups } = this.state;
groups.unshift(newDepartment);
this.setState({
groups: groups
});
};
onDeleteDepartment = (id) => {
const { groups } = this.state;
this.setState({
groups: groups.filter(item => item.id != id)
});
};
onSetDepartmentQuota = (target) => {
const { groups } = this.state;
this.setState({
groups: groups.map((item) => {
if (item.id == target.id) {
item.quota = target.quota;
}
return item;
})
});
};
render() {
const { groups } = this.state;
const { groupID } = this.props;
return (
<Department
groupID={groupID}
currentItem="subDepartments"
onAddNewDepartment={this.onAddNewDepartment}
>
<div className="cur-view-content">
{groups && groups.length > 0 ?
<table>
<thead>
<tr>
<th width="40%">{gettext('Name')}</th>
<th width="25%">{gettext('Created At')}</th>
<th width="20%">{gettext('Quota')}</th>
<th width="15%"></th>
</tr>
</thead>
<tbody>
{groups.map((group, index) => {
return (
<GroupItem
key={group.id}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
onDepartmentNameChanged={this.onSubDepartmentNameChanged}
group={group}
onDeleteDepartment={this.onDeleteDepartment}
onSetDepartmentQuota={this.onSetDepartmentQuota}
/>
);
})}
</tbody>
</table>
: <EmptyTip text={gettext('No sub-departments')}/>
}
</div>
</Department>
);
}
}
SubDepartments.propTypes = SubDepartmentsPropTypes;
export default SubDepartments;

View File

@@ -5,7 +5,7 @@ import { Dropdown, DropdownToggle } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import DepartmentNodeMenu from './departments-node-dropdown-menu';
const departmentsV2TreeNodePropTypes = {
const departmentsTreeNodePropTypes = {
node: PropTypes.object,
checkedDepartmentId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
listSubDepartments: PropTypes.func,
@@ -17,7 +17,7 @@ const departmentsV2TreeNodePropTypes = {
toggleDelete: PropTypes.func
};
class DepartmentsV2TreeNode extends Component {
class DepartmentsTreeNode extends Component {
constructor(props) {
super(props);
@@ -74,7 +74,7 @@ class DepartmentsV2TreeNode extends Component {
if (nodes.length > 0) {
return nodes.map((node) => {
return (
<DepartmentsV2TreeNode
<DepartmentsTreeNode
key={node.id}
node={node}
onChangeDepartment={this.props.onChangeDepartment}
@@ -167,6 +167,6 @@ class DepartmentsV2TreeNode extends Component {
}
}
DepartmentsV2TreeNode.propTypes = departmentsV2TreeNodePropTypes;
DepartmentsTreeNode.propTypes = departmentsTreeNodePropTypes;
export default DepartmentsV2TreeNode;
export default DepartmentsTreeNode;

View File

@@ -46,7 +46,7 @@ import SearchGroups from './groups/search-groups';
import GroupRepos from './groups/group-repos';
import GroupMembers from './groups/group-members';
import DepartmentsV2 from './departments-v2/departments-v2';
import Departments from './departments/departments';
import ShareLinks from './links/share-links';
import UploadLinks from './links/upload-links';
@@ -237,7 +237,7 @@ class SysAdmin extends React.Component {
<SearchGroups path={siteRoot + 'sys/search-groups'} {...commonProps} />
<GroupRepos path={siteRoot + 'sys/groups/:groupID/libraries'} {...commonProps} />
<GroupMembers path={siteRoot + 'sys/groups/:groupID/members'} {...commonProps} />
<DepartmentsV2 path={siteRoot + 'sys/departments/'} onCloseSidePanel={this.onCloseSidePanel} />
<Departments path={siteRoot + 'sys/departments/'} {...commonProps} />
<ShareLinks path={siteRoot + 'sys/share-links'} {...commonProps} />
<UploadLinks path={siteRoot + 'sys/upload-links'} {...commonProps} />
<Orgs path={siteRoot + 'sys/organizations'} {...commonProps} />