feat: invites (#967)

* stub invite endpoint

* feat: First pass invite UI

* feat: allow removing invite rows

* First pass: sending logic

* fix: label accessibility

* fix: add button submits
incorrect permissions
middleware flow error

* 💚

* Error handling, email filtering, tests

* Flow

* Add Invite to people page
Remove old Tip

* Add copy link to subdomain
This commit is contained in:
Tom Moor
2019-06-24 22:14:59 -07:00
committed by GitHub
parent f406faf08e
commit d5192acabf
21 changed files with 509 additions and 103 deletions

View File

@@ -114,7 +114,7 @@ export default function Button({
const hasIcon = icon !== undefined;
return (
<RealButton small={small} {...rest}>
<RealButton small={small} type={type} {...rest}>
<Inner hasIcon={hasIcon} small={small} disclosure={disclosure}>
{hasIcon && icon}
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}

View File

@@ -3,6 +3,7 @@ import * as React from 'react';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
import styled from 'styled-components';
import VisuallyHidden from 'components/VisuallyHidden';
import Flex from 'shared/components/Flex';
const RealTextarea = styled.textarea`
@@ -38,9 +39,10 @@ const RealInput = styled.input`
`;
const Wrapper = styled.div`
flex: ${props => (props.flex ? '1' : '0')};
max-width: ${props => (props.short ? '350px' : '100%')};
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : '0')};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'auto')};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'initial')};
`;
export const Outline = styled(Flex)`
@@ -70,6 +72,8 @@ export type Props = {
value?: string,
label?: string,
className?: string,
labelHidden?: boolean,
flex?: boolean,
short?: boolean,
};
@@ -86,14 +90,28 @@ class Input extends React.Component<Props> {
};
render() {
const { type = 'text', label, className, short, ...rest } = this.props;
const {
type = 'text',
label,
className,
short,
flex,
labelHidden,
...rest
} = this.props;
const InputComponent = type === 'textarea' ? RealTextarea : RealInput;
const wrappedLabel = <LabelText>{label}</LabelText>;
return (
<Wrapper className={className} short={short}>
<Wrapper className={className} short={short} flex={flex}>
<label>
{label && <LabelText>{label}</LabelText>}
{label &&
(labelHidden ? (
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
) : (
wrappedLabel
))}
<Outline focused={this.focused}>
<InputComponent
onBlur={this.handleBlur}

View File

@@ -7,9 +7,12 @@ import {
EditIcon,
SearchIcon,
StarredIcon,
PlusIcon,
} from 'outline-icons';
import Flex from 'shared/components/Flex';
import Modal from 'components/Modal';
import Invite from 'scenes/Invite';
import AccountMenu from 'menus/AccountMenu';
import Sidebar from './Sidebar';
import Scrollable from 'components/Scrollable';
@@ -22,6 +25,7 @@ import Bubble from './components/Bubble';
import AuthStore from 'stores/AuthStore';
import DocumentsStore from 'stores/DocumentsStore';
import UiStore from 'stores/UiStore';
import { observable } from 'mobx';
type Props = {
auth: AuthStore,
@@ -31,6 +35,8 @@ type Props = {
@observer
class MainSidebar extends React.Component<Props> {
@observable inviteModalOpen: boolean = false;
componentDidMount() {
this.props.documents.fetchDrafts();
}
@@ -39,6 +45,14 @@ class MainSidebar extends React.Component<Props> {
this.props.ui.setActiveModal('collection-new');
};
handleInviteModalOpen = () => {
this.inviteModalOpen = true;
};
handleInviteModalClose = () => {
this.inviteModalOpen = false;
};
render() {
const { auth, documents } = this.props;
const { user, team } = auth;
@@ -110,9 +124,23 @@ class MainSidebar extends React.Component<Props> {
documents.active ? documents.active.isArchived : undefined
}
/>
{user.isAdmin && (
<SidebarLink
onClick={this.handleInviteModalOpen}
icon={<PlusIcon />}
label="Invite people…"
/>
)}
</Section>
</Scrollable>
</Flex>
<Modal
title="Invite people"
onRequestClose={this.handleInviteModalClose}
isOpen={this.inviteModalOpen}
>
<Invite onSubmit={this.handleInviteModalClose} />
</Modal>
</Sidebar>
);
}

View File

@@ -1,61 +0,0 @@
// @flow
import * as React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled from 'styled-components';
import Tip from './Tip';
import CopyToClipboard from './CopyToClipboard';
import Team from '../models/Team';
type Props = {
team: Team,
disabled: boolean,
};
@observer
class TipInvite extends React.Component<Props> {
@observable linkCopied: boolean = false;
handleCopy = () => {
this.linkCopied = true;
};
render() {
const { team, disabled } = this.props;
if (disabled) return null;
return (
<Tip id="subdomain-invite">
<Heading>Looking to invite your team?</Heading>
<Paragraph>
Your teammates can sign in with{' '}
{team.slackConnected ? 'Slack' : 'Google'} to join this knowledgebase
at your teams own subdomain ({team.url.replace(/^https?:\/\//, '')})
{' '}
<CopyToClipboard text={team.url} onCopy={this.handleCopy}>
<a>
{this.linkCopied
? 'link copied to clipboard!'
: 'copy a link to share.'}
</a>
</CopyToClipboard>
</Paragraph>
</Tip>
);
}
}
const Heading = styled.h3`
margin: 0.25em 0 0.5em 0;
`;
const Paragraph = styled.p`
margin: 0.25em 0;
a {
color: ${props => props.theme.text};
text-decoration: underline;
}
`;
export default TipInvite;

View File

@@ -0,0 +1,13 @@
// @flow
import styled from 'styled-components';
const VisuallyHidden = styled('span')`
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
`;
export default VisuallyHidden;