mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-13 05:39:59 +00:00
Invitations Modal
This commit is contained in:
99
frontend/src/components/dialog/invite-people-dialog.js
Normal file
99
frontend/src/components/dialog/invite-people-dialog.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {gettext} from '../../utils/constants';
|
||||||
|
import {seafileAPI} from '../../utils/seafile-api';
|
||||||
|
import {Modal, ModalHeader, ModalBody, ModalFooter, Input, Button} from 'reactstrap';
|
||||||
|
import InvitationsToolbar from "../toolbar/invitations-toolbar";
|
||||||
|
|
||||||
|
class InvitePeopleDialog extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
emails: '',
|
||||||
|
errorMsg: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEmailsChange = (event) => {
|
||||||
|
let emails = event.target.value;
|
||||||
|
this.setState({
|
||||||
|
emails: emails
|
||||||
|
});
|
||||||
|
if (this.state.errorMsg) {
|
||||||
|
this.setState({
|
||||||
|
errorMsg: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown = (e) => {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.handleSubmitInvite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmitInvite = () => {
|
||||||
|
let emails = this.state.emails.trim();
|
||||||
|
if (emails) {
|
||||||
|
seafileAPI.invitePeople(emails).then((res) => {
|
||||||
|
|
||||||
|
if (res.data.failed.length > 0) {
|
||||||
|
let inviteFailed = '';
|
||||||
|
for (let i = 0; i < res.data.failed.length; i++) {
|
||||||
|
inviteFailed += res.data.failed[i].error_msg
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
errorMsg: inviteFailed,
|
||||||
|
});
|
||||||
|
this.props.listInvitations();
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
emails: '',
|
||||||
|
});
|
||||||
|
this.props.onInvitePeople();
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
let errorMsg = gettext(error.response.data.error_msg);
|
||||||
|
this.setState({
|
||||||
|
errorMsg: errorMsg,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
errorMsg: gettext('Email is required')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={this.props.showInvitationsModal} toggle={this.props.toggleInvitationsModal}>
|
||||||
|
<ModalHeader toggle={this.props.toggleInvitationsModal}>{gettext('Invite People')}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<label htmlFor="emails">{gettext('Email')}</label>
|
||||||
|
<Input type="text" id="emails" placeholder="Emails, separated by ','"
|
||||||
|
value={this.state.emails}
|
||||||
|
onChange={this.handleEmailsChange}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
/>
|
||||||
|
<span className="error">{this.state.errorMsg}</span>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="secondary" onClick={this.props.toggleInvitationsModal}>{gettext('Cancel')}</Button>
|
||||||
|
<Button color="primary" onClick={this.handleSubmitInvite}>{gettext('Submit')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const InvitePeopleDialogPropTypes = {
|
||||||
|
toggleInvitationsModal: PropTypes.func.isRequired,
|
||||||
|
showInvitationsModal: PropTypes.bool.isRequired,
|
||||||
|
onInvitePeople: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
InvitePeopleDialog.propTypes = InvitePeopleDialogPropTypes;
|
||||||
|
|
||||||
|
export default InvitePeopleDialog;
|
@@ -8,6 +8,7 @@ import { gettext } from '../../utils/constants';
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
onShowSidePanel: PropTypes.func.isRequired,
|
onShowSidePanel: PropTypes.func.isRequired,
|
||||||
onSearchedClick: PropTypes.func.isRequired,
|
onSearchedClick: PropTypes.func.isRequired,
|
||||||
|
toggleInvitationsModal: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
class InvitationsToolbar extends React.Component {
|
class InvitationsToolbar extends React.Component {
|
||||||
@@ -17,7 +18,7 @@ class InvitationsToolbar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { onShowSidePanel, onSearchedClick } = this.props;
|
let { onShowSidePanel, onSearchedClick, toggleInvitationsModal } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="main-panel-north border-left-show">
|
<div className="main-panel-north border-left-show">
|
||||||
<div className="cur-view-toolbar">
|
<div className="cur-view-toolbar">
|
||||||
@@ -27,7 +28,7 @@ class InvitationsToolbar extends React.Component {
|
|||||||
|
|
||||||
<MediaQuery query="(min-width: 768px)">
|
<MediaQuery query="(min-width: 768px)">
|
||||||
<div className="operation">
|
<div className="operation">
|
||||||
<Button color="btn btn-secondary operation-item" >
|
<Button color="btn btn-secondary operation-item" onClick={toggleInvitationsModal}>
|
||||||
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Invite People')}
|
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Invite People')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,5 +43,6 @@ class InvitationsToolbar extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InvitationsToolbar.propTypes = propTypes;
|
||||||
|
|
||||||
export default InvitationsToolbar;
|
export default InvitationsToolbar;
|
||||||
|
8
frontend/src/css/invitations.css
Normal file
8
frontend/src/css/invitations.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.invite-accept-icon{
|
||||||
|
color:green;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
@@ -2,26 +2,205 @@ import React, { Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {gettext} from '../../utils/constants';
|
import {gettext} from '../../utils/constants';
|
||||||
import InvitationsToolbar from '../../components/toolbar/invitations-toolbar';
|
import InvitationsToolbar from '../../components/toolbar/invitations-toolbar';
|
||||||
|
import InvitePeopleDialog from "../../components/dialog/invite-people-dialog";
|
||||||
|
import {seafileAPI} from "../../utils/seafile-api";
|
||||||
|
import {Table} from 'reactstrap';
|
||||||
|
import Loading from "../../components/loading";
|
||||||
|
import moment from 'moment';
|
||||||
|
import "../../css/invitations.css";
|
||||||
|
|
||||||
|
class InvitationsListItem extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isOperationShow: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseEnter = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState({
|
||||||
|
isOperationShow: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOver = () => {
|
||||||
|
this.setState({
|
||||||
|
isOperationShow: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave = () => {
|
||||||
|
this.setState({
|
||||||
|
isOperationShow: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const invitationItem = this.props.invitation;
|
||||||
|
const acceptIcon = <i className="sf2-icon-tick invite-accept-icon"></i>;
|
||||||
|
const deleteOperation = <i className="action-icon sf2-icon-x3"
|
||||||
|
title={gettext('Delete')}
|
||||||
|
style={!this.state.isOperationShow ? {opacity: 0} : {}}
|
||||||
|
onClick={this.props.onItemDelete.bind(this, invitationItem.token)}></i>;
|
||||||
|
return (
|
||||||
|
<tr onMouseEnter={this.onMouseEnter}
|
||||||
|
onMouseOver={this.onMouseOver}
|
||||||
|
onMouseLeave={this.onMouseLeave}>
|
||||||
|
<td>{invitationItem.accepter}</td>
|
||||||
|
<td>{moment(invitationItem.invite_time).format("YYYY-MM-DD")}</td>
|
||||||
|
<td>{moment(invitationItem.expire_time).format("YYYY-MM-DD")}</td>
|
||||||
|
<td>{invitationItem.accept_time && acceptIcon}</td>
|
||||||
|
<td>{!invitationItem.accept_time && deleteOperation}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const InvitationsListItemPropTypes = {
|
||||||
|
invitation: PropTypes.object.isRequired,
|
||||||
|
onItemDelete: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
InvitationsListItem.propTypes = InvitationsListItemPropTypes;
|
||||||
|
|
||||||
|
class InvitationsListView extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemDelete = (token) => {
|
||||||
|
seafileAPI.deleteInvitation(token).then((res) => {
|
||||||
|
this.props.listInvitations();
|
||||||
|
}).catch((error) => {
|
||||||
|
}); //todo
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const invitationsTables = this.props.invitationsList.map((invitation, index) => {
|
||||||
|
return (
|
||||||
|
<InvitationsListItem
|
||||||
|
key={index}
|
||||||
|
onItemDelete={this.onItemDelete}
|
||||||
|
invitation={invitation}
|
||||||
|
/>)
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table hover>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="40%">email</th>
|
||||||
|
<th width="20%">invite time</th>
|
||||||
|
<th width="20%">expire time</th>
|
||||||
|
<th width="10%">accept</th>
|
||||||
|
<th width="10%"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{invitationsTables}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const InvitationsListViewPropTypes = {
|
||||||
|
invitationsList: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
InvitationsListView.propTypes = InvitationsListViewPropTypes;
|
||||||
|
|
||||||
class InvitationsView extends React.Component {
|
class InvitationsView extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
showInvitationsModal: false,
|
||||||
|
invitationsList: [],
|
||||||
|
isLoading: true,
|
||||||
|
errorMsg: '',
|
||||||
|
showEmptyTip: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
listInvitations = () => {
|
||||||
|
seafileAPI.listInvitations().then((res) => {
|
||||||
|
this.setState({
|
||||||
|
invitationsList: res.data,
|
||||||
|
showEmptyTip: true,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
let errorMsg = gettext(error.response.data.error_msg);
|
||||||
|
this.setState({
|
||||||
|
errorMsg: errorMsg,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onInvitePeople = () => {
|
||||||
|
this.setState({
|
||||||
|
showInvitationsModal: false,
|
||||||
|
invitationsList: [],
|
||||||
|
isLoading: true,
|
||||||
|
});
|
||||||
|
this.listInvitations();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.listInvitations();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleInvitationsModal = () => {
|
||||||
|
this.setState({
|
||||||
|
showInvitationsModal: !this.state.showInvitationsModal
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyTip = () => {
|
||||||
|
return (
|
||||||
|
<div className="message empty-tip">
|
||||||
|
<h2>{gettext('You have not invited any people.')}</h2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InvitationsToolbar
|
<InvitationsToolbar
|
||||||
onShowSidePanel={this.props.onShowSidePanel}
|
onShowSidePanel={this.props.onShowSidePanel}
|
||||||
onSearchedClick={this.props.onSearchedClick}
|
onSearchedClick={this.props.onSearchedClick}
|
||||||
|
toggleInvitationsModal={this.toggleInvitationsModal}
|
||||||
/>
|
/>
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<div className="cur-view-path">
|
<div className="cur-view-path">
|
||||||
<h3 className="sf-heading">{gettext('Invite People')}</h3>
|
<h3 className="sf-heading">{gettext('Invite People')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="message empty-tip">
|
{this.state.isLoading && <Loading/>}
|
||||||
<h2>{gettext('You have not invited any people.')}</h2>
|
{(!this.state.isLoading && this.state.errorMsg !== '') && this.state.errorMsg}
|
||||||
</div>
|
{(!this.state.isLoading && this.state.invitationsList.length !== 0) &&
|
||||||
|
< InvitationsListView
|
||||||
|
invitationsList={this.state.invitationsList}
|
||||||
|
listInvitations={this.listInvitations}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{(!this.state.isLoading && this.state.showEmptyTip && this.state.invitationsList.length === 0) && this.emptyTip()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{this.state.showInvitationsModal &&
|
||||||
|
<InvitePeopleDialog
|
||||||
|
toggleInvitationsModal={this.toggleInvitationsModal}
|
||||||
|
showInvitationsModal={this.state.showInvitationsModal}
|
||||||
|
onInvitePeople={this.onInvitePeople}
|
||||||
|
listInvitations={this.listInvitations}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user