mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-26 07:22:34 +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}
|
||||
toggleRename={this.props.toggleRename}
|
||||
toggleDelete={this.props.toggleDelete}
|
||||
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { DropdownItem, DropdownMenu } from 'reactstrap';
|
||||
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 (
|
||||
<DropdownMenu
|
||||
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)}>
|
||||
{gettext('Delete')}
|
||||
</DropdownItem>
|
||||
<DropdownItem key={`${node.id}-move`} onClick={() => toggleMoveDepartment(node)}>
|
||||
{gettext('Move department')}
|
||||
</DropdownItem>
|
||||
<DropdownItem key={`${node.id}-set-quota`} onClick={() => toggleSetQuotaDialog(node)}>
|
||||
{gettext('Set quota')}
|
||||
</DropdownItem>
|
||||
@@ -42,6 +45,7 @@ DepartmentNodeMenu.propTypes = {
|
||||
toggleAddDepartment: PropTypes.func.isRequired,
|
||||
toggleAddLibrary: PropTypes.func.isRequired,
|
||||
toggleSetQuotaDialog: PropTypes.func.isRequired,
|
||||
toggleMoveDepartment: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DepartmentNodeMenu;
|
||||
|
@@ -13,7 +13,8 @@ const DepartmentsTreePanelPropTypes = {
|
||||
toggleAddLibrary: PropTypes.func,
|
||||
toggleAddMembers: PropTypes.func,
|
||||
toggleRename: PropTypes.func,
|
||||
toggleDelete: PropTypes.func
|
||||
toggleDelete: PropTypes.func,
|
||||
toggleMoveDepartment: PropTypes.func,
|
||||
};
|
||||
|
||||
class DepartmentsTreePanel extends Component {
|
||||
@@ -35,6 +36,7 @@ class DepartmentsTreePanel extends Component {
|
||||
toggleAddMembers={this.props.toggleAddMembers}
|
||||
toggleRename={this.props.toggleRename}
|
||||
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 AddDepartmentDialog from '../../../components/dialog/sysadmin-dialog/add-department-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 DeleteDepartmentConfirmDialog from '../../../components/dialog/sysadmin-dialog/delete-department-v2-confirm-dialog';
|
||||
import AddRepoDialog from '../../../components/dialog/org-add-repo-dialog';
|
||||
@@ -28,6 +29,7 @@ class Departments extends React.Component {
|
||||
operateNode: null,
|
||||
isAddDepartmentDialogShow: false,
|
||||
isAddMembersDialogShow: false,
|
||||
isMoveDeparmentDialogShow: false,
|
||||
isRenameDepartmentDialogShow: false,
|
||||
isDeleteDepartmentDialogShow: false,
|
||||
isShowAddRepoDialog: false,
|
||||
@@ -143,6 +145,10 @@ class Departments extends React.Component {
|
||||
this.setState({ operateNode: node, isAddMembersDialogShow: !this.state.isAddMembersDialogShow });
|
||||
};
|
||||
|
||||
toggleMoveDepartment = (node) => {
|
||||
this.setState({ operateNode: node, isMoveDeparmentDialogShow: !this.state.isMoveDeparmentDialogShow });
|
||||
};
|
||||
|
||||
toggleRename = (node) => {
|
||||
this.setState({ operateNode: node, isRenameDepartmentDialogShow: !this.state.isRenameDepartmentDialogShow });
|
||||
};
|
||||
@@ -180,6 +186,62 @@ class Departments extends React.Component {
|
||||
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 = () => {
|
||||
const { operateNode, checkedDepartmentId } = this.state;
|
||||
orgAdminAPI.orgAdminDeleteDepartGroup(orgID, operateNode.id).then((res) => {
|
||||
@@ -315,7 +377,7 @@ class Departments extends React.Component {
|
||||
render() {
|
||||
const { rootNodes, operateNode, checkedDepartmentId, isAddDepartmentDialogShow, isAddMembersDialogShow,
|
||||
membersList, isMembersListLoading, isTopDepartmentLoading, isRenameDepartmentDialogShow,
|
||||
isDeleteDepartmentDialogShow, sortBy, sortOrder } = this.state;
|
||||
isDeleteDepartmentDialogShow, sortBy, sortOrder, isMoveDeparmentDialogShow } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<MainPanelTopbar />
|
||||
@@ -339,6 +401,7 @@ class Departments extends React.Component {
|
||||
toggleAddMembers={this.toggleAddMembers}
|
||||
toggleRename={this.toggleRename}
|
||||
toggleDelete={this.toggleDelete}
|
||||
toggleMoveDepartment={this.toggleMoveDepartment}
|
||||
/>
|
||||
<Department
|
||||
rootNodes={rootNodes}
|
||||
@@ -358,6 +421,7 @@ class Departments extends React.Component {
|
||||
toggleSetQuotaDialog={this.toggleSetQuotaDialog}
|
||||
toggleAddLibrary={this.toggleAddLibrary}
|
||||
toggleAddMembers={this.toggleAddMembers}
|
||||
toggleMoveDepartment={this.toggleMoveDepartment}
|
||||
toggleRename={this.toggleRename}
|
||||
toggleDelete={this.toggleDelete}
|
||||
/>
|
||||
@@ -381,6 +445,14 @@ class Departments extends React.Component {
|
||||
onMemberChanged={this.onMemberChanged}
|
||||
/>
|
||||
}
|
||||
{isMoveDeparmentDialogShow &&
|
||||
<MoveDepartmentDialog
|
||||
orgID={orgID}
|
||||
toggle={this.toggleMoveDepartment}
|
||||
nodeId={operateNode.id}
|
||||
onDepartmentChanged={this.onDepartmentChanged}
|
||||
/>
|
||||
}
|
||||
{isRenameDepartmentDialogShow &&
|
||||
<RenameDepartmentDialog
|
||||
orgID={orgID}
|
||||
|
@@ -87,6 +87,7 @@ class DepartmentsV2TreeNode extends Component {
|
||||
toggleDelete={this.props.toggleDelete}
|
||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -156,6 +157,7 @@ class DepartmentsV2TreeNode extends Component {
|
||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||
toggleRename={this.props.toggleRename}
|
||||
toggleDelete={this.props.toggleDelete}
|
||||
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||
/>
|
||||
</Dropdown>
|
||||
}
|
||||
|
@@ -153,6 +153,7 @@ class Department extends React.Component {
|
||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||
toggleAddMembers={this.props.toggleAddMembers}
|
||||
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||
toggleRename={this.props.toggleRename}
|
||||
toggleDelete={this.props.toggleDelete}
|
||||
/>
|
||||
|
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { DropdownItem, DropdownMenu } from 'reactstrap';
|
||||
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 (
|
||||
<DropdownMenu
|
||||
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)}>
|
||||
{gettext('Delete')}
|
||||
</DropdownItem>
|
||||
<DropdownItem key={`${node.id}-move`} onClick={() => toggleMoveDepartment(node)}>
|
||||
{gettext('Move department')}
|
||||
</DropdownItem>
|
||||
<DropdownItem key={`${node.id}-set-quota`} onClick={() => toggleSetQuotaDialog(node)}>
|
||||
{gettext('Set quota')}
|
||||
</DropdownItem>
|
||||
@@ -42,6 +45,7 @@ DepartmentNodeMenu.propTypes = {
|
||||
toggleAddDepartment: PropTypes.func.isRequired,
|
||||
toggleAddLibrary: PropTypes.func.isRequired,
|
||||
toggleSetQuotaDialog: PropTypes.func.isRequired,
|
||||
toggleMoveDepartment: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DepartmentNodeMenu;
|
||||
|
@@ -13,7 +13,8 @@ const DepartmentsTreePanelPropTypes = {
|
||||
toggleAddLibrary: PropTypes.func,
|
||||
toggleAddMembers: PropTypes.func,
|
||||
toggleRename: PropTypes.func,
|
||||
toggleDelete: PropTypes.func
|
||||
toggleDelete: PropTypes.func,
|
||||
toggleMoveDepartment: PropTypes.func,
|
||||
};
|
||||
|
||||
class DepartmentsTreePanel extends Component {
|
||||
@@ -33,6 +34,7 @@ class DepartmentsTreePanel extends Component {
|
||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||
toggleAddMembers={this.props.toggleAddMembers}
|
||||
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||
toggleRename={this.props.toggleRename}
|
||||
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 RenameDepartmentV2Dialog from '../../../components/dialog/sysadmin-dialog/rename-department-v2-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 DepartmentNode from './department-node';
|
||||
import DepartmentsTreePanel from './departments-tree-panel';
|
||||
@@ -32,6 +33,7 @@ class Departments extends React.Component {
|
||||
isRenameDepartmentDialogShow: false,
|
||||
isDeleteDepartmentDialogShow: false,
|
||||
isShowAddRepoDialog: false,
|
||||
isMoveDeparmentDialogShow: false,
|
||||
membersList: [],
|
||||
isTopDepartmentLoading: false,
|
||||
isMembersListLoading: false,
|
||||
@@ -188,6 +190,10 @@ class Departments extends React.Component {
|
||||
this.setState({ operateNode: node, isDeleteDepartmentDialogShow: !this.state.isDeleteDepartmentDialogShow });
|
||||
};
|
||||
|
||||
toggleMoveDepartment = (node) => {
|
||||
this.setState({ operateNode: node, isMoveDeparmentDialogShow: !this.state.isMoveDeparmentDialogShow });
|
||||
};
|
||||
|
||||
addDepartment = (parentNode, department) => {
|
||||
parentNode.addChildren([new DepartmentNode({
|
||||
id: department.id,
|
||||
@@ -217,6 +223,57 @@ class Departments extends React.Component {
|
||||
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 = () => {
|
||||
const { operateNode, checkedDepartmentId } = this.state;
|
||||
systemAdminAPI.sysAdminDeleteDepartment(operateNode.id).then(() => {
|
||||
@@ -348,7 +405,7 @@ class Departments extends React.Component {
|
||||
render() {
|
||||
const { rootNodes, operateNode, checkedDepartmentId, isAddDepartmentDialogShow, isAddMembersDialogShow,
|
||||
membersList, isMembersListLoading, isTopDepartmentLoading, isRenameDepartmentDialogShow,
|
||||
isDeleteDepartmentDialogShow, sortBy, sortOrder } = this.state;
|
||||
isDeleteDepartmentDialogShow, sortBy, sortOrder, isMoveDeparmentDialogShow } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<MainPanelTopbar {...this.props} />
|
||||
@@ -372,6 +429,7 @@ class Departments extends React.Component {
|
||||
toggleAddMembers={this.toggleAddMembers}
|
||||
toggleRename={this.toggleRename}
|
||||
toggleDelete={this.toggleDelete}
|
||||
toggleMoveDepartment={this.toggleMoveDepartment}
|
||||
/>
|
||||
<Department
|
||||
rootNodes={rootNodes}
|
||||
@@ -398,6 +456,7 @@ class Departments extends React.Component {
|
||||
resetPerPage={this.resetPerPage}
|
||||
currentPageInfo={this.state.currentPageInfo}
|
||||
perPage={this.state.perPage}
|
||||
toggleMoveDepartment={this.toggleMoveDepartment}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
@@ -418,6 +477,13 @@ class Departments extends React.Component {
|
||||
onMemberChanged={this.onMemberChanged}
|
||||
/>
|
||||
}
|
||||
{isMoveDeparmentDialogShow &&
|
||||
<MoveDepartmentDialog
|
||||
toggle={this.toggleMoveDepartment}
|
||||
nodeId={operateNode.id}
|
||||
onDepartmentChanged={this.onDepartmentChanged}
|
||||
/>
|
||||
}
|
||||
{isRenameDepartmentDialogShow &&
|
||||
<RenameDepartmentV2Dialog
|
||||
node={operateNode}
|
||||
|
@@ -15,7 +15,8 @@ const departmentsTreeNodePropTypes = {
|
||||
toggleAddLibrary: PropTypes.func,
|
||||
toggleAddMembers: PropTypes.func,
|
||||
toggleRename: PropTypes.func,
|
||||
toggleDelete: PropTypes.func
|
||||
toggleDelete: PropTypes.func,
|
||||
toggleMoveDepartment: PropTypes.func,
|
||||
};
|
||||
|
||||
class DepartmentsTreeNode extends Component {
|
||||
@@ -87,6 +88,7 @@ class DepartmentsTreeNode extends Component {
|
||||
toggleRename={this.props.toggleRename}
|
||||
toggleDelete={this.props.toggleDelete}
|
||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -154,6 +156,7 @@ class DepartmentsTreeNode extends Component {
|
||||
toggleSetQuotaDialog={this.props.toggleSetQuotaDialog}
|
||||
toggleAddLibrary={this.props.toggleAddLibrary}
|
||||
toggleAddMembers={this.props.toggleAddMembers}
|
||||
toggleMoveDepartment={this.props.toggleMoveDepartment}
|
||||
toggleRename={this.props.toggleRename}
|
||||
toggleDelete={this.props.toggleDelete}
|
||||
/>
|
||||
|
@@ -425,6 +425,13 @@ class OrgAdminAPI {
|
||||
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) {
|
||||
const url = this.server + '/api/v2.1/org/' + orgID + '/admin/address-book/groups/' + groupID + '/';
|
||||
return this.req.delete(url);
|
||||
|
@@ -522,6 +522,13 @@ class SystemAdminAPI {
|
||||
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) {
|
||||
const url = this.server + '/api/v2.1/admin/address-book/groups/' + groupID + '/';
|
||||
return this.req.delete(url);
|
||||
|
@@ -188,6 +188,7 @@ class AdminGroup(APIView):
|
||||
1. transfer a group.
|
||||
2. set group quota.
|
||||
3. rename group.
|
||||
4. move a group to another group.
|
||||
|
||||
Permission checking:
|
||||
1. Admin user;
|
||||
@@ -286,6 +287,56 @@ class AdminGroup(APIView):
|
||||
error_msg = 'Internal Server Error'
|
||||
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)
|
||||
return Response(group_info)
|
||||
|
||||
@@ -368,18 +419,15 @@ class AdminDepartments(APIView):
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def get(self, request):
|
||||
if not request.user.admin_permissions.can_manage_group():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
try:
|
||||
all_groups = ccnet_api.list_all_departments()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
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 = []
|
||||
for group in all_groups:
|
||||
|
@@ -226,10 +226,6 @@ class OrgAdminDepartments(APIView):
|
||||
error_msg = 'Internal Server Error'
|
||||
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 = []
|
||||
for group in departments:
|
||||
created_at = timestamp_to_isoformat_timestr(group.timestamp)
|
||||
@@ -289,3 +285,74 @@ class OrgAdminGroupToDeptView(APIView):
|
||||
'creator_contact_email': email2contact_email(group.creator_name),
|
||||
}
|
||||
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
|
||||
from .api.admin.user_set_password import OrgAdminUserSetPassword
|
||||
from .api.admin.groups import OrgAdminGroups, OrgAdminGroup, OrgAdminSearchGroup, \
|
||||
OrgAdminDepartments, OrgAdminGroupToDeptView
|
||||
OrgAdminDepartments, OrgAdminGroupToDeptView, OrgAdminMoveDepartment
|
||||
from .api.admin.repos import OrgAdminRepos, OrgAdminRepo
|
||||
from .api.admin.trash_libraries import OrgAdminTrashLibraries, OrgAdminTrashLibrary
|
||||
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>/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>/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'),
|
||||
|
||||
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 -*-
|
||||
import os
|
||||
from django.db import connection
|
||||
from django.db import connection, transaction
|
||||
|
||||
|
||||
def get_ccnet_db_name():
|
||||
@@ -256,3 +256,62 @@ class CcnetDB:
|
||||
staffs = cursor.fetchall()
|
||||
|
||||
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