mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-26 15:26:19 +00:00
update department structure func (#8043)
* update department structure * optimize * optimize var * Update groups.py * code-optimize * Update api_urls.py * Update department-select.css --------- Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com> Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, Modal, ModalBody, ModalFooter, Popover } from 'reactstrap';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
import { gettext, orgID } from '../../../utils/constants';
|
||||||
|
import { orgAdminAPI } from '../../../utils/org-admin-api';
|
||||||
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||||
|
import SearchInput from '../../search-input';
|
||||||
|
import ClickOutside from '../../click-outside';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import '../../../css/department-select.css';
|
||||||
|
|
||||||
|
export default class MoveDepartmentDialog extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
nodeId: PropTypes.number.isRequired,
|
||||||
|
onDepartmentChanged: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
selectedDepartment: null,
|
||||||
|
searchedDepartments: [],
|
||||||
|
searchValue: '',
|
||||||
|
highlightIndex: -1,
|
||||||
|
isPopoverOpen: false,
|
||||||
|
errMsgs: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged = (newSearchValue) => {
|
||||||
|
this.setState({
|
||||||
|
searchValue: newSearchValue
|
||||||
|
});
|
||||||
|
const searchValue = newSearchValue.trim();
|
||||||
|
if (searchValue.length === 0) {
|
||||||
|
this.setState({
|
||||||
|
searchedDepartments: [],
|
||||||
|
highlightIndex: -1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (orgID) {
|
||||||
|
orgAdminAPI.orgAdminSearchGroup(orgID, searchValue).then((res) => {
|
||||||
|
const filteredGroupList = res.data.group_list
|
||||||
|
.filter(item => item.creator_email === 'system admin')
|
||||||
|
.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.group_name,
|
||||||
|
creator_email: item.creator_email,
|
||||||
|
creator_name: item.creator_name,
|
||||||
|
ctime: item.ctime,
|
||||||
|
creator_contact_email: item.creator_contact_email,
|
||||||
|
}));
|
||||||
|
this.setState({
|
||||||
|
searchedDepartments: filteredGroupList,
|
||||||
|
highlightIndex: filteredGroupList.length > 0 ? 0 : -1,
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
systemAdminAPI.sysAdminSearchGroups(searchValue).then((res) => {
|
||||||
|
const filteredGroupList = res.data.group_list
|
||||||
|
.filter(item => item.owner === 'system admin')
|
||||||
|
.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
creator_email: item.owner,
|
||||||
|
creator_name: item.owner_name,
|
||||||
|
ctime: item.ctime,
|
||||||
|
creator_contact_email: item.creator_contact_email,
|
||||||
|
}));
|
||||||
|
this.setState({
|
||||||
|
searchedDepartments: filteredGroupList,
|
||||||
|
highlightIndex: filteredGroupList.length > 0 ? 0 : -1,
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onDepartmentClick = (department) => {
|
||||||
|
this.setState({
|
||||||
|
selectedDepartment: department,
|
||||||
|
isPopoverOpen: false,
|
||||||
|
searchValue: '',
|
||||||
|
searchedDepartments: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onClickOutside = (e) => {
|
||||||
|
if (e.target.id !== 'department-select' && this.state.isPopoverOpen) {
|
||||||
|
this.setState({
|
||||||
|
isPopoverOpen: false,
|
||||||
|
searchedDepartments: [],
|
||||||
|
searchValue: '',
|
||||||
|
highlightIndex: -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onTogglePopover = () => {
|
||||||
|
this.setState({ isPopoverOpen: !this.state.isPopoverOpen });
|
||||||
|
if (!this.state.isPopoverOpen) {
|
||||||
|
this.onValueChanged(this.state.searchValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
if (!this.state.selectedDepartment) return;
|
||||||
|
|
||||||
|
const { nodeId } = this.props;
|
||||||
|
const targetDepartmentId = this.state.selectedDepartment.id;
|
||||||
|
const req = orgID ?
|
||||||
|
orgAdminAPI.orgAdminMoveDepartment(orgID, nodeId, targetDepartmentId) :
|
||||||
|
systemAdminAPI.sysAdminMoveDepartment(nodeId, targetDepartmentId);
|
||||||
|
|
||||||
|
req.then((res) => {
|
||||||
|
this.props.toggle();
|
||||||
|
this.props.onDepartmentChanged(this.state.selectedDepartment);
|
||||||
|
toaster.success(gettext('Department moved successfully'));
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { searchValue, searchedDepartments, selectedDepartment, errMsgs } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={true} toggle={this.props.toggle}>
|
||||||
|
<SeahubModalHeader toggle={this.props.toggle}>{gettext('Move to department')}</SeahubModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="department-select-component">
|
||||||
|
<ClickOutside onClickOutside={this.onClickOutside}>
|
||||||
|
<div className="department-select-wrapper">
|
||||||
|
<div
|
||||||
|
className={classnames('department-select-input', { 'focus': this.state.isPopoverOpen })}
|
||||||
|
id="department-select"
|
||||||
|
onClick={this.onTogglePopover}
|
||||||
|
>
|
||||||
|
{selectedDepartment ? (
|
||||||
|
<div className="selected-department">
|
||||||
|
{selectedDepartment.name}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="department-select-placeholder">
|
||||||
|
{gettext('Select target department')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
placement="bottom-start"
|
||||||
|
isOpen={this.state.isPopoverOpen}
|
||||||
|
target={'department-select'}
|
||||||
|
hideArrow={true}
|
||||||
|
fade={false}
|
||||||
|
className="department-select-popover"
|
||||||
|
>
|
||||||
|
<div className="department-select-container">
|
||||||
|
<div className="department-search-container">
|
||||||
|
<SearchInput
|
||||||
|
autoFocus={true}
|
||||||
|
placeholder={gettext('Search departments')}
|
||||||
|
value={searchValue}
|
||||||
|
onChange={this.onValueChanged}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="department-list-container">
|
||||||
|
{searchedDepartments.length > 0 ? (
|
||||||
|
searchedDepartments.map((department, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={department.id}
|
||||||
|
className={classnames('department-item', {
|
||||||
|
'department-item-highlight': index === this.state.highlightIndex
|
||||||
|
})}
|
||||||
|
onClick={() => this.onDepartmentClick(department)}
|
||||||
|
>
|
||||||
|
<span className="department-name">{department.name}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="no-search-result">
|
||||||
|
{searchValue ? gettext('Department not found') : gettext('Enter characters to start searching')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</ClickOutside>
|
||||||
|
</div>
|
||||||
|
{errMsgs.length > 0 && (
|
||||||
|
<ul className="list-unstyled">
|
||||||
|
{errMsgs.map((item, index) => {
|
||||||
|
return <li key={index} className="error mt-2">{item}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
|
||||||
|
<Button color="primary" onClick={this.handleSubmit} disabled={!selectedDepartment}>{gettext('Submit')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
88
frontend/src/css/department-select.css
Normal file
88
frontend/src/css/department-select.css
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
.department-select-component {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-select-input {
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-select-input.focus {
|
||||||
|
color: #495057;
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: #80bdff;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-select-placeholder {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-department {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-select-popover {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-select-container {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-search-container {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-list-container {
|
||||||
|
padding: 4px 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-item {
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-item:hover,
|
||||||
|
.department-item-highlight {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-search-result {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
text-align: center;
|
||||||
|
}
|
@@ -132,6 +132,7 @@ class Department extends React.Component {
|
|||||||
toggleAddMembers={this.props.toggleAddMembers}
|
toggleAddMembers={this.props.toggleAddMembers}
|
||||||
toggleRename={this.props.toggleRename}
|
toggleRename={this.props.toggleRename}
|
||||||
toggleDelete={this.props.toggleDelete}
|
toggleDelete={this.props.toggleDelete}
|
||||||
|
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { DropdownItem, DropdownMenu } from 'reactstrap';
|
import { DropdownItem, DropdownMenu } from 'reactstrap';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
|
|
||||||
function DepartmentNodeMenu({ node, toggleDelete, toggleRename, toggleAddMembers, toggleAddDepartment, toggleAddLibrary, toggleSetQuotaDialog }) {
|
function DepartmentNodeMenu({ node, toggleDelete, toggleRename, toggleAddMembers, toggleAddDepartment, toggleAddLibrary, toggleSetQuotaDialog, toggleMoveDepartment }) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
modifiers={[{ name: 'preventOverflow', options: { boundary: document.body } }]}
|
modifiers={[{ name: 'preventOverflow', options: { boundary: document.body } }]}
|
||||||
@@ -24,6 +24,9 @@ function DepartmentNodeMenu({ node, toggleDelete, toggleRename, toggleAddMembers
|
|||||||
<DropdownItem key={`${node.id}-delete`} onClick={() => toggleDelete(node)}>
|
<DropdownItem key={`${node.id}-delete`} onClick={() => toggleDelete(node)}>
|
||||||
{gettext('Delete')}
|
{gettext('Delete')}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<DropdownItem key={`${node.id}-move`} onClick={() => toggleMoveDepartment(node)}>
|
||||||
|
{gettext('Move department')}
|
||||||
|
</DropdownItem>
|
||||||
<DropdownItem key={`${node.id}-set-quota`} onClick={() => toggleSetQuotaDialog(node)}>
|
<DropdownItem key={`${node.id}-set-quota`} onClick={() => toggleSetQuotaDialog(node)}>
|
||||||
{gettext('Set quota')}
|
{gettext('Set quota')}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -42,6 +45,7 @@ DepartmentNodeMenu.propTypes = {
|
|||||||
toggleAddDepartment: PropTypes.func.isRequired,
|
toggleAddDepartment: PropTypes.func.isRequired,
|
||||||
toggleAddLibrary: PropTypes.func.isRequired,
|
toggleAddLibrary: PropTypes.func.isRequired,
|
||||||
toggleSetQuotaDialog: PropTypes.func.isRequired,
|
toggleSetQuotaDialog: PropTypes.func.isRequired,
|
||||||
|
toggleMoveDepartment: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DepartmentNodeMenu;
|
export default DepartmentNodeMenu;
|
||||||
|
@@ -13,7 +13,8 @@ const DepartmentsTreePanelPropTypes = {
|
|||||||
toggleAddLibrary: PropTypes.func,
|
toggleAddLibrary: PropTypes.func,
|
||||||
toggleAddMembers: PropTypes.func,
|
toggleAddMembers: PropTypes.func,
|
||||||
toggleRename: PropTypes.func,
|
toggleRename: PropTypes.func,
|
||||||
toggleDelete: PropTypes.func
|
toggleDelete: PropTypes.func,
|
||||||
|
toggleMoveDepartment: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
class DepartmentsTreePanel extends Component {
|
class DepartmentsTreePanel extends Component {
|
||||||
@@ -35,6 +36,7 @@ class DepartmentsTreePanel extends Component {
|
|||||||
toggleAddMembers={this.props.toggleAddMembers}
|
toggleAddMembers={this.props.toggleAddMembers}
|
||||||
toggleRename={this.props.toggleRename}
|
toggleRename={this.props.toggleRename}
|
||||||
toggleDelete={this.props.toggleDelete}
|
toggleDelete={this.props.toggleDelete}
|
||||||
|
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -7,6 +7,7 @@ import toaster from '../../../components/toast';
|
|||||||
import SetGroupQuotaDialog from '../../../components/dialog/org-set-group-quota-dialog';
|
import SetGroupQuotaDialog from '../../../components/dialog/org-set-group-quota-dialog';
|
||||||
import AddDepartmentDialog from '../../../components/dialog/sysadmin-dialog/add-department-v2-dialog';
|
import AddDepartmentDialog from '../../../components/dialog/sysadmin-dialog/add-department-v2-dialog';
|
||||||
import AddDepartMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-depart-member-v2-dialog';
|
import AddDepartMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-depart-member-v2-dialog';
|
||||||
|
import MoveDepartmentDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-move-group-dialog';
|
||||||
import RenameDepartmentDialog from '../../../components/dialog/sysadmin-dialog/rename-department-v2-dialog';
|
import RenameDepartmentDialog from '../../../components/dialog/sysadmin-dialog/rename-department-v2-dialog';
|
||||||
import DeleteDepartmentConfirmDialog from '../../../components/dialog/sysadmin-dialog/delete-department-v2-confirm-dialog';
|
import DeleteDepartmentConfirmDialog from '../../../components/dialog/sysadmin-dialog/delete-department-v2-confirm-dialog';
|
||||||
import AddRepoDialog from '../../../components/dialog/org-add-repo-dialog';
|
import AddRepoDialog from '../../../components/dialog/org-add-repo-dialog';
|
||||||
@@ -28,6 +29,7 @@ class Departments extends React.Component {
|
|||||||
operateNode: null,
|
operateNode: null,
|
||||||
isAddDepartmentDialogShow: false,
|
isAddDepartmentDialogShow: false,
|
||||||
isAddMembersDialogShow: false,
|
isAddMembersDialogShow: false,
|
||||||
|
isMoveDeparmentDialogShow: false,
|
||||||
isRenameDepartmentDialogShow: false,
|
isRenameDepartmentDialogShow: false,
|
||||||
isDeleteDepartmentDialogShow: false,
|
isDeleteDepartmentDialogShow: false,
|
||||||
isShowAddRepoDialog: false,
|
isShowAddRepoDialog: false,
|
||||||
@@ -143,6 +145,10 @@ class Departments extends React.Component {
|
|||||||
this.setState({ operateNode: node, isAddMembersDialogShow: !this.state.isAddMembersDialogShow });
|
this.setState({ operateNode: node, isAddMembersDialogShow: !this.state.isAddMembersDialogShow });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleMoveDepartment = (node) => {
|
||||||
|
this.setState({ operateNode: node, isMoveDeparmentDialogShow: !this.state.isMoveDeparmentDialogShow });
|
||||||
|
};
|
||||||
|
|
||||||
toggleRename = (node) => {
|
toggleRename = (node) => {
|
||||||
this.setState({ operateNode: node, isRenameDepartmentDialogShow: !this.state.isRenameDepartmentDialogShow });
|
this.setState({ operateNode: node, isRenameDepartmentDialogShow: !this.state.isRenameDepartmentDialogShow });
|
||||||
};
|
};
|
||||||
@@ -180,6 +186,62 @@ class Departments extends React.Component {
|
|||||||
node.name = department.name;
|
node.name = department.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onDepartmentChanged = (targetDepartment) => {
|
||||||
|
const { operateNode, rootNodes } = this.state;
|
||||||
|
|
||||||
|
// Remove from original parent but keep the node reference
|
||||||
|
let nodeToMove = operateNode;
|
||||||
|
if (operateNode.parentNode) {
|
||||||
|
operateNode.parentNode.deleteChildById(operateNode.id);
|
||||||
|
} else {
|
||||||
|
const index = rootNodes.indexOf(operateNode);
|
||||||
|
if (index !== -1) {
|
||||||
|
rootNodes.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetDepartment) {
|
||||||
|
// Find existing target node in tree
|
||||||
|
let existingTargetNode = null;
|
||||||
|
const findTargetNode = (nodes) => {
|
||||||
|
for (let n of nodes) {
|
||||||
|
if (n.id === targetDepartment.id) {
|
||||||
|
existingTargetNode = n;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n.children.length > 0) {
|
||||||
|
findTargetNode(n.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
findTargetNode(rootNodes);
|
||||||
|
|
||||||
|
if (existingTargetNode) {
|
||||||
|
// Update node's parent and add to target's children
|
||||||
|
nodeToMove.parentNode = existingTargetNode;
|
||||||
|
const isAlreadyChild = existingTargetNode.children.some(child => child.id === nodeToMove.id);
|
||||||
|
if (!isAlreadyChild) {
|
||||||
|
existingTargetNode.addChildren([nodeToMove]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If targetDepartment is null, it becomes a root node
|
||||||
|
nodeToMove.parentNode = null;
|
||||||
|
const isAlreadyRoot = rootNodes.some(node => node.id === nodeToMove.id);
|
||||||
|
if (!isAlreadyRoot) {
|
||||||
|
this.setState({
|
||||||
|
rootNodes: [...rootNodes, nodeToMove]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update checked department if needed
|
||||||
|
const { checkedDepartmentId } = this.state;
|
||||||
|
if (checkedDepartmentId === nodeToMove.id) {
|
||||||
|
this.onChangeDepartment(nodeToMove.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onDelete = () => {
|
onDelete = () => {
|
||||||
const { operateNode, checkedDepartmentId } = this.state;
|
const { operateNode, checkedDepartmentId } = this.state;
|
||||||
orgAdminAPI.orgAdminDeleteDepartGroup(orgID, operateNode.id).then((res) => {
|
orgAdminAPI.orgAdminDeleteDepartGroup(orgID, operateNode.id).then((res) => {
|
||||||
@@ -315,7 +377,7 @@ class Departments extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { rootNodes, operateNode, checkedDepartmentId, isAddDepartmentDialogShow, isAddMembersDialogShow,
|
const { rootNodes, operateNode, checkedDepartmentId, isAddDepartmentDialogShow, isAddMembersDialogShow,
|
||||||
membersList, isMembersListLoading, isTopDepartmentLoading, isRenameDepartmentDialogShow,
|
membersList, isMembersListLoading, isTopDepartmentLoading, isRenameDepartmentDialogShow,
|
||||||
isDeleteDepartmentDialogShow, sortBy, sortOrder } = this.state;
|
isDeleteDepartmentDialogShow, sortBy, sortOrder, isMoveDeparmentDialogShow } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<MainPanelTopbar />
|
<MainPanelTopbar />
|
||||||
@@ -339,6 +401,7 @@ class Departments extends React.Component {
|
|||||||
toggleAddMembers={this.toggleAddMembers}
|
toggleAddMembers={this.toggleAddMembers}
|
||||||
toggleRename={this.toggleRename}
|
toggleRename={this.toggleRename}
|
||||||
toggleDelete={this.toggleDelete}
|
toggleDelete={this.toggleDelete}
|
||||||
|
toggleMoveDepartment={this.toggleMoveDepartment}
|
||||||
/>
|
/>
|
||||||
<Department
|
<Department
|
||||||
rootNodes={rootNodes}
|
rootNodes={rootNodes}
|
||||||
@@ -358,6 +421,7 @@ class Departments extends React.Component {
|
|||||||
toggleSetQuotaDialog={this.toggleSetQuotaDialog}
|
toggleSetQuotaDialog={this.toggleSetQuotaDialog}
|
||||||
toggleAddLibrary={this.toggleAddLibrary}
|
toggleAddLibrary={this.toggleAddLibrary}
|
||||||
toggleAddMembers={this.toggleAddMembers}
|
toggleAddMembers={this.toggleAddMembers}
|
||||||
|
toggleMoveDepartment={this.toggleMoveDepartment}
|
||||||
toggleRename={this.toggleRename}
|
toggleRename={this.toggleRename}
|
||||||
toggleDelete={this.toggleDelete}
|
toggleDelete={this.toggleDelete}
|
||||||
/>
|
/>
|
||||||
@@ -381,6 +445,14 @@ class Departments extends React.Component {
|
|||||||
onMemberChanged={this.onMemberChanged}
|
onMemberChanged={this.onMemberChanged}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{isMoveDeparmentDialogShow &&
|
||||||
|
<MoveDepartmentDialog
|
||||||
|
orgID={orgID}
|
||||||
|
toggle={this.toggleMoveDepartment}
|
||||||
|
nodeId={operateNode.id}
|
||||||
|
onDepartmentChanged={this.onDepartmentChanged}
|
||||||
|
/>
|
||||||
|
}
|
||||||
{isRenameDepartmentDialogShow &&
|
{isRenameDepartmentDialogShow &&
|
||||||
<RenameDepartmentDialog
|
<RenameDepartmentDialog
|
||||||
orgID={orgID}
|
orgID={orgID}
|
||||||
|
@@ -87,6 +87,7 @@ class DepartmentsV2TreeNode extends Component {
|
|||||||
toggleDelete={this.props.toggleDelete}
|
toggleDelete={this.props.toggleDelete}
|
||||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||||
|
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -156,6 +157,7 @@ class DepartmentsV2TreeNode extends Component {
|
|||||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||||
toggleRename={this.props.toggleRename}
|
toggleRename={this.props.toggleRename}
|
||||||
toggleDelete={this.props.toggleDelete}
|
toggleDelete={this.props.toggleDelete}
|
||||||
|
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
}
|
}
|
||||||
|
@@ -153,6 +153,7 @@ class Department extends React.Component {
|
|||||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||||
toggleAddMembers={this.props.toggleAddMembers}
|
toggleAddMembers={this.props.toggleAddMembers}
|
||||||
|
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||||
toggleRename={this.props.toggleRename}
|
toggleRename={this.props.toggleRename}
|
||||||
toggleDelete={this.props.toggleDelete}
|
toggleDelete={this.props.toggleDelete}
|
||||||
/>
|
/>
|
||||||
|
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { DropdownItem, DropdownMenu } from 'reactstrap';
|
import { DropdownItem, DropdownMenu } from 'reactstrap';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
|
|
||||||
function DepartmentNodeMenu({ node, toggleDelete, toggleRename, toggleAddMembers, toggleAddDepartment, toggleAddLibrary, toggleSetQuotaDialog }) {
|
function DepartmentNodeMenu({ node, toggleDelete, toggleRename, toggleAddMembers, toggleAddDepartment, toggleAddLibrary, toggleSetQuotaDialog, toggleMoveDepartment }) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
modifiers={[{ name: 'preventOverflow', options: { boundary: document.body } }]}
|
modifiers={[{ name: 'preventOverflow', options: { boundary: document.body } }]}
|
||||||
@@ -24,6 +24,9 @@ function DepartmentNodeMenu({ node, toggleDelete, toggleRename, toggleAddMembers
|
|||||||
<DropdownItem key={`${node.id}-delete`} onClick={() => toggleDelete(node)}>
|
<DropdownItem key={`${node.id}-delete`} onClick={() => toggleDelete(node)}>
|
||||||
{gettext('Delete')}
|
{gettext('Delete')}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<DropdownItem key={`${node.id}-move`} onClick={() => toggleMoveDepartment(node)}>
|
||||||
|
{gettext('Move department')}
|
||||||
|
</DropdownItem>
|
||||||
<DropdownItem key={`${node.id}-set-quota`} onClick={() => toggleSetQuotaDialog(node)}>
|
<DropdownItem key={`${node.id}-set-quota`} onClick={() => toggleSetQuotaDialog(node)}>
|
||||||
{gettext('Set quota')}
|
{gettext('Set quota')}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -42,6 +45,7 @@ DepartmentNodeMenu.propTypes = {
|
|||||||
toggleAddDepartment: PropTypes.func.isRequired,
|
toggleAddDepartment: PropTypes.func.isRequired,
|
||||||
toggleAddLibrary: PropTypes.func.isRequired,
|
toggleAddLibrary: PropTypes.func.isRequired,
|
||||||
toggleSetQuotaDialog: PropTypes.func.isRequired,
|
toggleSetQuotaDialog: PropTypes.func.isRequired,
|
||||||
|
toggleMoveDepartment: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DepartmentNodeMenu;
|
export default DepartmentNodeMenu;
|
||||||
|
@@ -13,7 +13,8 @@ const DepartmentsTreePanelPropTypes = {
|
|||||||
toggleAddLibrary: PropTypes.func,
|
toggleAddLibrary: PropTypes.func,
|
||||||
toggleAddMembers: PropTypes.func,
|
toggleAddMembers: PropTypes.func,
|
||||||
toggleRename: PropTypes.func,
|
toggleRename: PropTypes.func,
|
||||||
toggleDelete: PropTypes.func
|
toggleDelete: PropTypes.func,
|
||||||
|
toggleMoveDepartment: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
class DepartmentsTreePanel extends Component {
|
class DepartmentsTreePanel extends Component {
|
||||||
@@ -33,6 +34,7 @@ class DepartmentsTreePanel extends Component {
|
|||||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||||
toggleAddMembers={this.props.toggleAddMembers}
|
toggleAddMembers={this.props.toggleAddMembers}
|
||||||
|
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||||
toggleRename={this.props.toggleRename}
|
toggleRename={this.props.toggleRename}
|
||||||
toggleDelete={this.props.toggleDelete}
|
toggleDelete={this.props.toggleDelete}
|
||||||
/>
|
/>
|
||||||
|
@@ -10,6 +10,7 @@ import AddDepartmentV2Dialog from '../../../components/dialog/sysadmin-dialog/ad
|
|||||||
import AddDepartMemberV2Dialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-depart-member-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 RenameDepartmentV2Dialog from '../../../components/dialog/sysadmin-dialog/rename-department-v2-dialog';
|
||||||
import DeleteDepartmentV2ConfirmDialog from '../../../components/dialog/sysadmin-dialog/delete-department-v2-confirm-dialog';
|
import DeleteDepartmentV2ConfirmDialog from '../../../components/dialog/sysadmin-dialog/delete-department-v2-confirm-dialog';
|
||||||
|
import MoveDepartmentDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-move-group-dialog';
|
||||||
import AddRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-repo-dialog';
|
import AddRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-repo-dialog';
|
||||||
import DepartmentNode from './department-node';
|
import DepartmentNode from './department-node';
|
||||||
import DepartmentsTreePanel from './departments-tree-panel';
|
import DepartmentsTreePanel from './departments-tree-panel';
|
||||||
@@ -32,6 +33,7 @@ class Departments extends React.Component {
|
|||||||
isRenameDepartmentDialogShow: false,
|
isRenameDepartmentDialogShow: false,
|
||||||
isDeleteDepartmentDialogShow: false,
|
isDeleteDepartmentDialogShow: false,
|
||||||
isShowAddRepoDialog: false,
|
isShowAddRepoDialog: false,
|
||||||
|
isMoveDeparmentDialogShow: false,
|
||||||
membersList: [],
|
membersList: [],
|
||||||
isTopDepartmentLoading: false,
|
isTopDepartmentLoading: false,
|
||||||
isMembersListLoading: false,
|
isMembersListLoading: false,
|
||||||
@@ -188,6 +190,10 @@ class Departments extends React.Component {
|
|||||||
this.setState({ operateNode: node, isDeleteDepartmentDialogShow: !this.state.isDeleteDepartmentDialogShow });
|
this.setState({ operateNode: node, isDeleteDepartmentDialogShow: !this.state.isDeleteDepartmentDialogShow });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleMoveDepartment = (node) => {
|
||||||
|
this.setState({ operateNode: node, isMoveDeparmentDialogShow: !this.state.isMoveDeparmentDialogShow });
|
||||||
|
};
|
||||||
|
|
||||||
addDepartment = (parentNode, department) => {
|
addDepartment = (parentNode, department) => {
|
||||||
parentNode.addChildren([new DepartmentNode({
|
parentNode.addChildren([new DepartmentNode({
|
||||||
id: department.id,
|
id: department.id,
|
||||||
@@ -217,6 +223,57 @@ class Departments extends React.Component {
|
|||||||
node.name = department.name;
|
node.name = department.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onDepartmentChanged = (targetDepartment) => {
|
||||||
|
const { operateNode, rootNodes } = this.state;
|
||||||
|
|
||||||
|
let nodeToMove = operateNode;
|
||||||
|
if (operateNode.parentNode) {
|
||||||
|
operateNode.parentNode.deleteChildById(operateNode.id);
|
||||||
|
} else {
|
||||||
|
const index = rootNodes.indexOf(operateNode);
|
||||||
|
if (index !== -1) {
|
||||||
|
rootNodes.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetDepartment) {
|
||||||
|
let existingTargetNode = null;
|
||||||
|
const findTargetNode = (nodes) => {
|
||||||
|
for (let n of nodes) {
|
||||||
|
if (n.id === targetDepartment.id) {
|
||||||
|
existingTargetNode = n;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n.children.length > 0) {
|
||||||
|
findTargetNode(n.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
findTargetNode(rootNodes);
|
||||||
|
|
||||||
|
if (existingTargetNode) {
|
||||||
|
nodeToMove.parentNode = existingTargetNode;
|
||||||
|
const isAlreadyChild = existingTargetNode.children.some(child => child.id === nodeToMove.id);
|
||||||
|
if (!isAlreadyChild) {
|
||||||
|
existingTargetNode.addChildren([nodeToMove]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nodeToMove.parentNode = null;
|
||||||
|
const isAlreadyRoot = rootNodes.some(node => node.id === nodeToMove.id);
|
||||||
|
if (!isAlreadyRoot) {
|
||||||
|
this.setState({
|
||||||
|
rootNodes: [...rootNodes, nodeToMove]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { checkedDepartmentId } = this.state;
|
||||||
|
if (checkedDepartmentId === nodeToMove.id) {
|
||||||
|
this.onChangeDepartment(nodeToMove.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onDelete = () => {
|
onDelete = () => {
|
||||||
const { operateNode, checkedDepartmentId } = this.state;
|
const { operateNode, checkedDepartmentId } = this.state;
|
||||||
systemAdminAPI.sysAdminDeleteDepartment(operateNode.id).then(() => {
|
systemAdminAPI.sysAdminDeleteDepartment(operateNode.id).then(() => {
|
||||||
@@ -348,7 +405,7 @@ class Departments extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { rootNodes, operateNode, checkedDepartmentId, isAddDepartmentDialogShow, isAddMembersDialogShow,
|
const { rootNodes, operateNode, checkedDepartmentId, isAddDepartmentDialogShow, isAddMembersDialogShow,
|
||||||
membersList, isMembersListLoading, isTopDepartmentLoading, isRenameDepartmentDialogShow,
|
membersList, isMembersListLoading, isTopDepartmentLoading, isRenameDepartmentDialogShow,
|
||||||
isDeleteDepartmentDialogShow, sortBy, sortOrder } = this.state;
|
isDeleteDepartmentDialogShow, sortBy, sortOrder, isMoveDeparmentDialogShow } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<MainPanelTopbar {...this.props} />
|
<MainPanelTopbar {...this.props} />
|
||||||
@@ -372,6 +429,7 @@ class Departments extends React.Component {
|
|||||||
toggleAddMembers={this.toggleAddMembers}
|
toggleAddMembers={this.toggleAddMembers}
|
||||||
toggleRename={this.toggleRename}
|
toggleRename={this.toggleRename}
|
||||||
toggleDelete={this.toggleDelete}
|
toggleDelete={this.toggleDelete}
|
||||||
|
toggleMoveDepartment={this.toggleMoveDepartment}
|
||||||
/>
|
/>
|
||||||
<Department
|
<Department
|
||||||
rootNodes={rootNodes}
|
rootNodes={rootNodes}
|
||||||
@@ -398,6 +456,7 @@ class Departments extends React.Component {
|
|||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
currentPageInfo={this.state.currentPageInfo}
|
currentPageInfo={this.state.currentPageInfo}
|
||||||
perPage={this.state.perPage}
|
perPage={this.state.perPage}
|
||||||
|
toggleMoveDepartment={this.toggleMoveDepartment}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -418,6 +477,13 @@ class Departments extends React.Component {
|
|||||||
onMemberChanged={this.onMemberChanged}
|
onMemberChanged={this.onMemberChanged}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{isMoveDeparmentDialogShow &&
|
||||||
|
<MoveDepartmentDialog
|
||||||
|
toggle={this.toggleMoveDepartment}
|
||||||
|
nodeId={operateNode.id}
|
||||||
|
onDepartmentChanged={this.onDepartmentChanged}
|
||||||
|
/>
|
||||||
|
}
|
||||||
{isRenameDepartmentDialogShow &&
|
{isRenameDepartmentDialogShow &&
|
||||||
<RenameDepartmentV2Dialog
|
<RenameDepartmentV2Dialog
|
||||||
node={operateNode}
|
node={operateNode}
|
||||||
|
@@ -15,7 +15,8 @@ const departmentsTreeNodePropTypes = {
|
|||||||
toggleAddLibrary: PropTypes.func,
|
toggleAddLibrary: PropTypes.func,
|
||||||
toggleAddMembers: PropTypes.func,
|
toggleAddMembers: PropTypes.func,
|
||||||
toggleRename: PropTypes.func,
|
toggleRename: PropTypes.func,
|
||||||
toggleDelete: PropTypes.func
|
toggleDelete: PropTypes.func,
|
||||||
|
toggleMoveDepartment: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
class DepartmentsTreeNode extends Component {
|
class DepartmentsTreeNode extends Component {
|
||||||
@@ -87,6 +88,7 @@ class DepartmentsTreeNode extends Component {
|
|||||||
toggleRename={this.props.toggleRename}
|
toggleRename={this.props.toggleRename}
|
||||||
toggleDelete={this.props.toggleDelete}
|
toggleDelete={this.props.toggleDelete}
|
||||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||||
|
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -154,6 +156,7 @@ class DepartmentsTreeNode extends Component {
|
|||||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||||
toggleAddMembers={this.props.toggleAddMembers}
|
toggleAddMembers={this.props.toggleAddMembers}
|
||||||
|
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||||
toggleRename={this.props.toggleRename}
|
toggleRename={this.props.toggleRename}
|
||||||
toggleDelete={this.props.toggleDelete}
|
toggleDelete={this.props.toggleDelete}
|
||||||
/>
|
/>
|
||||||
|
@@ -425,6 +425,13 @@ class OrgAdminAPI {
|
|||||||
return this.req.put(url, form);
|
return this.req.put(url, form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orgAdminMoveDepartment(orgID, groupID, targetDepartmentId) {
|
||||||
|
const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/move-department/';
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('target_group_id', targetDepartmentId);
|
||||||
|
return this.req.put(url, form);
|
||||||
|
}
|
||||||
|
|
||||||
orgAdminDeleteDepartGroup(orgID, groupID) {
|
orgAdminDeleteDepartGroup(orgID, groupID) {
|
||||||
const url = this.server + '/api/v2.1/org/' + orgID + '/admin/address-book/groups/' + groupID + '/';
|
const url = this.server + '/api/v2.1/org/' + orgID + '/admin/address-book/groups/' + groupID + '/';
|
||||||
return this.req.delete(url);
|
return this.req.delete(url);
|
||||||
|
@@ -522,6 +522,13 @@ class SystemAdminAPI {
|
|||||||
return this.req.put(url, formData);
|
return this.req.put(url, formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sysAdminMoveDepartment(groupID, targetGroupID) {
|
||||||
|
const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/';
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append('target_group_id', targetGroupID);
|
||||||
|
return this.req.put(url, formData);
|
||||||
|
}
|
||||||
|
|
||||||
sysAdminDeleteDepartment(groupID) {
|
sysAdminDeleteDepartment(groupID) {
|
||||||
const url = this.server + '/api/v2.1/admin/address-book/groups/' + groupID + '/';
|
const url = this.server + '/api/v2.1/admin/address-book/groups/' + groupID + '/';
|
||||||
return this.req.delete(url);
|
return this.req.delete(url);
|
||||||
|
@@ -188,6 +188,7 @@ class AdminGroup(APIView):
|
|||||||
1. transfer a group.
|
1. transfer a group.
|
||||||
2. set group quota.
|
2. set group quota.
|
||||||
3. rename group.
|
3. rename group.
|
||||||
|
4. move a group to another group.
|
||||||
|
|
||||||
Permission checking:
|
Permission checking:
|
||||||
1. Admin user;
|
1. Admin user;
|
||||||
@@ -286,6 +287,56 @@ class AdminGroup(APIView):
|
|||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
# move a group to another group
|
||||||
|
target_group_id = request.data.get('target_group_id')
|
||||||
|
if target_group_id:
|
||||||
|
try:
|
||||||
|
target_group_id = int(target_group_id)
|
||||||
|
except ValueError:
|
||||||
|
error_msg = 'target_group_id invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if group.creator_name != 'system admin':
|
||||||
|
error_msg = 'Group %s is not a department' % group_id
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
target_group = ccnet_api.get_group(target_group_id)
|
||||||
|
if not target_group:
|
||||||
|
error_msg = 'Target group %d not found.' % target_group_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if target_group.creator_name != 'system admin':
|
||||||
|
error_msg = 'Group %s is not a department' % target_group_id
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if group.parent_group_id == target_group_id or group_id == target_group_id:
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
|
is_org = ccnet_api.is_org_group(group_id)
|
||||||
|
if is_org:
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ccnet_db = CcnetDB()
|
||||||
|
sub_groups = ccnet_db.get_all_sub_groups(group_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
if target_group_id in sub_groups:
|
||||||
|
error_msg = 'Cannot move to its own sub department.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ccnet_db.move_department(group_id, target_group_id)
|
||||||
|
return Response({'success': True})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
group_info = get_group_info(group_id)
|
group_info = get_group_info(group_id)
|
||||||
return Response(group_info)
|
return Response(group_info)
|
||||||
|
|
||||||
@@ -368,6 +419,9 @@ class AdminDepartments(APIView):
|
|||||||
throttle_classes = (UserRateThrottle,)
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
if not request.user.admin_permissions.can_manage_group():
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
all_groups = ccnet_api.list_all_departments()
|
all_groups = ccnet_api.list_all_departments()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -375,12 +429,6 @@ class AdminDepartments(APIView):
|
|||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
try:
|
|
||||||
avatar_size = int(request.GET.get('avatar_size', GROUP_AVATAR_DEFAULT_SIZE))
|
|
||||||
except ValueError:
|
|
||||||
avatar_size = GROUP_AVATAR_DEFAULT_SIZE
|
|
||||||
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for group in all_groups:
|
for group in all_groups:
|
||||||
created_at = timestamp_to_isoformat_timestr(group.timestamp)
|
created_at = timestamp_to_isoformat_timestr(group.timestamp)
|
||||||
|
@@ -226,10 +226,6 @@ class OrgAdminDepartments(APIView):
|
|||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
try:
|
|
||||||
avatar_size = int(request.GET.get('avatar_size', GROUP_AVATAR_DEFAULT_SIZE))
|
|
||||||
except ValueError:
|
|
||||||
avatar_size = GROUP_AVATAR_DEFAULT_SIZE
|
|
||||||
result = []
|
result = []
|
||||||
for group in departments:
|
for group in departments:
|
||||||
created_at = timestamp_to_isoformat_timestr(group.timestamp)
|
created_at = timestamp_to_isoformat_timestr(group.timestamp)
|
||||||
@@ -289,3 +285,74 @@ class OrgAdminGroupToDeptView(APIView):
|
|||||||
'creator_contact_email': email2contact_email(group.creator_name),
|
'creator_contact_email': email2contact_email(group.creator_name),
|
||||||
}
|
}
|
||||||
return Response(group_info)
|
return Response(group_info)
|
||||||
|
|
||||||
|
|
||||||
|
class OrgAdminMoveDepartment(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def put(self, request, org_id, group_id):
|
||||||
|
org_id = int(org_id)
|
||||||
|
if not ccnet_api.get_org_by_id(org_id):
|
||||||
|
error_msg = 'Organization %s not found.' % org_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
# permission check
|
||||||
|
group_id = int(group_id)
|
||||||
|
group = ccnet_api.get_group(group_id)
|
||||||
|
if not group:
|
||||||
|
error_msg = 'Group %s not found.' % group_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if group.creator_name != 'system admin':
|
||||||
|
error_msg = 'Group %s is not a department' % group_id
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if get_org_id_by_group(group_id) != org_id:
|
||||||
|
error_msg = 'Group %s not found.' % group_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
target_group_id = request.data.get('target_group_id')
|
||||||
|
try:
|
||||||
|
target_group_id = int(target_group_id)
|
||||||
|
except:
|
||||||
|
error_msg = 'target_group_id %s invalid' % target_group_id
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
target_group = ccnet_api.get_group(target_group_id)
|
||||||
|
if not target_group:
|
||||||
|
error_msg = 'Group %s not found.' % target_group_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if target_group.creator_name != 'system admin':
|
||||||
|
error_msg = 'Group %s is not a department' % target_group_id
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if get_org_id_by_group(target_group_id) != org_id:
|
||||||
|
error_msg = 'Group %s not found.' % target_group_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if group.parent_group_id == target_group_id or group_id == target_group_id:
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
|
try:
|
||||||
|
ccnet_db = CcnetDB()
|
||||||
|
sub_groups = ccnet_db.get_all_sub_groups(group_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
if target_group_id in sub_groups:
|
||||||
|
error_msg = 'Cannot move to its own sub department.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ccnet_db.move_department(group_id, target_group_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
return Response({'success': True})
|
||||||
|
@@ -14,7 +14,7 @@ from .api.admin.users import OrgAdminUser, OrgAdminUsers, OrgAdminSearchUser, \
|
|||||||
OrgAdminImportUsers, OrgAdminInviteUser
|
OrgAdminImportUsers, OrgAdminInviteUser
|
||||||
from .api.admin.user_set_password import OrgAdminUserSetPassword
|
from .api.admin.user_set_password import OrgAdminUserSetPassword
|
||||||
from .api.admin.groups import OrgAdminGroups, OrgAdminGroup, OrgAdminSearchGroup, \
|
from .api.admin.groups import OrgAdminGroups, OrgAdminGroup, OrgAdminSearchGroup, \
|
||||||
OrgAdminDepartments, OrgAdminGroupToDeptView
|
OrgAdminDepartments, OrgAdminGroupToDeptView, OrgAdminMoveDepartment
|
||||||
from .api.admin.repos import OrgAdminRepos, OrgAdminRepo
|
from .api.admin.repos import OrgAdminRepos, OrgAdminRepo
|
||||||
from .api.admin.trash_libraries import OrgAdminTrashLibraries, OrgAdminTrashLibrary
|
from .api.admin.trash_libraries import OrgAdminTrashLibraries, OrgAdminTrashLibrary
|
||||||
from .api.admin.info import OrgAdminInfo
|
from .api.admin.info import OrgAdminInfo
|
||||||
@@ -77,6 +77,7 @@ urlpatterns = [
|
|||||||
path('<int:org_id>/admin/groups/<int:group_id>/', OrgAdminGroup.as_view(), name='api-admin-group'),
|
path('<int:org_id>/admin/groups/<int:group_id>/', OrgAdminGroup.as_view(), name='api-admin-group'),
|
||||||
path('<int:org_id>/admin/groups/<int:group_id>/libraries/', AdminGroupLibraries.as_view(), name='api-admin-group-libraries'),
|
path('<int:org_id>/admin/groups/<int:group_id>/libraries/', AdminGroupLibraries.as_view(), name='api-admin-group-libraries'),
|
||||||
path('<int:org_id>/admin/groups/<int:group_id>/group-to-department/', OrgAdminGroupToDeptView.as_view(), name='api-admin-group-to-department'),
|
path('<int:org_id>/admin/groups/<int:group_id>/group-to-department/', OrgAdminGroupToDeptView.as_view(), name='api-admin-group-to-department'),
|
||||||
|
path('<int:org_id>/admin/groups/<int:group_id>/move-department/', OrgAdminMoveDepartment.as_view(), name='api-admin-move-department'),
|
||||||
re_path(r'^(?P<org_id>\d+)/admin/groups/(?P<group_id>\d+)/libraries/(?P<repo_id>[-0-9a-f]{36})/$', AdminGroupLibrary.as_view(), name='api-admin-group-library'),
|
re_path(r'^(?P<org_id>\d+)/admin/groups/(?P<group_id>\d+)/libraries/(?P<repo_id>[-0-9a-f]{36})/$', AdminGroupLibrary.as_view(), name='api-admin-group-library'),
|
||||||
|
|
||||||
path('<int:org_id>/admin/groups/<int:group_id>/group-owned-libraries/', AdminGroupOwnedLibraries.as_view(), name='api-admin-group-owned-libraries'),
|
path('<int:org_id>/admin/groups/<int:group_id>/group-owned-libraries/', AdminGroupOwnedLibraries.as_view(), name='api-admin-group-owned-libraries'),
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
from django.db import connection
|
from django.db import connection, transaction
|
||||||
|
|
||||||
|
|
||||||
def get_ccnet_db_name():
|
def get_ccnet_db_name():
|
||||||
@@ -256,3 +256,62 @@ class CcnetDB:
|
|||||||
staffs = cursor.fetchall()
|
staffs = cursor.fetchall()
|
||||||
|
|
||||||
return [s[0] for s in staffs]
|
return [s[0] for s in staffs]
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_sub_groups(self, group_id):
|
||||||
|
sql = f"""
|
||||||
|
SELECT group_id
|
||||||
|
FROM `{self.db_name}`.`GroupStructure`
|
||||||
|
WHERE path LIKE %s
|
||||||
|
"""
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(sql, [f'%{group_id}%'])
|
||||||
|
sub_groups = cursor.fetchall()
|
||||||
|
return [s[0] for s in sub_groups]
|
||||||
|
|
||||||
|
def move_department(self, department_id, target_department_id):
|
||||||
|
get_current_path_sql = f"""
|
||||||
|
SELECT path
|
||||||
|
FROM `{self.db_name}`.`GroupStructure`
|
||||||
|
WHERE group_id = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
update_group_sql = f"""
|
||||||
|
UPDATE `{self.db_name}`.`Group`
|
||||||
|
SET parent_group_id = %s
|
||||||
|
WHERE group_id = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
update_structure_sql = f"""
|
||||||
|
UPDATE `{self.db_name}`.`GroupStructure`
|
||||||
|
SET path = CONCAT(%s, SUBSTRING(path, LENGTH(%s) + 1))
|
||||||
|
WHERE path LIKE %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# Get target department's path
|
||||||
|
cursor.execute(get_current_path_sql, [target_department_id])
|
||||||
|
target_path_result = cursor.fetchone()
|
||||||
|
target_path = target_path_result[0] if target_path_result else str(target_department_id)
|
||||||
|
|
||||||
|
# Get current department's path
|
||||||
|
cursor.execute(get_current_path_sql, [department_id])
|
||||||
|
current_path_result = cursor.fetchone()
|
||||||
|
current_path = current_path_result[0] if current_path_result else str(department_id)
|
||||||
|
|
||||||
|
# Update parent in Group table
|
||||||
|
cursor.execute(update_group_sql, [target_department_id, department_id])
|
||||||
|
# Create new path prefix
|
||||||
|
new_path_prefix = f"{target_path}, {department_id}" if target_path else str(department_id)
|
||||||
|
old_path_prefix = current_path
|
||||||
|
|
||||||
|
# Update paths in GroupStructure table
|
||||||
|
cursor.execute(
|
||||||
|
update_structure_sql,
|
||||||
|
[
|
||||||
|
new_path_prefix, # New path prefix
|
||||||
|
old_path_prefix, # Old path prefix to remove
|
||||||
|
f"{old_path_prefix}%" # Pattern to match all children
|
||||||
|
]
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user