Skip to content

Commit

Permalink
org invite user
Browse files Browse the repository at this point in the history
  • Loading branch information
imwhatiam committed Dec 6, 2023
1 parent 99a95b4 commit 43839aa
Show file tree
Hide file tree
Showing 13 changed files with 290 additions and 49 deletions.
74 changes: 55 additions & 19 deletions frontend/src/components/dialog/org-admin-invite-user-dialog.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { Button, Modal, Input, ModalHeader, ModalBody, ModalFooter, Label, Form, InputGroup, InputGroupAddon, FormGroup } from 'reactstrap';
import { gettext } from '../../utils/constants';
import toaster from '../toast';
import copy from '../copy-to-clipboard';

const propTypes = {
toggle: PropTypes.func.isRequired,
invitationLink: PropTypes.string.isRequired
handleSubmit: PropTypes.func.isRequired,
};

class InviteUserDialog extends React.Component {
class OrgAdminInviteUserDialog extends React.Component {

constructor(props) {
super(props);
this.state = {
email: '',
errMessage: '',
isAddingUser: false,
};
}

copyLink = () => {
copy(this.props.invitationLink);
this.props.toggle();
const message = gettext('Internal link has been copied to clipboard');
toaster.success(message, {
duration: 2
});
handleSubmit = () => {
let isValid = this.validateInputParams();
if (isValid) {
let { email } = this.state;
this.setState({isAddingUser: true});
this.props.handleSubmit(email.trim());
}
}

handleKeyPress = (e) => {
e.preventDefault();
if (e.key == 'Enter') {
this.handleSubmit(e);
}
};

inputEmail = (e) => {
let email = e.target.value.trim();
this.setState({email: email});
}

toggle = () => {
this.props.toggle();
}

validateInputParams() {
let errMessage;
let email = this.state.email.trim();
if (!email.length) {
errMessage = gettext('email is required');
this.setState({errMessage: errMessage});
return false;
}
return true;
}

render() {
return (
<Modal isOpen={true} toggle={this.props.toggle}>
<ModalHeader toggle={this.props.toggle}>{gettext('Invite user')}</ModalHeader>
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Invite users')}</ModalHeader>
<ModalBody>
<p>{gettext('Send the invitation link to the others, and they will be able to join the organization via scanning the QR code.')}</p>
<p>{this.props.invitationLink}</p>
<p>{gettext('You can enter multiple emails, separated by commas. An invitation link will be sent to each user.')}</p>
<Form>
<FormGroup>
<Label for="userEmail">{gettext('Email')}</Label>
<Input id="userEmail" value={this.state.email || ''} onChange={this.inputEmail} />
</FormGroup>
</Form>
{this.state.errMessage && <Label className="err-message">{this.state.errMessage}</Label>}
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.copyLink}>{gettext('Copy')}</Button>
<Button color="primary" disabled={this.state.isAddingUser} onClick={this.handleSubmit} className={this.state.isAddingUser ? 'btn-loading' : ''}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}

InviteUserDialog.propTypes = propTypes;
OrgAdminInviteUserDialog.propTypes = propTypes;

export default InviteUserDialog;
export default OrgAdminInviteUserDialog;
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { gettext } from '../../utils/constants';
import toaster from '../toast';
import copy from '../copy-to-clipboard';

const propTypes = {
toggle: PropTypes.func.isRequired,
invitationLink: PropTypes.string.isRequired
};

class OrgAdminInviteUserViaWeiXinDialog extends React.Component {

constructor(props) {
super(props);
}

copyLink = () => {
copy(this.props.invitationLink);
this.props.toggle();
const message = gettext('Internal link has been copied to clipboard');
toaster.success(message), {
duration: 2
};
}

render() {
return (
<Modal isOpen={true}>
<ModalHeader toggle={this.props.toggle}>{'通过微信邀请用户'}</ModalHeader>
<ModalBody>
<p>{'请将邀请链接发送给其他人,这样他们就可以通过扫描链接里的二维码来加入组织。'}</p>
<p>{this.props.invitationLink}</p>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.copyLink}>{gettext('Copy')}</Button>
</ModalFooter>
</Modal>
);
}
}

OrgAdminInviteUserViaWeiXinDialog.propTypes = propTypes;

export default OrgAdminInviteUserViaWeiXinDialog;
58 changes: 50 additions & 8 deletions frontend/src/pages/org-admin/org-users-users.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import ModalPortal from '../../components/modal-portal';
import ImportOrgUsersDialog from '../../components/dialog/org-import-users-dialog';
import AddOrgUserDialog from '../../components/dialog/org-add-user-dialog';
import InviteUserDialog from '../../components/dialog/org-admin-invite-user-dialog';
import InviteUserViaWeiXinDialog from '../../components/dialog/org-admin-invite-user-via-weixin-dialog';
import toaster from '../../components/toast';
import { seafileAPI } from '../../utils/seafile-api';
import OrgUserInfo from '../../models/org-user';
import { gettext, invitationLink, orgID, siteRoot } from '../../utils/constants';
import { gettext, invitationLink, orgID, siteRoot, orgEnableAdminInviteUser} from '../../utils/constants';
import { Utils } from '../../utils/utils';

class Search extends React.Component {
Expand Down Expand Up @@ -80,7 +81,8 @@ class OrgUsers extends Component {
sortOrder: 'asc',
isShowAddOrgUserDialog: false,
isImportOrgUsersDialogOpen: false,
isInviteUserDialogOpen: false
isInviteUserDialogOpen: false,
isInviteUserViaWeiXinDialogOpen: false
};
}

Expand Down Expand Up @@ -130,6 +132,10 @@ class OrgUsers extends Component {
this.setState({isInviteUserDialogOpen: !this.state.isInviteUserDialogOpen});
};

toggleInviteUserViaWeiXinDialog = () => {
this.setState({isInviteUserViaWeiXinDialogOpen: !this.state.isInviteUserViaWeiXinDialogOpen});
}

initOrgUsersData = (page) => {
const { sortBy, sortOrder } = this.state;
seafileAPI.orgAdminListOrgUsers(orgID, '', page, sortBy, sortOrder).then(res => {
Expand Down Expand Up @@ -202,6 +208,33 @@ class OrgUsers extends Component {
});
};

inviteOrgUser = (emails) => {
seafileAPI.orgAdminInviteOrgUsers(orgID, emails.split(',')).then(res => {
this.toggleInviteUserDialog();
let users = res.data.success.map(user => {
return new OrgUserInfo(user);
});
this.setState({
orgUsers: users.concat(this.state.orgUsers)
});

res.data.success.map(item => {
let msg = gettext('successfully sent email to %s.');
msg = msg.replace('%s', item.email);
toaster.success(msg);
});

res.data.failed.map(item => {
const msg = `${item.email}: ${item.error_msg}`;
toaster.danger(msg);
});
}).catch(error => {
this.toggleInviteUserDialog();
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};

changeStatus= (email, isActive) => {
seafileAPI.orgAdminChangeOrgUserStatus(orgID, email, isActive).then(res => {
let users = this.state.orgUsers.map(item => {
Expand Down Expand Up @@ -234,12 +267,16 @@ class OrgUsers extends Component {
let topbarChildren;
topbarChildren = (
<Fragment>
<button className="btn btn-secondary operation-item" onClick={this.toggleImportOrgUsersDialog}>{gettext('Import Users')}</button>
<button className={topBtn} title={gettext('Add User')} onClick={this.toggleAddOrgUser}>
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Add User')}</button>
<button className="btn btn-secondary operation-item" onClick={this.toggleImportOrgUsersDialog}>{gettext('Import users')}</button>
<button className={topBtn} title={gettext('Add user')} onClick={this.toggleAddOrgUser}>
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Add user')}</button>
{orgEnableAdminInviteUser &&
<button className={topBtn} title={gettext('Invite users')} onClick={this.toggleInviteUserDialog}>
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Invite users')}</button>
}
{invitationLink &&
<button className={topBtn} title={gettext('Invite user')} onClick={this.toggleInviteUserDialog}>
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Invite user')}</button>
<button className={topBtn} title={'通过微信邀请用户'} onClick={this.toggleInviteUserViaWeiXinDialog}>
<i className="fas fa-plus-square text-secondary mr-1"></i>{'通过微信邀请用户'}</button>
}
{this.state.isImportOrgUsersDialogOpen &&
<ModalPortal>
Expand All @@ -253,7 +290,12 @@ class OrgUsers extends Component {
}
{this.state.isInviteUserDialogOpen &&
<ModalPortal>
<InviteUserDialog invitationLink={invitationLink} toggle={this.toggleInviteUserDialog}/>
<InviteUserDialog handleSubmit={this.inviteOrgUser} toggle={this.toggleInviteUserDialog}/>
</ModalPortal>
}
{this.state.isInviteUserViaWeiXinDialogOpen &&
<ModalPortal>
<InviteUserViaWeiXinDialog invitationLink={invitationLink} toggle={this.toggleInviteUserViaWeiXinDialog}/>
</ModalPortal>
}
</Fragment>
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/pages/sys-admin/invitations/invitations.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ class Item extends Component {
case 'Guest':
translateResult = gettext('Guest');
break;
case 'guest':
translateResult = gettext('Guest');
break;
case 'Default':
translateResult = gettext('Default');
break;
case 'default':
translateResult = gettext('Default');
break;
}
return translateResult;
};
Expand Down
1 change: 1 addition & 0 deletions frontend/src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const invitationLink = window.org ? window.org.pageOptions.invitationLink
export const orgMemberQuotaEnabled = window.org ? window.org.pageOptions.orgMemberQuotaEnabled : '';
export const orgEnableAdminCustomLogo = window.org ? window.org.pageOptions.orgEnableAdminCustomLogo === 'True' : false;
export const orgEnableAdminCustomName = window.org ? window.org.pageOptions.orgEnableAdminCustomName === 'True' : false;
export const orgEnableAdminInviteUser = window.org ? window.org.pageOptions.orgEnableAdminInviteUser === 'True' : false;
export const enableMultiADFS = window.org ? window.org.pageOptions.enableMultiADFS === 'True' : false;

// sys admin
Expand Down
19 changes: 10 additions & 9 deletions seahub/invitations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
from seahub.utils import gen_token, get_site_name
from seahub.utils.timeutils import datetime_to_isoformat_timestr
from seahub.utils.mail import send_html_email_with_dj_template
from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE

GUEST = 'Guest'
from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE, \
GUEST_USER, DEFAULT_USER


class InvitationManager(models.Manager):
def add(self, inviter, accepter, invite_type=GUEST):
def add(self, inviter, accepter, invite_type=GUEST_USER):
token = gen_token(max_length=32)
expire_at = timezone.now() + timedelta(hours=int(INVITATIONS_TOKEN_AGE))

Expand All @@ -40,15 +39,16 @@ def get_by_token(self, token):

class Invitation(models.Model):
INVITE_TYPE_CHOICES = (
(GUEST, 'Guest'),
(GUEST_USER, 'guest'),
(DEFAULT_USER, 'default'),
)

token = models.CharField(max_length=40, db_index=True)
inviter = LowerCaseCharField(max_length=255, db_index=True)
accepter = LowerCaseCharField(max_length=255)
invite_type = models.CharField(max_length=20,
choices=INVITE_TYPE_CHOICES,
default=GUEST)
default=GUEST_USER)
invite_time = models.DateTimeField(auto_now_add=True)
accept_time = models.DateTimeField(null=True, blank=True)
expire_time = models.DateTimeField()
Expand Down Expand Up @@ -77,12 +77,12 @@ def to_dict(self):
}

def is_guest(self):
return self.invite_type == GUEST
return self.invite_type == GUEST_USER

def is_expired(self):
return timezone.now() >= self.expire_time

def send_to(self, email=None):
def send_to(self, email=None, org_name=None):
"""
Send an invitation email to ``email``.
"""
Expand All @@ -91,8 +91,9 @@ def send_to(self, email=None):

context = self.to_dict()
context['site_name'] = get_site_name()
context['org_name'] = org_name

subject = _('You are invited to join %(site_name)s.') % {'site_name': get_site_name()}
subject = _(f'You are invited to join team {org_name}.')

return send_html_email_with_dj_template(email,
subject=subject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
<p style="color:#121214;font-size:14px;">{% trans "Hi," %}</p>

<p style="font-size:14px;color:#434144;">
{% blocktrans with inviter_name=inviter|email2nickname %}{{ inviter_name }} invited you to join {{ site_name }}. Please click the link below:{% endblocktrans %}
{% if org_name %}
{% blocktrans with org_name=org_name|escape %}You are invited to join team {{ org_name }}. Please click the link below:{% endblocktrans %}
{% else %}
{% blocktrans with inviter_name=inviter|email2nickname|escape %}{{ inviter_name }} invited you to join {{ site_name }}. Please click the link below:{% endblocktrans %}
{% endif %}
</p>

<a href="{{ url_base }}{% url 'invitations:token_view' token %}" target="_blank">{{ url_base }}{% url 'invitations:token_view' token %}</a>
Expand Down
Loading

0 comments on commit 43839aa

Please sign in to comment.