This commit is contained in:
Tom Moor
2019-12-22 14:54:12 -08:00
committed by GitHub
parent adf323713e
commit 6bf2069fa7
8 changed files with 52 additions and 11 deletions

View File

@ -49,7 +49,7 @@ const Modal = ({
isOpen={isOpen}
{...rest}
>
<Content column>
<Content onClick={ev => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
<Close onClick={onRequestClose}>
<CloseIcon size={40} color="currentColor" />

View File

@ -38,7 +38,14 @@ const MemberListItem = ({
title={user.name}
subtitle={
<React.Fragment>
Joined <Time dateTime={user.createdAt} /> ago
{user.lastActiveAt ? (
<React.Fragment>
Active <Time dateTime={user.lastActiveAt} /> ago
</React.Fragment>
) : (
'Never signed in'
)}
{!user.lastActiveAt && <Badge>Invited</Badge>}
{user.isAdmin && <Badge admin={user.isAdmin}>Admin</Badge>}
</React.Fragment>
}

View File

@ -1,8 +1,10 @@
// @flow
import * as React from 'react';
import { PlusIcon } from 'outline-icons';
import Time from 'shared/components/Time';
import Avatar from 'components/Avatar';
import Button from 'components/Button';
import Badge from 'components/Badge';
import ListItem from 'components/List/Item';
import User from 'models/User';
@ -17,6 +19,19 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
<ListItem
title={user.name}
image={<Avatar src={user.avatarUrl} size={32} />}
subtitle={
<React.Fragment>
{user.lastActiveAt ? (
<React.Fragment>
Active <Time dateTime={user.lastActiveAt} /> ago
</React.Fragment>
) : (
'Never signed in'
)}
{!user.lastActiveAt && <Badge>Invited</Badge>}
{user.isAdmin && <Badge admin={user.isAdmin}>Admin</Badge>}
</React.Fragment>
}
actions={
canEdit ? (
<Button type="button" onClick={onAdd} icon={<PlusIcon />} neutral>

View File

@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import { Link, withRouter, type RouterHistory } from 'react-router-dom';
import { observable } from 'mobx';
import { observable, action } from 'mobx';
import { inject, observer } from 'mobx-react';
import { CloseIcon } from 'outline-icons';
import styled from 'styled-components';
@ -30,12 +30,18 @@ type Props = {
onSubmit: () => void,
};
type InviteRequest = {
email: string,
name: string,
guest: boolean,
};
@observer
class Invite extends React.Component<Props> {
@observable isSaving: boolean;
@observable linkCopied: boolean = false;
@observable
invites: { email: string, name: string, guest: boolean }[] = [
invites: InviteRequest[] = [
{ email: '', name: '', guest: false },
{ email: '', name: '', guest: false },
{ email: '', name: '', guest: false },
@ -56,14 +62,17 @@ class Invite extends React.Component<Props> {
}
};
@action
handleChange = (ev, index) => {
this.invites[index][ev.target.name] = ev.target.value;
};
@action
handleGuestChange = (ev, index) => {
this.invites[index][ev.target.name] = ev.target.checked;
};
@action
handleAdd = () => {
if (this.invites.length >= MAX_INVITES) {
this.props.ui.showToast(
@ -74,6 +83,7 @@ class Invite extends React.Component<Props> {
this.invites.push({ email: '', name: '', guest: false });
};
@action
handleRemove = (ev: SyntheticEvent<>, index: number) => {
ev.preventDefault();
this.invites.splice(index, 1);
@ -115,7 +125,7 @@ class Invite extends React.Component<Props> {
<CopyBlock>
Want a link to share directly with your team?
<Flex>
<Input type="text" value={team.url} flex />&nbsp;&nbsp;
<Input type="text" value={team.url} readOnly flex />&nbsp;&nbsp;
<CopyToClipboard text={team.url} onCopy={this.handleCopy}>
<Button type="button" neutral>
{this.linkCopied ? 'Link copied' : 'Copy link'}

View File

@ -56,7 +56,7 @@ class UserListItem extends React.Component<Props> {
Active <Time dateTime={user.lastActiveAt} /> ago
</React.Fragment>
) : (
'Pending'
'Invited'
)}
{user.isAdmin && <Badge admin={user.isAdmin}>Admin</Badge>}
{user.isSuspended && <Badge>Suspended</Badge>}

View File

@ -69,6 +69,9 @@ export default class UsersStore extends BaseStore<User> {
invite = async (invites: { email: string, name: string }[]) => {
const res = await client.post(`/users.invite`, { invites });
invariant(res && res.data, 'Data should be available');
runInAction(`invite`, () => {
res.data.users.forEach(this.add);
});
return res.data;
};

View File

@ -246,10 +246,13 @@ router.post('users.invite', auth(), async ctx => {
const user = ctx.state.user;
authorize(user, 'invite', User);
const invitesSent = await userInviter({ user, invites, ip: ctx.request.ip });
const response = await userInviter({ user, invites, ip: ctx.request.ip });
ctx.body = {
data: invitesSent,
data: {
sent: response.sent,
users: response.users.map(user => presentUser(user)),
},
};
});

View File

@ -14,7 +14,7 @@ export default async function userInviter({
user: User,
invites: Invite[],
ip: string,
}): Promise<{ sent: Invite[] }> {
}): Promise<{ sent: Invite[], users: User[] }> {
const team = await Team.findByPk(user.teamId);
// filter out empties and obvious non-emails
@ -44,12 +44,14 @@ export default async function userInviter({
invite => !existingEmails.includes(invite.email)
);
let users = [];
// send and record remaining invites
await Promise.all(
filteredInvites.map(async invite => {
const transaction = await sequelize.transaction();
try {
await User.create(
const newUser = await User.create(
{
teamId: user.teamId,
name: invite.name,
@ -58,6 +60,7 @@ export default async function userInviter({
},
{ transaction }
);
users.push(newUser);
await Event.create(
{
name: 'users.invite',
@ -88,5 +91,5 @@ export default async function userInviter({
})
);
return { sent: filteredInvites };
return { sent: filteredInvites, users };
}