@ -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" />
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 />
|
<Input type="text" value={team.url} readOnly flex />
|
||||||
<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'}
|
||||||
|
@ -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>}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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)),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user