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:
@@ -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>}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 team’s 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;
|
||||
13
app/components/VisuallyHidden.js
Normal file
13
app/components/VisuallyHidden.js
Normal 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;
|
||||
Reference in New Issue
Block a user