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} isOpen={isOpen}
{...rest} {...rest}
> >
<Content column> <Content onClick={ev => ev.stopPropagation()} column>
{title && <h1>{title}</h1>} {title && <h1>{title}</h1>}
<Close onClick={onRequestClose}> <Close onClick={onRequestClose}>
<CloseIcon size={40} color="currentColor" /> <CloseIcon size={40} color="currentColor" />

View File

@ -38,7 +38,14 @@ const MemberListItem = ({
title={user.name} title={user.name}
subtitle={ subtitle={
<React.Fragment> <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>} {user.isAdmin && <Badge admin={user.isAdmin}>Admin</Badge>}
</React.Fragment> </React.Fragment>
} }

View File

@ -1,8 +1,10 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { PlusIcon } from 'outline-icons'; import { PlusIcon } from 'outline-icons';
import Time from 'shared/components/Time';
import Avatar from 'components/Avatar'; import Avatar from 'components/Avatar';
import Button from 'components/Button'; import Button from 'components/Button';
import Badge from 'components/Badge';
import ListItem from 'components/List/Item'; import ListItem from 'components/List/Item';
import User from 'models/User'; import User from 'models/User';
@ -17,6 +19,19 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
<ListItem <ListItem
title={user.name} title={user.name}
image={<Avatar src={user.avatarUrl} size={32} />} 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={ actions={
canEdit ? ( canEdit ? (
<Button type="button" onClick={onAdd} icon={<PlusIcon />} neutral> <Button type="button" onClick={onAdd} icon={<PlusIcon />} neutral>

View File

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

View File

@ -56,7 +56,7 @@ class UserListItem extends React.Component<Props> {
Active <Time dateTime={user.lastActiveAt} /> ago Active <Time dateTime={user.lastActiveAt} /> ago
</React.Fragment> </React.Fragment>
) : ( ) : (
'Pending' 'Invited'
)} )}
{user.isAdmin && <Badge admin={user.isAdmin}>Admin</Badge>} {user.isAdmin && <Badge admin={user.isAdmin}>Admin</Badge>}
{user.isSuspended && <Badge>Suspended</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 }[]) => { invite = async (invites: { email: string, name: string }[]) => {
const res = await client.post(`/users.invite`, { invites }); const res = await client.post(`/users.invite`, { invites });
invariant(res && res.data, 'Data should be available'); invariant(res && res.data, 'Data should be available');
runInAction(`invite`, () => {
res.data.users.forEach(this.add);
});
return res.data; return res.data;
}; };

View File

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