1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-16 15:19:06 +00:00

[org admin - departments] redesigned it (#6070)

This commit is contained in:
llj
2024-05-12 08:17:02 +08:00
committed by GitHub
parent d62a566e20
commit 67e09a4295
19 changed files with 1198 additions and 986 deletions

View File

@@ -8,15 +8,15 @@ const propTypes = {
groupID: PropTypes.string,
parentGroupID: PropTypes.string,
toggle: PropTypes.func.isRequired,
onDepartChanged: PropTypes.func.isRequired,
onAddNewDepartment: PropTypes.func.isRequired,
};
class AddDepartDialog extends React.Component {
class AddDepartmentDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
departName: '',
departmentName: '',
errMessage: '',
};
}
@@ -28,9 +28,9 @@ class AddDepartDialog extends React.Component {
if (this.props.parentGroupID) {
parentGroup = this.props.parentGroupID;
}
seafileAPI.orgAdminAddDepartGroup(orgID, parentGroup, this.state.departName.trim()).then((res) => {
seafileAPI.orgAdminAddDepartGroup(orgID, parentGroup, this.state.departmentName.trim()).then((res) => {
this.props.toggle();
this.props.onDepartChanged();
this.props.onAddNewDepartment(res.data);
}).catch(error => {
let errorMsg = gettext(error.response.data.error_msg);
this.setState({ errMessage: errorMsg });
@@ -40,7 +40,7 @@ class AddDepartDialog extends React.Component {
validateName = () => {
let errMessage = '';
const name = this.state.departName.trim();
const name = this.state.departmentName.trim();
if (!name.length) {
errMessage = gettext('Name is required');
this.setState({ errMessage: errMessage });
@@ -51,7 +51,7 @@ class AddDepartDialog extends React.Component {
handleChange = (e) => {
this.setState({
departName: e.target.value,
departmentName: e.target.value,
});
};
@@ -70,11 +70,11 @@ class AddDepartDialog extends React.Component {
<ModalBody>
<Form>
<FormGroup>
<Label for="departName">{gettext('Name')}</Label>
<Label for="departmentName">{gettext('Name')}</Label>
<Input
id="departName"
id="departmentName"
onKeyDown={this.handleKeyDown}
value={this.state.departName}
value={this.state.departmentName}
onChange={this.handleChange}
autoFocus={true}
/>
@@ -83,6 +83,7 @@ class AddDepartDialog extends React.Component {
{ this.state.errMessage && <p className="error">{this.state.errMessage}</p> }
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
@@ -90,6 +91,6 @@ class AddDepartDialog extends React.Component {
}
}
AddDepartDialog.propTypes = propTypes;
AddDepartmentDialog.propTypes = propTypes;
export default AddDepartDialog;
export default AddDepartmentDialog;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Alert } from 'reactstrap';
import { gettext, orgID } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
@@ -10,7 +10,7 @@ import UserSelect from '../user-select';
const propTypes = {
toggle: PropTypes.func.isRequired,
groupID: PropTypes.string.isRequired,
onMemberChanged: PropTypes.func.isRequired
onAddNewMembers: PropTypes.func.isRequired
};
class AddMemberDialog extends React.Component {
@@ -40,7 +40,7 @@ class AddMemberDialog extends React.Component {
this.setState({ errMessage: res.data.failed[0].error_msg });
}
if (res.data.success.length > 0) {
this.props.onMemberChanged();
this.props.onAddNewMembers(res.data.success);
this.props.toggle();
}
}).catch(error => {
@@ -50,6 +50,7 @@ class AddMemberDialog extends React.Component {
};
render() {
const { errMessage } = this.state;
return (
<Modal isOpen={true} toggle={this.props.toggle}>
<ModalHeader toggle={this.props.toggle}>{gettext('Add Member')}</ModalHeader>
@@ -61,11 +62,11 @@ class AddMemberDialog extends React.Component {
isMulti={false}
className='org-add-member-select'
/>
{ this.state.errMessage && <p className="error">{this.state.errMessage}</p> }
{errMessage && <Alert color="danger" className="mt-2">{errMessage}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Form, FormGroup, Label } from 'reactstrap';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Form, FormGroup, Label, Alert } from 'reactstrap';
import { gettext, orgID } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
@@ -8,7 +8,7 @@ import { Utils } from '../../utils/utils';
const propTypes = {
toggle: PropTypes.func.isRequired,
groupID: PropTypes.string,
onRepoChanged: PropTypes.func.isRequired,
onAddNewRepo: PropTypes.func.isRequired,
};
class AddRepoDialog extends React.Component {
@@ -26,7 +26,7 @@ class AddRepoDialog extends React.Component {
if (isValid) {
seafileAPI.orgAdminAddDepartmentRepo(orgID, this.props.groupID, this.state.repoName.trim()).then((res) => {
this.props.toggle();
this.props.onRepoChanged();
this.props.onAddNewRepo(res.data);
}).catch(error => {
let errorMsg = Utils.getErrorMsg(error);
this.setState({ errMessage: errorMsg });
@@ -59,6 +59,7 @@ class AddRepoDialog extends React.Component {
};
render() {
const { errMessage } = this.state;
return (
<Modal isOpen={true} toggle={this.props.toggle} autoFocus={false}>
<ModalHeader toggle={this.props.toggle}>{gettext('New Library')}</ModalHeader>
@@ -75,9 +76,10 @@ class AddRepoDialog extends React.Component {
/>
</FormGroup>
</Form>
{ this.state.errMessage && <p className="error">{this.state.errMessage}</p> }
{errMessage && <Alert color="danger" className="mt-2">{errMessage}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>

View File

@@ -10,7 +10,7 @@ const propTypes = {
groupName: PropTypes.string,
groupID: PropTypes.number.isRequired,
toggle: PropTypes.func.isRequired,
onDepartChanged: PropTypes.func.isRequired
onDeleteDepartment: PropTypes.func.isRequired
};
class DeleteDepartDialog extends React.Component {
@@ -20,9 +20,10 @@ class DeleteDepartDialog extends React.Component {
}
deleteDepart = () => {
const { groupID } = this.props;
this.props.toggle();
seafileAPI.orgAdminDeleteDepartGroup(orgID, this.props.groupID).then((res) => {
this.props.onDepartChanged();
seafileAPI.orgAdminDeleteDepartGroup(orgID, groupID).then((res) => {
this.props.onDeleteDepartment(groupID);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);

View File

@@ -42,8 +42,8 @@ class DeleteMemberDialog extends React.Component {
<div dangerouslySetInnerHTML={{__html: subtitle}}></div>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.deleteMember}>{gettext('Delete')}</Button>
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.deleteMember}>{gettext('Delete')}</Button>
</ModalFooter>
</Modal>
);

View File

@@ -13,9 +13,10 @@ class DeleteRepoDialog extends React.Component {
}
deleteRepo = () => {
const { repo } = this.props;
seafileAPI.orgAdminDeleteDepartmentRepo(orgID, this.props.groupID, this.props.repo.repo_id).then((res) => {
if (res.data.success) {
this.props.onRepoChanged();
this.props.onDeleteRepo(repo.repo_id);
this.props.toggle();
}
}).catch(error => {
@@ -34,8 +35,8 @@ class DeleteRepoDialog extends React.Component {
<div dangerouslySetInnerHTML={{__html: subtitle}}></div>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.deleteRepo}>{gettext('Delete')}</Button>
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.deleteRepo}>{gettext('Delete')}</Button>
</ModalFooter>
</Modal>
);
@@ -46,7 +47,7 @@ const propTypes = {
repo: PropTypes.object.isRequired,
toggle: PropTypes.func.isRequired,
groupID: PropTypes.string,
onRepoChanged: PropTypes.func.isRequired
onDeleteRepo: PropTypes.func.isRequired
};
DeleteRepoDialog.propTypes = propTypes;

View File

@@ -97,6 +97,7 @@ class RenameDepartmentDialog extends React.Component {
{this.state.errMessage && <p className="error">{this.state.errMessage}</p>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>

View File

@@ -9,7 +9,7 @@ import toaster from '../toast';
const propTypes = {
toggle: PropTypes.func.isRequired,
groupID: PropTypes.number.isRequired,
onDepartChanged: PropTypes.func.isRequired,
onSetQuota: PropTypes.func.isRequired,
};
class SetGroupQuotaDialog extends React.Component {
@@ -30,7 +30,7 @@ class SetGroupQuotaDialog extends React.Component {
let newQuota = this.state.quota == -2 ? this.state.quota : this.state.quota * 1000000;
seafileAPI.orgAdminSetGroupQuota(orgID, this.props.groupID, newQuota).then((res) => {
this.props.toggle();
this.props.onDepartChanged();
this.props.onSetQuota(res.data);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@@ -74,6 +74,7 @@ class SetGroupQuotaDialog extends React.Component {
{ this.state.errMessage && <p className="error">{this.state.errMessage}</p> }
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.setGroupQuota}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,162 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import ModalPortal from '../../../components/modal-portal';
import DeleteRepoDialog from '../../../components/dialog/org-delete-repo-dialog';
import { gettext, orgID, lang } from '../../../utils/constants';
import Department from './department';
import '../../../css/org-department-item.css';
moment.locale(lang);
class OrgDepartmentItem extends React.Component {
constructor(props) {
super(props);
this.state = {
repos: [],
};
}
componentDidMount() {
const groupID = this.props.groupID;
this.listOrgGroupRepo(groupID);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.groupID !== nextProps.groupID) {
this.listOrgGroupRepo(nextProps.groupID);
}
}
listOrgGroupRepo = (groupID) => {
seafileAPI.orgAdminListGroupRepos(orgID, groupID).then(res => {
this.setState({ repos: res.data.libraries });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
onAddNewRepo = (newRepo) => {
const { repos } = this.state;
repos.unshift({...newRepo, name: newRepo.repo_name}); // 'name': to be compatible with the data returned by the 'GET' request
this.setState({ repos });
};
onDeleteRepo = (repoID) => {
const { repos } = this.state;
this.setState({repos: repos.filter(item => item.repo_id != repoID)});
};
render() {
const { repos } = this.state;
const groupID = this.props.groupID;
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}
groupID={groupID}
repo={repo}
onDeleteRepo={this.onDeleteRepo}
/>
);
})}
</tbody>
</table>
</div>
: <p className="no-libraty">{gettext('No libraries')}</p>
}
</Department>
</Fragment>
);
}
}
class RepoItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
isDeleteDialogOpen: false
};
}
onMouseEnter = () => {
this.setState({ highlight: true });
};
onMouseLeave = () => {
this.setState({ highlight: false });
};
toggleDeleteDialog = () => {
this.setState({
isDeleteDialogOpen: !this.state.isDeleteDialogOpen
});
};
render() {
const { repo, groupID } = this.props;
const { highlight, isDeleteDialogOpen } = this.state;
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>
<td>{repo.name}</td>
<td>{Utils.bytesToSize(repo.size)}{' '}</td>
<td className="cursor-pointer text-center">
<span onClick={this.toggleDeleteDialog} className={`sf2-icon-delete action-icon ${highlight ? '' : 'vh'}`} title="Delete"></span>
</td>
</tr>
{isDeleteDialogOpen && (
<ModalPortal>
<DeleteRepoDialog
toggle={this.toggleDeleteDialog}
onDeleteRepo={this.props.onDeleteRepo}
repo={repo}
groupID={groupID}
/>
</ModalPortal>
)}
</>
);
}
}
const RepoItemPropTypes = {
repo: PropTypes.object.isRequired,
groupID: PropTypes.string.isRequired,
onDeleteRepo: PropTypes.func.isRequired,
};
RepoItem.propTypes = RepoItemPropTypes;
const OrgDepartmentItemPropTypes = {
groupID: PropTypes.string,
};
OrgDepartmentItem.propTypes = OrgDepartmentItemPropTypes;
export default OrgDepartmentItem;

View File

@@ -0,0 +1,153 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { seafileAPI } from '../../../utils/seafile-api';
import MainPanelTopbar from '../main-panel-topbar';
import ModalPortal from '../../../components/modal-portal';
import AddDepartDialog from '../../../components/dialog/org-add-department-dialog';
import { gettext, orgID, lang } from '../../../utils/constants';
import GroupItem from './group-item';
import '../../../css/org-department-item.css';
moment.locale(lang);
class OrgDepartmentsList extends React.Component {
constructor(props) {
super(props);
this.state = {
groups: null,
groupID: '',
groupName: '',
isShowAddDepartDialog: false,
isItemFreezed: false,
};
}
componentDidMount() {
this.listDepartGroups();
}
listDepartGroups = () => {
seafileAPI.orgAdminListDepartGroups(orgID).then(res => {
this.setState({ groups: res.data.data });
});
};
onFreezedItem = () => {
this.setState({isItemFreezed: true});
};
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
};
onDepartmentNameChanged = (dept) => {
this.setState({
groups: this.state.groups.map(item => {
if (item.id == dept.id) {
item.name = dept.name;
}
return item;
})
});
};
toggleAddDepartDialog = () => {
this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog});
};
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 topbarChildren = (
<Fragment>
<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>
)}
</Fragment>
);
return (
<Fragment>
<MainPanelTopbar children={topbarChildren}/>
<div className="main-panel-center flex-row h-100">
<div className="cur-view-container o-auto">
<div className="cur-view-path">
<h3 className="sf-heading">{gettext('Departments')}</h3>
</div>
<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}
group={group}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
onDepartmentNameChanged={this.onDepartmentNameChanged}
onDeleteDepartment={this.onDeleteDepartment}
onSetDepartmentQuota={this.onSetDepartmentQuota}
/>
);
})}
</tbody>
</table>
:
<p className="no-group">{gettext('No departments')}</p>
}
</div>
</div>
</div>
</Fragment>
);
}
}
export default OrgDepartmentsList;

View File

@@ -0,0 +1,303 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import ModalPortal from '../../../components/modal-portal';
import RoleSelector from '../../../components/single-selector';
import DeleteMemberDialog from '../../../components/dialog/org-delete-member-dialog';
import { serviceURL, gettext, orgID, lang } from '../../../utils/constants';
import Department from './department';
import '../../../css/org-department-item.css';
moment.locale(lang);
class OrgDepartmentItem extends React.Component {
constructor(props) {
super(props);
this.state = {
groupName: '',
isItemFreezed: false,
isDepartFreezed: false,
ancestorGroups: [],
members: [],
deletedMember: {},
isShowAddMemberDialog: false,
showDeleteMemberDialog: false,
repos: [],
deletedRepo: {},
isShowAddRepoDialog: false,
groups: [],
subGroupID: '',
subGroupName: '',
isShowAddDepartDialog: false,
showSetGroupQuotaDialog: false,
};
}
componentDidMount() {
const groupID = this.props.groupID;
this.listOrgGroupRepo(groupID);
this.listOrgMembers(groupID);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.groupID !== nextProps.groupID) {
this.listOrgGroupRepo(nextProps.groupID);
this.listOrgMembers(nextProps.groupID);
}
}
listOrgGroupRepo = (groupID) => {
seafileAPI.orgAdminListGroupRepos(orgID, groupID).then(res => {
this.setState({ repos: res.data.libraries });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
listOrgMembers = (groupID) => {
seafileAPI.orgAdminListGroupInfo(orgID, groupID, true).then(res => {
this.setState({
members: res.data.members,
groups: res.data.groups,
ancestorGroups: res.data.ancestor_groups,
groupName: res.data.name,
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
listSubDepartGroups = (groupID) => {
seafileAPI.orgAdminListGroupInfo(orgID, groupID, true).then(res => {
this.setState({ groups: res.data.groups });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
toggleCancel = () => {
this.setState({
showDeleteMemberDialog: false,
showDeleteRepoDialog: false,
showDeleteDepartDialog: false,
showSetGroupQuotaDialog: false,
});
};
onFreezedDepart = () => {
this.setState({isDepartFreezed: true});
};
onUnfreezedDepart = () => {
this.setState({isDepartFreezed: false});
};
onDepartmentNameChanged = (dept) => {
this.setState({
groups: this.state.groups.map(item => {
if (item.id == dept.id) {
item.name = dept.name;
}
return item;
})
});
};
onSubDepartChanged = () => {
this.listSubDepartGroups(this.props.groupID);
};
onRepoChanged = () => {
this.listOrgGroupRepo(this.props.groupID);
};
onMemberChanged = () => {
this.listOrgMembers(this.props.groupID);
};
toggleItemFreezed = (isFreezed) => {
this.setState({ isItemFreezed: isFreezed });
};
showDeleteMemberDialog = (member) => {
this.setState({ showDeleteMemberDialog: true, deletedMember: member });
};
toggleAddRepoDialog = () => {
this.setState({ isShowAddRepoDialog: !this.state.isShowAddRepoDialog });
};
toggleAddMemberDialog = () => {
this.setState({ isShowAddMemberDialog: !this.state.isShowAddMemberDialog });
};
toggleAddDepartDialog = () => {
this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog});
};
showSetGroupQuotaDialog = (subGroupID) => {
this.setState({
showSetGroupQuotaDialog: true,
subGroupID: subGroupID
});
};
onAddNewMembers = (newMembers) => {
const { members } = this.state;
members.unshift(...newMembers);
this.setState({ members });
};
render() {
const { members } = this.state;
const { groupID } = this.props;
return (
<Fragment>
<Department
groupID={groupID}
currentItem="members"
onAddNewMembers={this.onAddNewMembers}
>
<div className="cur-view-content">
{(!members || members.length === 0) ?
<p className="no-member">{gettext('No members')}</p> :
<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 (
<MemberItem
key={index}
member={member}
showDeleteMemberDialog={this.showDeleteMemberDialog}
isItemFreezed={this.state.isItemFreezed}
onMemberChanged={this.onMemberChanged}
toggleItemFreezed={this.toggleItemFreezed}
groupID={groupID}
/>
);
})}
</tbody>
</table>
}
</div>
</Department>
{this.state.showDeleteMemberDialog && (
<ModalPortal>
<DeleteMemberDialog
toggle={this.toggleCancel}
onMemberChanged={this.onMemberChanged}
member={this.state.deletedMember}
groupID={groupID}
/>
</ModalPortal>
)}
</Fragment>
);
}
}
class MemberItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false
};
this.roleOptions = [
{ value: 'Admin', text: gettext('Admin'), isSelected: false },
{ value: 'Member', text: gettext('Member'), isSelected: false }
];
}
onMouseEnter = () => {
if (this.props.isItemFreezed) return;
this.setState({ highlight: true });
};
onMouseLeave = () => {
if (this.props.isItemFreezed) return;
this.setState({ highlight: false });
};
onChangeUserRole = (roleOption) => {
let isAdmin = roleOption.value === 'Admin' ? true : false;
seafileAPI.orgAdminSetGroupMemberRole(orgID, this.props.groupID, this.props.member.email, isAdmin).then((res) => {
this.props.onMemberChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
this.setState({
highlight: false,
});
};
render() {
const member = this.props.member;
const highlight = this.state.highlight;
let memberLink = serviceURL + '/org/useradmin/info/' + member.email + '/';
if (member.role === 'Owner') return null;
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><a href={memberLink}>{member.name}</a></td>
<td>
<RoleSelector
isDropdownToggleShown={highlight}
currentSelectedOption={currentSelectedOption}
options={this.roleOptions}
selectOption={this.onChangeUserRole}
toggleItemFreezed={this.props.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>
</tr>
);
}
}
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,
};
MemberItem.propTypes = MemberItemPropTypes;
const OrgDepartmentItemPropTypes = {
groupID: PropTypes.string,
};
OrgDepartmentItem.propTypes = OrgDepartmentItemPropTypes;
export default OrgDepartmentItem;

View File

@@ -0,0 +1,184 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { Link } from '@gatsbyjs/reach-router';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import MainPanelTopbar from '../main-panel-topbar';
import ModalPortal from '../../../components/modal-portal';
import AddDepartDialog from '../../../components/dialog/org-add-department-dialog';
import AddMemberDialog from '../../../components/dialog/org-add-member-dialog';
import AddRepoDialog from '../../../components/dialog/org-add-repo-dialog';
import { siteRoot, gettext, orgID, lang } from '../../../utils/constants';
import '../../../css/org-department-item.css';
moment.locale(lang);
class OrgDepartmentItem extends React.Component {
constructor(props) {
super(props);
this.state = {
groupName: '',
isItemFreezed: false,
isDepartFreezed: false,
ancestorGroups: [],
members: [],
deletedMember: {},
isShowAddMemberDialog: false,
showDeleteMemberDialog: false,
repos: [],
deletedRepo: {},
isShowAddRepoDialog: false,
showDeleteRepoDialog: false,
groups: [],
subGroupID: '',
subGroupName: '',
isShowAddDepartDialog: 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.listOrgMembers(groupID);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.groupID !== nextProps.groupID) {
this.listOrgMembers(nextProps.groupID);
}
}
listOrgMembers = (groupID) => {
seafileAPI.orgAdminListGroupInfo(orgID, groupID, true).then(res => {
this.setState({
members: res.data.members,
groups: res.data.groups,
ancestorGroups: res.data.ancestor_groups,
groupName: res.data.name,
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
toggleAddRepoDialog = () => {
this.setState({ isShowAddRepoDialog: !this.state.isShowAddRepoDialog });
};
toggleAddMemberDialog = () => {
this.setState({ isShowAddMemberDialog: !this.state.isShowAddMemberDialog });
};
toggleAddDepartDialog = () => {
this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog});
};
render() {
const { groupID, currentItem } = this.props;
const topBtn = 'btn btn-secondary operation-item';
const topbarChildren = (
<Fragment>
{groupID && (
<>
{currentItem == 'subDepartments' &&
<button className={topBtn} title={gettext('New Sub-department')} onClick={this.toggleAddDepartDialog}>{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>
}
</>
)
}
{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.isShowAddDepartDialog && (
<ModalPortal>
<AddDepartDialog
onAddNewDepartment={this.props.onAddNewDepartment}
parentGroupID={groupID}
toggle={this.toggleAddDepartDialog}
/>
</ModalPortal>
)}
</Fragment>
);
return (
<Fragment>
<MainPanelTopbar children={topbarChildren} />
<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 + 'org/departmentadmin/'}>{gettext('Departments')}</Link>
: <span>{gettext('Departments')}</span>
}
{this.state.ancestorGroups.map(ancestor => {
let newHref = siteRoot + 'org/departmentadmin/groups/' + ancestor.id + '/';
return <span key={ancestor.id}>{' / '}<Link to={newHref}>{ancestor.name}</Link></span>;
})}
{groupID && <span>{' / '}{this.state.groupName}</span>}
</h3>
</div>
</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}org/departmentadmin/groups/${groupID}${item.urlPart}`} className={`nav-link ${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
</li>
);
})}
</ul>
{this.props.children}
</div>
</div>
</Fragment>
);
}
}
const OrgDepartmentItemPropTypes = {
groupID: PropTypes.string,
currentItem: PropTypes.string.isRequired,
onAddNewDepartment: PropTypes.func,
onAddNewMembers: PropTypes.func,
onAddNewRepo: PropTypes.func,
children: PropTypes.object
};
OrgDepartmentItem.propTypes = OrgDepartmentItemPropTypes;
export default OrgDepartmentItem;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import '../../css/org-department-item.css';
import '../../../css/org-department-item.css';
class OrgDepartments extends React.Component {
render() {

View File

@@ -0,0 +1,173 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { Link } from '@gatsbyjs/reach-router';
import { Utils } from '../../../utils/utils';
import ModalPortal from '../../../components/modal-portal';
import DeleteDepartDialog from '../../../components/dialog/org-delete-department-dialog';
import SetGroupQuotaDialog from '../../../components/dialog/org-set-group-quota-dialog';
import RenameDepartmentDialog from '../../../components/dialog/org-rename-department-dialog';
import OpMenu from '../../../components/dialog/op-menu';
import { siteRoot, gettext, orgID, lang } from '../../../utils/constants';
import '../../../css/org-department-item.css';
moment.locale(lang);
class GroupItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
isOpIconShown: false,
isRenameDialogOpen: false,
isSetGroupQuotaDialogOpen: false
};
}
onMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: true,
highlight: true
});
}
};
onMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: false,
highlight: false
});
}
};
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;
}
};
onUnfreezedItem = () => {
this.setState({
highlight: false,
isOpIconShow: false
});
this.props.onUnfreezedItem();
};
toggleRenameDialog = () => {
this.setState({
isRenameDialogOpen: !this.state.isRenameDialogOpen
});
};
toggleSetQuotaDialog = () => {
this.setState({
isSetGroupQuotaDialogOpen: !this.state.isSetGroupQuotaDialogOpen
});
};
toggleDeleteDialog = () => {
this.setState({
isDeleteDialogOpen: !this.state.isDeleteDialogOpen
});
};
render() {
const group = this.props.group;
const { highlight, isOpIconShown, isRenameDialogOpen, isSetGroupQuotaDialogOpen, isDeleteDialogOpen } = this.state;
const newHref = siteRoot+ 'org/departmentadmin/groups/' + group.id + '/';
return (
<Fragment>
<tr className={highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td><Link to={newHref}>{group.name}</Link></td>
<td>{moment(group.created_at).fromNow()}</td>
<td>
{Utils.bytesToSize(group.quota)}{' '}
<span onClick={this.toggleSetQuotaDialog} title={gettext('Edit Quota')} className={`fa fa-pencil-alt 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>
{isSetGroupQuotaDialogOpen && (
<ModalPortal>
<SetGroupQuotaDialog
toggle={this.toggleSetQuotaDialog}
groupID={group.id}
onSetQuota={this.props.onSetDepartmentQuota}
/>
</ModalPortal>
)}
{isRenameDialogOpen && (
<RenameDepartmentDialog
orgID={orgID}
groupID={group.id}
name={group.name}
toggle={this.toggleRenameDialog}
onDepartmentNameChanged={this.props.onDepartmentNameChanged}
/>
)}
{isDeleteDialogOpen && (
<ModalPortal>
<DeleteDepartDialog
toggle={this.toggleDeleteDialog}
groupID={group.id}
groupName={group.name}
onDeleteDepartment={this.props.onDeleteDepartment}
/>
</ModalPortal>
)}
</Fragment>
);
}
}
const GroupItemPropTypes = {
group: PropTypes.object.isRequired,
isItemFreezed: PropTypes.bool.isRequired,
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired,
onDepartmentNameChanged: PropTypes.func.isRequired,
onSetDepartmentQuota: PropTypes.func.isRequired,
onDeleteDepartment: PropTypes.func.isRequired,
};
GroupItem.propTypes = GroupItemPropTypes;
export default GroupItem;

View File

@@ -0,0 +1,167 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import { gettext, orgID, lang } from '../../../utils/constants';
import Department from './department';
import GroupItem from './group-item';
import '../../../css/org-department-item.css';
moment.locale(lang);
class OrgDepartmentItem extends React.Component {
constructor(props) {
super(props);
this.state = {
groupName: '',
isItemFreezed: false,
isDepartFreezed: false,
ancestorGroups: [],
members: [],
isShowAddMemberDialog: false,
repos: [],
deletedRepo: {},
groups: [],
};
}
componentDidMount() {
const groupID = this.props.groupID;
this.listOrgMembers(groupID);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.groupID !== nextProps.groupID) {
this.listOrgMembers(nextProps.groupID);
}
}
listOrgMembers = (groupID) => {
seafileAPI.orgAdminListGroupInfo(orgID, groupID, true).then(res => {
this.setState({
members: res.data.members,
groups: res.data.groups,
ancestorGroups: res.data.ancestor_groups,
groupName: res.data.name,
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
listSubDepartGroups = (groupID) => {
seafileAPI.orgAdminListGroupInfo(orgID, groupID, true).then(res => {
this.setState({ groups: res.data.groups });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
onFreezedDepart = () => {
this.setState({isDepartFreezed: true});
};
onUnfreezedDepart = () => {
this.setState({isDepartFreezed: false});
};
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;
const groupID = this.props.groupID;
return (
<Fragment>
<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(
<React.Fragment key={group.id}>
<GroupItem
group={group}
isItemFreezed={this.state.isDepartFreezed}
onFreezedItem={this.onFreezedDepart}
onUnfreezedItem={this.onUnfreezedDepart}
onDepartmentNameChanged={this.onDepartmentNameChanged}
onDeleteDepartment={this.onDeleteDepartment}
onSetDepartmentQuota={this.onSetDepartmentQuota}
/>
</React.Fragment>
);
})}
</tbody>
</table>
: <p className="no-group">{gettext('No sub-departments')}</p>
}
</div>
</Department>
</Fragment>
);
}
}
const OrgDepartmentItemPropTypes = {
groupID: PropTypes.string,
};
OrgDepartmentItem.propTypes = OrgDepartmentItemPropTypes;
export default OrgDepartmentItem;

View File

@@ -28,9 +28,13 @@ import OrgAllRepos from './libraries/org-all-repos';
import OrgTrashRepos from './libraries/org-repo-trash';
import OrgInfo from './org-info';
import OrgLinks from './org-links';
import OrgDepartments from './org-departments';
import OrgDepartmentsList from './org-departments-list';
import OrgDepartmentItem from './org-department-item';
import Departments from './departments/departments';
import DepartmentList from './departments/department-list';
import SubDepartments from './departments/sub-departments';
import DepartmentMembers from './departments/department-members';
import DepartmentLibraries from './departments/department-libraries';
import OrgLogs from './org-logs';
import OrgLogsFileAudit from './org-logs-file-audit';
import OrgLogsFileUpdate from './org-logs-file-update';
@@ -110,10 +114,12 @@ class Org extends React.Component {
<OrgAllRepos path={siteRoot + 'org/repoadmin'}/>
<OrgTrashRepos path={siteRoot + 'org/repoadmin-trash'}/>
<OrgLinks path={siteRoot + 'org/publinkadmin'}/>
<OrgDepartments path={siteRoot + 'org/departmentadmin'}>
<OrgDepartmentsList path='/'/>
<OrgDepartmentItem path='groups/:groupID'/>
</OrgDepartments>
<Departments path={siteRoot + 'org/departmentadmin'}>
<DepartmentList path='/' />
<SubDepartments path='groups/:groupID' />
<DepartmentMembers path='groups/:groupID/members' />
<DepartmentLibraries path='groups/:groupID/libraries' />
</Departments>
<OrgLogs path={siteRoot + 'org/logadmin'} currentTab={currentTab} tabItemClick={this.tabItemClick}>
<OrgLogsFileAudit path='/' />
<OrgLogsFileUpdate path='file-update' />

View File

@@ -1,648 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { Link } from '@gatsbyjs/reach-router';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import toaster from '../../components/toast';
import MainPanelTopbar from './main-panel-topbar';
import ModalPortal from '../../components/modal-portal';
import RoleSelector from '../../components/single-selector';
import AddDepartDialog from '../../components/dialog/org-add-department-dialog';
import AddMemberDialog from '../../components/dialog/org-add-member-dialog';
import DeleteMemberDialog from '../../components/dialog/org-delete-member-dialog';
import AddRepoDialog from '../../components/dialog/org-add-repo-dialog';
import DeleteRepoDialog from '../../components/dialog/org-delete-repo-dialog';
import DeleteDepartDialog from '../../components/dialog/org-delete-department-dialog';
import SetGroupQuotaDialog from '../../components/dialog/org-set-group-quota-dialog';
import RenameDepartmentDialog from '../../components/dialog/org-rename-department-dialog';
import OpMenu from '../../components/dialog/op-menu';
import { serviceURL, siteRoot, gettext, orgID, lang } from '../../utils/constants';
import '../../css/org-department-item.css';
moment.locale(lang);
class OrgDepartmentItem extends React.Component {
constructor(props) {
super(props);
this.state = {
groupName: '',
isItemFreezed: false,
isDepartFreezed: false,
ancestorGroups: [],
members: [],
deletedMember: {},
isShowAddMemberDialog: false,
showDeleteMemberDialog: false,
repos: [],
deletedRepo: {},
isShowAddRepoDialog: false,
showDeleteRepoDialog: false,
groups: [],
subGroupID: '',
subGroupName: '',
isShowAddDepartDialog: false,
showDeleteDepartDialog: false,
showSetGroupQuotaDialog: false,
};
}
componentDidMount() {
const groupID = this.props.groupID;
this.listOrgGroupRepo(groupID);
this.listOrgMembers(groupID);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.groupID !== nextProps.groupID) {
this.listOrgGroupRepo(nextProps.groupID);
this.listOrgMembers(nextProps.groupID);
}
}
listOrgGroupRepo = (groupID) => {
seafileAPI.orgAdminListGroupRepos(orgID, groupID).then(res => {
this.setState({ repos: res.data.libraries });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
listOrgMembers = (groupID) => {
seafileAPI.orgAdminListGroupInfo(orgID, groupID, true).then(res => {
this.setState({
members: res.data.members,
groups: res.data.groups,
ancestorGroups: res.data.ancestor_groups,
groupName: res.data.name,
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
listSubDepartGroups = (groupID) => {
seafileAPI.orgAdminListGroupInfo(orgID, groupID, true).then(res => {
this.setState({ groups: res.data.groups });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
toggleCancel = () => {
this.setState({
showDeleteMemberDialog: false,
showDeleteRepoDialog: false,
showDeleteDepartDialog: false,
showSetGroupQuotaDialog: false,
});
};
onFreezedDepart = () => {
this.setState({isDepartFreezed: true});
};
onUnfreezedDepart = () => {
this.setState({isDepartFreezed: false});
};
onDepartmentNameChanged = (dept) => {
this.setState({
groups: this.state.groups.map(item => {
if (item.id == dept.id) {
item.name = dept.name;
}
return item;
})
});
};
onSubDepartChanged = () => {
this.listSubDepartGroups(this.props.groupID);
};
onRepoChanged = () => {
this.listOrgGroupRepo(this.props.groupID);
};
onMemberChanged = () => {
this.listOrgMembers(this.props.groupID);
};
toggleItemFreezed = (isFreezed) => {
this.setState({ isItemFreezed: isFreezed });
};
showDeleteMemberDialog = (member) => {
this.setState({ showDeleteMemberDialog: true, deletedMember: member });
};
showDeleteRepoDialog = (repo) => {
this.setState({ showDeleteRepoDialog: true, deletedRepo: repo });
};
toggleAddRepoDialog = () => {
this.setState({ isShowAddRepoDialog: !this.state.isShowAddRepoDialog });
};
toggleAddMemberDialog = () => {
this.setState({ isShowAddMemberDialog: !this.state.isShowAddMemberDialog });
};
toggleAddDepartDialog = () => {
this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog});
};
showDeleteDepartDialog = (subGroup) => {
this.setState({
showDeleteDepartDialog: true,
subGroupID: subGroup.id,
subGroupName: subGroup.name
});
};
showSetGroupQuotaDialog = (subGroupID) => {
this.setState({
showSetGroupQuotaDialog: true,
subGroupID: subGroupID
});
};
render() {
const { members, repos, groups } = this.state;
const groupID = this.props.groupID;
const topBtn = 'btn btn-secondary operation-item';
const topbarChildren = (
<Fragment>
{groupID &&
<button className={topBtn} title={gettext('New Sub-department')} onClick={this.toggleAddDepartDialog}>{gettext('New Sub-department')}</button>
}
{groupID &&
<button className={topBtn} title={gettext('Add Member')} onClick={this.toggleAddMemberDialog}>{gettext('Add Member')}</button>
}
{groupID &&
<button className={topBtn} onClick={this.toggleAddRepoDialog} title={gettext('New Library')}>{gettext('New Library')}</button>
}
{this.state.isShowAddMemberDialog && (
<ModalPortal>
<AddMemberDialog
toggle={this.toggleAddMemberDialog}
onMemberChanged={this.onMemberChanged}
groupID={groupID}
/>
</ModalPortal>
)}
{this.state.isShowAddRepoDialog && (
<ModalPortal>
<AddRepoDialog
toggle={this.toggleAddRepoDialog}
onRepoChanged={this.onRepoChanged}
groupID={groupID}
/>
</ModalPortal>
)}
{this.state.isShowAddDepartDialog && (
<ModalPortal>
<AddDepartDialog
onDepartChanged={this.onSubDepartChanged}
parentGroupID={groupID}
toggle={this.toggleAddDepartDialog}
/>
</ModalPortal>
)}
</Fragment>
);
return (
<Fragment>
<MainPanelTopbar children={topbarChildren} />
<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 + 'org/departmentadmin/'}>{gettext('Departments')}</Link>
: <span>{gettext('Departments')}</span>
}
{this.state.ancestorGroups.map(ancestor => {
let newHref = siteRoot + 'org/departmentadmin/groups/' + ancestor.id + '/';
return <span key={ancestor.id}>{' / '}<Link to={newHref}>{ancestor.name}</Link></span>;
})}
{groupID && <span>{' / '}{this.state.groupName}</span>}
</h3>
</div>
</div>
<div className="cur-view-subcontainer org-groups">
<div className="cur-view-path">
<div className="fleft"><h3 className="sf-heading">{gettext('Sub-departments')}</h3></div>
</div>
<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(
<React.Fragment key={group.id}>
<GroupItem
group={group}
isItemFreezed={this.state.isDepartFreezed}
onFreezedItem={this.onFreezedDepart}
onUnfreezedItem={this.onUnfreezedDepart}
onDepartmentNameChanged={this.onDepartmentNameChanged}
showDeleteDepartDialog={this.showDeleteDepartDialog}
showSetGroupQuotaDialog={this.showSetGroupQuotaDialog}
/>
</React.Fragment>
);
})}
</tbody>
</table>
: <p className="no-group">{gettext('No sub-departments')}</p>
}
</div>
</div>
<div className="cur-view-subcontainer org-members">
<div className="cur-view-path">
<div className="fleft"><h3 className="sf-heading">{gettext('Members')}</h3></div>
</div>
<div className="cur-view-content">
{(!members || members.length === 0) ?
<p className="no-member">{gettext('No members')}</p> :
<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 (
<React.Fragment key={index}>
<MemberItem
member={member}
showDeleteMemberDialog={this.showDeleteMemberDialog}
isItemFreezed={this.state.isItemFreezed}
onMemberChanged={this.onMemberChanged}
toggleItemFreezed={this.toggleItemFreezed}
groupID={groupID}
/>
</React.Fragment>
);
})}
</tbody>
</table>
}
</div>
</div>
<div className="cur-view-subcontainer org-libriries">
<div className="cur-view-path">
<div className="fleft"><h3 className="sf-heading">{gettext('Libraries')}</h3></div>
</div>
{ 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(
<React.Fragment key={index}>
<RepoItem repo={repo} showDeleteRepoDialog={this.showDeleteRepoDialog}/>
</React.Fragment>
);
})}
</tbody>
</table>
</div>
: <p className="no-libraty">{gettext('No libraries')}</p>
}
</div>
</div>
<React.Fragment>
{this.state.showDeleteMemberDialog && (
<ModalPortal>
<DeleteMemberDialog
toggle={this.toggleCancel}
onMemberChanged={this.onMemberChanged}
member={this.state.deletedMember}
groupID={groupID}
/>
</ModalPortal>
)}
{this.state.showDeleteRepoDialog && (
<ModalPortal>
<DeleteRepoDialog
toggle={this.toggleCancel}
onRepoChanged={this.onRepoChanged}
repo={this.state.deletedRepo}
groupID={groupID}
/>
</ModalPortal>
)}
{this.state.showDeleteDepartDialog && (
<ModalPortal>
<DeleteDepartDialog
toggle={this.toggleCancel}
groupID={this.state.subGroupID}
groupName={this.state.subGroupName}
onDepartChanged={this.onSubDepartChanged}
/>
</ModalPortal>
)}
{this.state.showSetGroupQuotaDialog && (
<ModalPortal>
<SetGroupQuotaDialog
toggle={this.toggleCancel}
groupID={this.state.subGroupID}
onDepartChanged={this.onSubDepartChanged}
/>
</ModalPortal>
)}
</React.Fragment>
</div>
</Fragment>
);
}
}
class MemberItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false
};
this.roleOptions = [
{ value: 'Admin', text: gettext('Admin'), isSelected: false },
{ value: 'Member', text: gettext('Member'), isSelected: false }
];
}
onMouseEnter = () => {
if (this.props.isItemFreezed) return;
this.setState({ highlight: true });
};
onMouseLeave = () => {
if (this.props.isItemFreezed) return;
this.setState({ highlight: false });
};
onChangeUserRole = (roleOption) => {
let isAdmin = roleOption.value === 'Admin' ? true : false;
seafileAPI.orgAdminSetGroupMemberRole(orgID, this.props.groupID, this.props.member.email, isAdmin).then((res) => {
this.props.onMemberChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
this.setState({
highlight: false,
});
};
render() {
const member = this.props.member;
const highlight = this.state.highlight;
let memberLink = serviceURL + '/org/useradmin/info/' + member.email + '/';
if (member.role === 'Owner') return null;
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><a href={memberLink}>{member.name}</a></td>
<td>
<RoleSelector
isDropdownToggleShown={highlight}
currentSelectedOption={currentSelectedOption}
options={this.roleOptions}
selectOption={this.onChangeUserRole}
toggleItemFreezed={this.props.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>
</tr>
);
}
}
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,
};
MemberItem.propTypes = MemberItemPropTypes;
class RepoItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
};
}
onMouseEnter = () => {
this.setState({ highlight: true });
};
onMouseLeave = () => {
this.setState({ highlight: false });
};
render() {
const repo = this.props.repo;
const highlight = this.state.highlight;
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>
<td>{repo.name}</td>
<td>{Utils.bytesToSize(repo.size)}{' '}</td>
<td className="cursor-pointer text-center" onClick={this.props.showDeleteRepoDialog.bind(this, repo)}>
<span className={`sf2-icon-delete action-icon ${highlight ? '' : 'vh'}`} title="Delete"></span>
</td>
</tr>
);
}
}
const RepoItemPropTypes = {
repo: PropTypes.object.isRequired,
showDeleteRepoDialog: PropTypes.func.isRequired,
};
RepoItem.propTypes = RepoItemPropTypes;
class GroupItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
isOpIconShown: false,
isRenameDialogOpen: false,
};
}
onMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: true,
highlight: true
});
}
};
onMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: false,
highlight: false
});
}
};
translateOperations = (item) => {
let translateResult = '';
switch(item) {
case 'Rename':
translateResult = gettext('Rename');
break;
case 'Delete':
translateResult = gettext('Delete');
break;
default:
break;
}
return translateResult;
};
onMenuItemClick = (operation) => {
const { group } = this.props;
switch(operation) {
case 'Rename':
this.toggleRenameDialog();
break;
case 'Delete':
this.props.showDeleteDepartDialog(group);
break;
default:
break;
}
};
onUnfreezedItem = () => {
this.setState({
highlight: false,
isOpIconShow: false
});
this.props.onUnfreezedItem();
};
toggleRenameDialog = () => {
this.setState({
isRenameDialogOpen: !this.state.isRenameDialogOpen
});
};
render() {
const group = this.props.group;
const { highlight, isOpIconShown, isRenameDialogOpen } = this.state;
const newHref = siteRoot+ 'org/departmentadmin/groups/' + group.id + '/';
return (
<Fragment>
<tr className={highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td><Link to={newHref}>{group.name}</Link></td>
<td>{moment(group.created_at).fromNow()}</td>
<td onClick={this.props.showSetGroupQuotaDialog.bind(this, group.id)}>
{Utils.bytesToSize(group.quota)}{' '}
<span title="Edit Quota" className={`fa fa-pencil-alt 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>
{isRenameDialogOpen && (
<RenameDepartmentDialog
orgID={orgID}
groupID={group.id}
name={group.name}
toggle={this.toggleRenameDialog}
onDepartmentNameChanged={this.props.onDepartmentNameChanged}
/>
)}
</Fragment>
);
}
}
const GroupItemPropTypes = {
group: PropTypes.object.isRequired,
groupID: PropTypes.string,
isItemFreezed: PropTypes.bool.isRequired,
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired,
onDepartmentNameChanged: PropTypes.func.isRequired,
showSetGroupQuotaDialog: PropTypes.func.isRequired,
showDeleteDepartDialog: PropTypes.func.isRequired,
isSubdepartChanged: PropTypes.bool,
};
GroupItem.propTypes = GroupItemPropTypes;
const OrgDepartmentItemPropTypes = {
groupID: PropTypes.string,
};
OrgDepartmentItem.propTypes = OrgDepartmentItemPropTypes;
export default OrgDepartmentItem;

View File

@@ -1,298 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { Link } from '@gatsbyjs/reach-router';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
import MainPanelTopbar from './main-panel-topbar';
import ModalPortal from '../../components/modal-portal';
import AddDepartDialog from '../../components/dialog/org-add-department-dialog';
import DeleteDepartDialog from '../../components/dialog/org-delete-department-dialog';
import SetGroupQuotaDialog from '../../components/dialog/org-set-group-quota-dialog';
import RenameDepartmentDialog from '../../components/dialog/org-rename-department-dialog';
import OpMenu from '../../components/dialog/op-menu';
import { siteRoot, gettext, orgID, lang } from '../../utils/constants';
import '../../css/org-department-item.css';
moment.locale(lang);
class OrgDepartmentsList extends React.Component {
constructor(props) {
super(props);
this.state = {
groups: null,
groupID: '',
groupName: '',
showDeleteDepartDialog: false,
showSetGroupQuotaDialog: false,
isShowAddDepartDialog: false,
isItemFreezed: false,
};
}
componentDidMount() {
this.listDepartGroups();
}
listDepartGroups = () => {
seafileAPI.orgAdminListDepartGroups(orgID).then(res => {
this.setState({ groups: res.data.data });
});
};
onFreezedItem = () => {
this.setState({isItemFreezed: true});
};
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
};
onDepartmentNameChanged = (dept) => {
this.setState({
groups: this.state.groups.map(item => {
if (item.id == dept.id) {
item.name = dept.name;
}
return item;
})
});
};
showDeleteDepartDialog = (group) => {
this.setState({ showDeleteDepartDialog: true, groupID: group.id, groupName: group.name });
};
showSetGroupQuotaDialog = (groupID) => {
this.setState({ showSetGroupQuotaDialog: true, groupID: groupID });
};
toggleAddDepartDialog = () => {
this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog});
};
toggleCancel = () => {
this.setState({
showDeleteDepartDialog: false,
showSetGroupQuotaDialog: false,
});
};
onDepartChanged = () => {
this.listDepartGroups();
};
render() {
const groups = this.state.groups;
const topbarChildren = (
<Fragment>
<button className='btn btn-secondary operation-item' title={gettext('New Department')} onClick={this.toggleAddDepartDialog}>{gettext('New Department')}
</button>
{this.state.isShowAddDepartDialog && (
<ModalPortal>
<AddDepartDialog
onDepartChanged={this.onDepartChanged}
groupID={this.state.groupID}
toggle={this.toggleAddDepartDialog}
/>
</ModalPortal>
)}
</Fragment>
);
return (
<Fragment>
<MainPanelTopbar children={topbarChildren}/>
<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">
{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(
<React.Fragment key={group.id}>
<GroupItem
group={group}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
onDepartmentNameChanged={this.onDepartmentNameChanged}
showDeleteDepartDialog={this.showDeleteDepartDialog}
showSetGroupQuotaDialog={this.showSetGroupQuotaDialog}
/>
</React.Fragment>
);
})}
</tbody>
</table>
:
<p className="no-group">{gettext('No departments')}</p>
}
</div>
<React.Fragment>
{this.state.showDeleteDepartDialog && (
<ModalPortal>
<DeleteDepartDialog
toggle={this.toggleCancel}
groupID={this.state.groupID}
groupName={this.state.groupName}
onDepartChanged={this.onDepartChanged}
/>
</ModalPortal>
)}
{this.state.showSetGroupQuotaDialog && (
<ModalPortal>
<SetGroupQuotaDialog
toggle={this.toggleCancel}
groupID={this.state.groupID}
onDepartChanged={this.onDepartChanged}
/>
</ModalPortal>
)}
</React.Fragment>
</div>
</div>
</Fragment>
);
}
}
class GroupItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
isOpIconShown: false,
isRenameDialogOpen: false,
};
}
onMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: true,
highlight: true
});
}
};
onMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: false,
highlight: false
});
}
};
translateOperations = (item) => {
let translateResult = '';
switch(item) {
case 'Rename':
translateResult = gettext('Rename');
break;
case 'Delete':
translateResult = gettext('Delete');
break;
default:
break;
}
return translateResult;
};
onMenuItemClick = (operation) => {
const { group } = this.props;
switch(operation) {
case 'Rename':
this.toggleRenameDialog();
break;
case 'Delete':
this.props.showDeleteDepartDialog(group);
break;
default:
break;
}
};
onUnfreezedItem = () => {
this.setState({
highlight: false,
isOpIconShow: false
});
this.props.onUnfreezedItem();
};
toggleRenameDialog = () => {
this.setState({
isRenameDialogOpen: !this.state.isRenameDialogOpen
});
};
render() {
const group = this.props.group;
const { highlight, isOpIconShown, isRenameDialogOpen } = this.state;
const newHref = siteRoot+ 'org/departmentadmin/groups/' + group.id + '/';
return (
<Fragment>
<tr className={highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td><Link to={newHref}>{group.name}</Link></td>
<td>{moment(group.created_at).fromNow()}</td>
<td onClick={this.props.showSetGroupQuotaDialog.bind(this, group.id)}>
{Utils.bytesToSize(group.quota)}{' '}
<span title="Edit Quota" className={`fa fa-pencil-alt 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>
{isRenameDialogOpen && (
<RenameDepartmentDialog
orgID={orgID}
groupID={group.id}
name={group.name}
toggle={this.toggleRenameDialog}
onDepartmentNameChanged={this.props.onDepartmentNameChanged}
/>
)}
</Fragment>
);
}
}
const GroupItemPropTypes = {
group: PropTypes.object.isRequired,
isItemFreezed: PropTypes.bool.isRequired,
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired,
onDepartmentNameChanged: PropTypes.func.isRequired,
showSetGroupQuotaDialog: PropTypes.func.isRequired,
showDeleteDepartDialog: PropTypes.func.isRequired,
};
GroupItem.propTypes = GroupItemPropTypes;
export default OrgDepartmentsList;

View File

@@ -38,8 +38,10 @@ urlpatterns = [
path('info/', react_fake_view, name='org_info'),
path('settings/', react_fake_view, name='org_settings'),
path('departmentadmin/', react_fake_view, name='org_department_admin'),
path('departmentadmin/', react_fake_view, name='org_departments_admin'),
re_path(r'^departmentadmin/groups/(?P<group_id>\d+)/', react_fake_view, name='org_department_admin'),
re_path(r'^departmentadmin/groups/(?P<group_id>\d+)/members', react_fake_view, name='org_department_members_admin'),
re_path(r'^departmentadmin/groups/(?P<group_id>\d+)/libraries', react_fake_view, name='org_department_libraries_admin'),
path('associate/<path:token>/', org_associate, name='org_associate'),
path('samlconfig/', react_fake_view, name='saml_config'),