Update master

This commit is contained in:
Tom Moor
2017-10-21 12:22:02 -07:00
59 changed files with 691 additions and 677 deletions

View File

@@ -1,5 +1,5 @@
// @flow
import React from 'react';
import React, { Component } from 'react';
import invariant from 'invariant';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
@@ -10,14 +10,14 @@ import { color } from 'styles/constants';
import { fadeAndScaleIn } from 'styles/animations';
type Props = {
label: React.Element<any>,
onShow?: () => void,
label: React.Element<*>,
onOpen?: () => void,
onClose?: () => void,
children?: React.Element<any>,
children?: React.Element<*>,
style?: Object,
};
@observer class DropdownMenu extends React.Component {
@observer class DropdownMenu extends Component {
props: Props;
actionRef: Object;
@observable open: boolean = false;
@@ -37,7 +37,7 @@ type Props = {
this.open = true;
this.top = targetRect.bottom - bodyRect.top;
this.right = bodyRect.width - targetRect.left - targetRect.width;
if (this.props.onShow) this.props.onShow();
if (this.props.onOpen) this.props.onOpen();
}
};

View File

@@ -7,7 +7,7 @@ const DropdownMenuItem = ({
onClick,
children,
}: {
onClick?: () => void,
onClick?: SyntheticEvent => void,
children?: React.Element<any>,
}) => {
return (
@@ -24,11 +24,15 @@ const MenuItem = styled.div`
color: ${color.slateDark};
display: flex;
justify-content: space-between;
justify-content: left;
align-items: center;
cursor: pointer;
font-size: 15px;
svg {
margin-right: 8px;
}
a {
text-decoration: none;
width: 100%;

View File

@@ -9,6 +9,7 @@ import getDataTransferFiles from 'utils/getDataTransferFiles';
import Flex from 'components/Flex';
import ClickablePadding from './components/ClickablePadding';
import Toolbar from './components/Toolbar';
import BlockInsert from './components/BlockInsert';
import Placeholder from './components/Placeholder';
import Contents from './components/Contents';
import Markdown from './serializer';
@@ -24,7 +25,7 @@ type Props = {
onCancel: Function,
onImageUploadStart: Function,
onImageUploadStop: Function,
emoji: string,
emoji?: string,
readOnly: boolean,
};
@@ -172,6 +173,8 @@ type KeyData = {
};
render = () => {
const { readOnly, emoji, onSave } = this.props;
return (
<Flex
onDrop={this.handleDrop}
@@ -182,25 +185,32 @@ type KeyData = {
auto
>
<MaxWidth column auto>
<Header onClick={this.focusAtStart} readOnly={this.props.readOnly} />
<Toolbar state={this.editorState} onChange={this.onChange} />
<Header onClick={this.focusAtStart} readOnly={readOnly} />
<Contents state={this.editorState} />
{!readOnly &&
<Toolbar state={this.editorState} onChange={this.onChange} />}
{!readOnly &&
<BlockInsert
state={this.editorState}
onChange={this.onChange}
onInsertImage={this.insertImageFile}
/>}
<StyledEditor
innerRef={ref => (this.editor = ref)}
placeholder="Start with a title…"
bodyPlaceholder="…the rest is your canvas"
schema={this.schema}
plugins={this.plugins}
emoji={this.props.emoji}
emoji={emoji}
state={this.editorState}
onKeyDown={this.onKeyDown}
onChange={this.onChange}
onDocumentChange={this.onDocumentChange}
onSave={this.props.onSave}
readOnly={this.props.readOnly}
onSave={onSave}
readOnly={readOnly}
/>
<ClickablePadding
onClick={!this.props.readOnly ? this.focusAtEnd : undefined}
onClick={!readOnly ? this.focusAtEnd : undefined}
grow
/>
</MaxWidth>
@@ -281,6 +291,10 @@ const StyledEditor = styled(Editor)`
position: relative;
}
a:hover {
text-decoration: ${({ readOnly }) => (readOnly ? 'underline' : 'none')};
}
li p {
display: inline;
margin: 0;
@@ -322,6 +336,10 @@ const StyledEditor = styled(Editor)`
td {
padding: 5px 20px 5px 0;
}
b, strong {
font-weight: 600;
}
`;
export default MarkdownEditor;

View File

@@ -0,0 +1,197 @@
// @flow
import React, { Component } from 'react';
import EditList from '../plugins/EditList';
import getDataTransferFiles from 'utils/getDataTransferFiles';
import Portal from 'react-portal';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled from 'styled-components';
import { color } from 'styles/constants';
import Icon from 'components/Icon';
import BlockMenu from 'menus/BlockMenu';
import type { State } from '../types';
const { transforms } = EditList;
type Props = {
state: State,
onChange: Function,
onInsertImage: File => Promise<*>,
};
@observer
export default class BlockInsert extends Component {
props: Props;
mouseMoveTimeout: number;
file: HTMLInputElement;
@observable active: boolean = false;
@observable menuOpen: boolean = false;
@observable top: number;
@observable left: number;
@observable mouseX: number;
componentDidMount = () => {
this.update();
window.addEventListener('mousemove', this.handleMouseMove);
};
componentWillUpdate = (nextProps: Props) => {
this.update(nextProps);
};
componentWillUnmount = () => {
window.removeEventListener('mousemove', this.handleMouseMove);
};
setInactive = () => {
if (this.menuOpen) return;
this.active = false;
};
handleMouseMove = (ev: SyntheticMouseEvent) => {
const windowWidth = window.innerWidth / 3;
let active = ev.clientX < windowWidth;
if (active !== this.active) {
this.active = active || this.menuOpen;
}
if (active) {
clearTimeout(this.mouseMoveTimeout);
this.mouseMoveTimeout = setTimeout(this.setInactive, 2000);
}
};
handleMenuOpen = () => {
this.menuOpen = true;
};
handleMenuClose = () => {
this.menuOpen = false;
};
update = (props?: Props) => {
if (!document.activeElement) return;
const { state } = props || this.props;
const boxRect = document.activeElement.getBoundingClientRect();
const selection = window.getSelection();
if (!selection.focusNode) return;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
if (rect.top <= 0 || boxRect.left <= 0) return;
if (state.startBlock.type === 'heading1') {
this.active = false;
}
this.top = Math.round(rect.top + window.scrollY);
this.left = Math.round(boxRect.left + window.scrollX - 20);
};
insertBlock = (
ev: SyntheticEvent,
options: {
type: string | Object,
wrapper?: string | Object,
append?: string | Object,
}
) => {
ev.preventDefault();
const { type, wrapper, append } = options;
let { state } = this.props;
let transform = state.transform();
const { document } = state;
const parent = document.getParent(state.startBlock.key);
// lists get some special treatment
if (parent && parent.type === 'list-item') {
transform = transforms.unwrapList(
transforms
.splitListItem(transform.collapseToStart())
.collapseToEndOfPreviousBlock()
);
}
transform = transform.insertBlock(type);
if (wrapper) transform = transform.wrapBlock(wrapper);
if (append) transform = transform.insertBlock(append);
state = transform.focus().apply();
this.props.onChange(state);
this.active = false;
};
onPickImage = (ev: SyntheticEvent) => {
// simulate a click on the file upload input element
this.file.click();
};
onChooseImage = async (ev: SyntheticEvent) => {
const files = getDataTransferFiles(ev);
for (const file of files) {
await this.props.onInsertImage(file);
}
};
render() {
const style = { top: `${this.top}px`, left: `${this.left}px` };
const todo = { type: 'list-item', data: { checked: false } };
const rule = { type: 'horizontal-rule', isVoid: true };
return (
<Portal isOpened>
<Trigger active={this.active} style={style}>
<HiddenInput
type="file"
innerRef={ref => (this.file = ref)}
onChange={this.onChooseImage}
accept="image/*"
/>
<BlockMenu
label={<Icon type="PlusCircle" />}
onPickImage={this.onPickImage}
onInsertList={ev =>
this.insertBlock(ev, {
type: 'list-item',
wrapper: 'bulleted-list',
})}
onInsertTodoList={ev =>
this.insertBlock(ev, { type: todo, wrapper: 'todo-list' })}
onInsertBreak={ev =>
this.insertBlock(ev, { type: rule, append: 'paragraph' })}
onOpen={this.handleMenuOpen}
onClose={this.handleMenuClose}
/>
</Trigger>
</Portal>
);
}
}
const HiddenInput = styled.input`
position: absolute;
top: -100px;
left: -100px;
visibility: hidden;
`;
const Trigger = styled.div`
position: absolute;
z-index: 1;
opacity: 0;
background-color: ${color.white};
border-radius: 4px;
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
line-height: 0;
height: 16px;
width: 16px;
transform: scale(.9);
${({ active }) => active && `
transform: scale(1);
opacity: .9;
`}
`;

View File

@@ -0,0 +1,17 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import type { Props } from '../types';
import { color } from 'styles/constants';
function HorizontalRule(props: Props) {
const { state, node } = props;
const active = state.isFocused && state.selection.hasEdgeIn(node);
return <StyledHr active={active} />;
}
const StyledHr = styled.hr`
border-bottom: 1px solid ${props => (props.active ? color.slate : color.slateLight)};
`;
export default HorizontalRule;

View File

@@ -9,7 +9,6 @@ import Heading2Icon from 'components/Icon/Heading2Icon';
import ItalicIcon from 'components/Icon/ItalicIcon';
import LinkIcon from 'components/Icon/LinkIcon';
import StrikethroughIcon from 'components/Icon/StrikethroughIcon';
import BulletedListIcon from 'components/Icon/BulletedListIcon';
export default class FormattingToolbar extends Component {
props: {
@@ -95,7 +94,6 @@ export default class FormattingToolbar extends Component {
{this.renderMarkButton('deleted', StrikethroughIcon)}
{this.renderBlockButton('heading1', Heading1Icon)}
{this.renderBlockButton('heading2', Heading2Icon)}
{this.renderBlockButton('bulleted-list', BulletedListIcon)}
{this.renderMarkButton('code', CodeIcon)}
<ToolbarButton onMouseDown={this.onCreateLink}>
<LinkIcon light />

View File

@@ -15,7 +15,6 @@ export default async function insertImageFile(
try {
// load the file as a data URL
const id = uuid.v4();
const alt = file.name;
const reader = new FileReader();
reader.addEventListener('load', () => {
const src = reader.result;
@@ -25,7 +24,7 @@ export default async function insertImageFile(
.insertBlock({
type: 'image',
isVoid: true,
data: { src, alt, id, loading: true },
data: { src, id, loading: true },
})
.apply();
editor.onChange(state);
@@ -46,7 +45,7 @@ export default async function insertImageFile(
);
return finalTransform.setNodeByKey(placeholder.key, {
data: { src, alt, loading: false },
data: { src, loading: false },
});
} catch (err) {
throw err;

View File

@@ -1,11 +1,11 @@
// @flow
import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images';
import PasteLinkify from 'slate-paste-linkify';
import EditList from 'slate-edit-list';
import CollapseOnEscape from 'slate-collapse-on-escape';
import TrailingBlock from 'slate-trailing-block';
import EditCode from 'slate-edit-code';
import Prism from 'slate-prism';
import EditList from './plugins/EditList';
import KeyboardShortcuts from './plugins/KeyboardShortcuts';
import MarkdownShortcuts from './plugins/MarkdownShortcuts';
import insertImage from './insertImage';
@@ -35,10 +35,7 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => {
);
},
}),
EditList({
types: ['ordered-list', 'bulleted-list', 'todo-list'],
typeItem: 'list-item',
}),
EditList,
EditCode({
onlyIn: onlyInCode,
containerType: 'code',

View File

@@ -0,0 +1,7 @@
// @flow
import EditList from 'slate-edit-list';
export default EditList({
types: ['ordered-list', 'bulleted-list', 'todo-list'],
typeItem: 'list-item',
});

View File

@@ -112,19 +112,17 @@ export default function MarkdownShortcuts() {
if (chars === '--') {
ev.preventDefault();
const transform = state
return state
.transform()
.extendToStartOf(startBlock)
.delete()
.setBlock({
type: 'horizontal-rule',
isVoid: true,
});
state = transform
})
.collapseToStartOfNextBlock()
.insertBlock('paragraph')
.apply();
return state;
}
},

View File

@@ -1,6 +1,7 @@
// @flow
import React from 'react';
import Code from './components/Code';
import HorizontalRule from './components/HorizontalRule';
import InlineCode from './components/InlineCode';
import Image from './components/Image';
import Link from './components/Link';
@@ -33,7 +34,7 @@ const createSchema = () => {
'block-quote': (props: Props) => (
<blockquote>{props.children}</blockquote>
),
'horizontal-rule': (props: Props) => <hr />,
'horizontal-rule': HorizontalRule,
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
'todo-list': (props: Props) => <TodoList>{props.children}</TodoList>,

View File

@@ -0,0 +1,21 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import { color } from 'styles/constants';
type Props = {
children: string,
};
const Empty = (props: Props) => {
const { children, ...rest } = props;
return <Container {...rest}>{children}</Container>;
};
const Container = styled.div`
display: flex;
color: ${color.slate};
text-align: center;
`;
export default Empty;

View File

@@ -0,0 +1,3 @@
// @flow
import Empty from './Empty';
export default Empty;

View File

@@ -1,84 +0,0 @@
// @flow
import styled from 'styled-components';
const HtmlContent = styled.div`
h1, h2, h3, h4, h5, h6 {
:global {
.anchor {
visibility: hidden;
color: ;
}
}
&:hover {
:global {
.anchor {
visibility: visible;
}
}
}
}
ul {
padding-left: 1.5em;
ul {
margin: 0;
}
}
blockquote {
font-style: italic;
border-left: 2px solid $lightGray;
padding-left: 0.8em;
}
table {
width: 100%;
overflow: auto;
display: block;
border-spacing: 0;
border-collapse: collapse;
thead, tbody {
width: 100%;
}
thead {
tr {
border-bottom: 2px solid $lightGray;
}
}
tbody {
tr {
border-bottom: 1px solid $lightGray;
}
}
tr {
background-color: #fff;
// &:nth-child(2n) {
// background-color: #f8f8f8;
// }
}
th, td {
text-align: left;
border: 1px 0 solid $lightGray;
padding: 5px 20px 5px 0;
&:last-child {
padding-right: 0;
width: 100%;
}
}
th {
font-weight: bold;
}
}
`;
export default HtmlContent;

View File

@@ -1,3 +0,0 @@
// @flow
import HtmlContent from './HtmlContent';
export default HtmlContent;

View File

@@ -95,7 +95,7 @@ type Props = {
<CollectionMenu
history={history}
collection={collection}
onShow={() => (this.menuOpen = true)}
onOpen={() => (this.menuOpen = true)}
onClose={() => (this.menuOpen = false)}
onImport={this.handleImport}
open={this.menuOpen}

View File

@@ -16,7 +16,7 @@ const activeStyle = {
const StyleableDiv = props => <div {...props} />;
const styleComponent = component => styled(component)`
display: block;
display: flex;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
@@ -42,7 +42,7 @@ function SidebarLink(props: Object) {
<Flex>
<Component exact activeStyle={activeStyle} {...props}>
{props.hasChildren && <StyledChevron expanded={props.expanded} />}
{props.children}
<Content>{props.children}</Content>
</Component>
</Flex>
);
@@ -62,4 +62,8 @@ const StyledChevron = styled(ChevronIcon)`
}
`;
const Content = styled.div`
width: 100%;
`;
export default SidebarLink;

View File

@@ -73,6 +73,15 @@ const Auth = ({ children }: AuthProps) => {
}),
};
if (window.Bugsnag) {
Bugsnag.user = {
id: user.id,
name: user.name,
teamId: team.id,
team: team.name,
};
}
authenticatedStores.collections.fetchAll();
}

View File

@@ -0,0 +1,49 @@
// @flow
import React, { Component } from 'react';
import Icon from 'components/Icon';
import { observer } from 'mobx-react';
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
@observer class BlockMenu extends Component {
props: {
label?: React$Element<*>,
onPickImage: SyntheticEvent => void,
onInsertList: SyntheticEvent => void,
onInsertTodoList: SyntheticEvent => void,
onInsertBreak: SyntheticEvent => void,
};
render() {
const {
label,
onPickImage,
onInsertList,
onInsertTodoList,
onInsertBreak,
...rest
} = this.props;
return (
<DropdownMenu
style={{ marginRight: -70, marginTop: 5 }}
label={label}
{...rest}
>
<DropdownMenuItem onClick={onPickImage}>
<Icon type="Image" /> Add images
</DropdownMenuItem>
<DropdownMenuItem onClick={onInsertList}>
<Icon type="List" /> Start list
</DropdownMenuItem>
<DropdownMenuItem onClick={onInsertTodoList}>
<Icon type="CheckSquare" /> Start checklist
</DropdownMenuItem>
<DropdownMenuItem onClick={onInsertBreak}>
<Icon type="Minus" /> Add break
</DropdownMenuItem>
</DropdownMenu>
);
}
}
export default BlockMenu;

View File

@@ -12,7 +12,7 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
@observer class CollectionMenu extends Component {
props: {
label?: React$Element<any>,
onShow?: () => void,
onOpen?: () => void,
onClose?: () => void,
onImport?: () => void,
history: Object,
@@ -36,13 +36,13 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
};
render() {
const { collection, label, onShow, onClose, onImport } = this.props;
const { collection, label, onOpen, onClose, onImport } = this.props;
const { allowDelete } = collection;
return (
<DropdownMenu
label={label || <MoreIcon type="MoreHorizontal" />}
onShow={onShow}
onOpen={onOpen}
onClose={onClose}
>
{collection &&

View File

@@ -2,30 +2,31 @@
import React from 'react';
import { observer } from 'mobx-react';
import CenteredContent from 'components/CenteredContent';
import HtmlContent from 'components/HtmlContent';
import Editor from 'components/Editor';
import PageTitle from 'components/PageTitle';
import { convertToMarkdown } from 'utils/markdown';
type Props = {
title: string,
content: string,
};
@observer class Flatpage extends React.Component {
props: Props;
const Flatpage = observer((props: Props) => {
const { title, content } = props;
render() {
const { title, content } = this.props;
const htmlContent = convertToMarkdown(content);
return (
<CenteredContent>
<PageTitle title={title} />
<HtmlContent dangerouslySetInnerHTML={{ __html: htmlContent }} />
</CenteredContent>
);
}
}
return (
<CenteredContent>
<PageTitle title={title} />
<Editor
text={content}
onChange={() => {}}
onSave={() => {}}
onCancel={() => {}}
onImageUploadStart={() => {}}
onImageUploadStop={() => {}}
readOnly
/>
</CenteredContent>
);
});
export default Flatpage;

View File

@@ -12,6 +12,7 @@ import { searchUrl } from 'utils/routeHelpers';
import styled from 'styled-components';
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
import Empty from 'components/Empty';
import Flex from 'components/Flex';
import CenteredContent from 'components/CenteredContent';
import LoadingIndicator from 'components/LoadingIndicator';
@@ -57,7 +58,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
firstDocument: HTMLElement;
props: Props;
@observable resultIds: Array<string> = []; // Document IDs
@observable resultIds: string[] = []; // Document IDs
@observable searchTerm: ?string = null;
@observable isFetching = false;
@@ -131,18 +132,19 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
}
render() {
const { documents } = this.props;
const { documents, notFound } = this.props;
const query = this.props.match.params.query;
const hasResults = this.resultIds.length > 0;
const showEmpty = !this.isFetching && this.searchTerm && !hasResults;
return (
<Container auto>
<PageTitle title={this.title} />
{this.isFetching && <LoadingIndicator />}
{this.props.notFound &&
{notFound &&
<div>
<h1>Not Found</h1>
<p>We're unable to find the page you're accessing.</p>
<p>Were unable to find the page youre accessing.</p>
</div>}
<ResultsWrapper pinToTop={hasResults} column auto>
<SearchField
@@ -151,6 +153,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
onChange={this.updateQuery}
value={query || ''}
/>
{showEmpty && <Empty>Oop, no matching documents.</Empty>}
<ResultList visible={hasResults}>
<StyledArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}

View File

@@ -5,22 +5,8 @@ import Flex from 'components/Flex';
import { color } from 'styles/constants';
import styled from 'styled-components';
const Field = styled.input`
width: 100%;
padding: 10px;
font-size: 48px;
font-weight: 400;
outline: none;
border: 0;
::-webkit-input-placeholder { color: ${color.slate}; }
:-moz-placeholder { color: ${color.slate}; }
::-moz-placeholder { color: ${color.slate}; }
:-ms-input-placeholder { color: ${color.slate}; }
`;
class SearchField extends Component {
input: HTMLElement;
input: HTMLInputElement;
props: {
onChange: Function,
};
@@ -33,23 +19,24 @@ class SearchField extends Component {
this.input.focus();
};
setRef = (ref: HTMLElement) => {
setRef = (ref: HTMLInputElement) => {
this.input = ref;
};
render() {
return (
<Flex align="center">
<Icon
<StyledIcon
type="Search"
size={48}
color="#C9CFD6"
size={46}
color={color.slateLight}
onClick={this.focusInput}
/>
<Field
<StyledInput
{...this.props}
innerRef={this.setRef}
onChange={this.handleChange}
spellCheck="false"
placeholder="Search…"
autoFocus
/>
@@ -58,4 +45,22 @@ class SearchField extends Component {
}
}
const StyledInput = styled.input`
width: 100%;
padding: 10px;
font-size: 48px;
font-weight: 400;
outline: none;
border: 0;
::-webkit-input-placeholder { color: ${color.slateLight}; }
:-moz-placeholder { color: ${color.slateLight}; }
::-moz-placeholder { color: ${color.slateLight}; }
:-ms-input-placeholder { color: ${color.slateLight}; }
`;
const StyledIcon = styled(Icon)`
top: 3px;
`;
export default SearchField;

View File

@@ -2,6 +2,7 @@
import React from 'react';
import { Redirect } from 'react-router';
import queryString from 'query-string';
import { observable } from 'mobx';
import { observer, inject } from 'mobx-react';
import { client } from 'utils/ApiClient';
@@ -12,17 +13,15 @@ type Props = {
location: Object,
};
type State = {
redirectTo: string,
};
@observer class SlackAuth extends React.Component {
props: Props;
state: State;
state = {};
@observable redirectTo: string;
// $FlowIssue Flow doesn't like async lifecycle components https://github.com/facebook/flow/issues/1803
async componentDidMount(): void {
componentDidMount() {
this.redirect();
}
async redirect() {
const { error, code, state } = queryString.parse(
this.props.location.search
);
@@ -30,18 +29,18 @@ type State = {
if (error) {
if (error === 'access_denied') {
// User selected "Deny" access on Slack OAuth
this.setState({ redirectTo: '/dashboard' });
this.redirectTo = '/dashboard';
} else {
this.setState({ redirectTo: '/auth/error' });
this.redirectTo = '/auth/error';
}
} else {
if (this.props.location.pathname === '/auth/slack/commands') {
// User adding webhook integrations
try {
await client.post('/auth.slackCommands', { code });
this.setState({ redirectTo: '/dashboard' });
this.redirectTo = '/dashboard';
} catch (e) {
this.setState({ redirectTo: '/auth/error' });
this.redirectTo = '/auth/error';
}
} else {
// Regular Slack authentication
@@ -50,18 +49,15 @@ type State = {
const { success } = await this.props.auth.authWithSlack(code, state);
success
? this.setState({ redirectTo: redirectTo || '/dashboard' })
: this.setState({ redirectTo: '/auth/error' });
? (this.redirectTo = redirectTo || '/dashboard')
: (this.redirectTo = '/auth/error');
}
}
}
render() {
return (
<div>
{this.state.redirectTo && <Redirect to={this.state.redirectTo} />}
</div>
);
if (this.redirectTo) return <Redirect to={this.redirectTo} />;
return null;
}
}

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { observer, inject } from 'mobx-react';
import CenteredContent from 'components/CenteredContent';
import { ListPlaceholder } from 'components/LoadingPlaceholder';
import Empty from 'components/Empty';
import PageTitle from 'components/PageTitle';
import DocumentList from 'components/DocumentList';
import DocumentsStore from 'stores/DocumentsStore';
@@ -17,14 +18,17 @@ import DocumentsStore from 'stores/DocumentsStore';
}
render() {
const { isLoaded, isFetching } = this.props.documents;
const { isLoaded, isFetching, starred } = this.props.documents;
const showLoading = !isLoaded && isFetching;
const showEmpty = isLoaded && !starred.length;
return (
<CenteredContent column auto>
<PageTitle title="Starred" />
<h1>Starred</h1>
{!isLoaded && isFetching && <ListPlaceholder />}
<DocumentList documents={this.props.documents.starred} />
{showLoading && <ListPlaceholder />}
{showEmpty && <Empty>No starred documents yet.</Empty>}
<DocumentList documents={starred} />
</CenteredContent>
);
}

View File

@@ -1,6 +1,11 @@
// @flow
import { keyframes } from 'styled-components';
export const fadeIn = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`;
export const fadeAndScaleIn = keyframes`
from {
opacity: 0;

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +0,0 @@
// @flow
import emojiMapping from './emoji-mapping.json';
const EMOJI_REGEX = /:([A-Za-z0-9_\-+]+?):/gm;
const emojify = (text: string = '') => {
let emojifiedText = text;
emojifiedText = text.replace(EMOJI_REGEX, (match, p1, offset, string) => {
return emojiMapping[p1] || match;
});
return emojifiedText;
};
export default emojify;

View File

@@ -1,53 +0,0 @@
// @flow
import slug from 'slug';
import marked from 'marked';
import sanitizedRenderer from 'marked-sanitized';
import highlight from 'highlight.js';
import _ from 'lodash';
import emojify from './emojify';
import toc from './toc';
// $FlowIssue invalid flow-typed
slug.defaults.mode = 'rfc3986';
const Renderer = sanitizedRenderer(marked.Renderer);
const renderer = new Renderer();
renderer.code = (code, language) => {
const validLang = !!(language && highlight.getLanguage(language));
const highlighted = validLang
? highlight.highlight(language, code).value
: _.escape(code);
return `<pre><code class="hljs ${_.escape(language)}">${highlighted}</code></pre>`;
};
renderer.heading = (text, level) => {
const headingSlug = _.escape(slug(text));
return `
<h${level}>
${text}
<a name="${headingSlug}" class="anchor" href="#${headingSlug}">#</a>
</h${level}>
`;
};
const convertToMarkdown = (text: string) => {
// Add TOC
text = toc.insert(text || '', {
slugify: heading => {
// FIXME: E.g. `&` gets messed up
const headingSlug = _.escape(slug(heading));
return headingSlug;
},
});
return marked.parse(emojify(text), {
renderer,
gfm: true,
tables: true,
breaks: false,
pedantic: false,
smartLists: true,
smartypants: true,
});
};
export { convertToMarkdown };

View File

@@ -1,148 +0,0 @@
// @flow
/* eslint-disable */
/**
* marked-toc <https://github.com/jonschlinkert/marked-toc>
*
* Copyright (c) 2014 Jon Schlinkert, contributors.
* Licensed under the MIT license.
*/
'use strict';
var marked = require('marked');
var _ = require('lodash');
var utils = require('./utils');
/**
* Expose `toc`
*/
module.exports = toc;
/**
* Default template to use for generating
* a table of contents.
*/
var defaultTemplate =
'<%= depth %><%= bullet %>[<%= heading %>](#<%= url %>)\n';
/**
* Create the table of contents object that
* will be used as context for the template.
*
* @param {String} `str`
* @param {Object} `options`
* @return {Object}
*/
function generate(str, options) {
var opts = _.extend(
{
firsth1: false,
blacklist: true,
omit: [],
maxDepth: 3,
slugify: function(text) {
return text; // Override this!
},
},
options
);
var toc = '';
// $FlowIssue invalid flow-typed
var tokens = marked.lexer(str);
var tocArray = [];
// Remove the very first h1, true by default
if (opts.firsth1 === false) {
tokens.shift();
}
// Do any h1's still exist?
var h1 = _.some(tokens, { depth: 1 });
tokens
.filter(function(token) {
// Filter out everything but headings
if (token.type !== 'heading' || token.type === 'code') {
return false;
}
// Since we removed the first h1, we'll check to see if other h1's
// exist. If none exist, then we unindent the rest of the TOC
if (!h1) {
token.depth = token.depth - 1;
}
// Store original text and create an id for linking
token.heading = opts.strip ? utils.strip(token.text, opts) : token.text;
// Create a "slugified" id for linking
token.id = opts.slugify(token.text);
// Omit headings with these strings
var omissions = ['Table of Contents', 'TOC', 'TABLE OF CONTENTS'];
var omit = _.union([], opts.omit, omissions);
if (utils.isMatch(omit, token.heading)) {
return;
}
return true;
})
.forEach(function(h) {
if (h.depth > opts.maxDepth) {
return;
}
var bullet = Array.isArray(opts.bullet)
? opts.bullet[(h.depth - 1) % opts.bullet.length]
: opts.bullet;
var data = _.extend({}, opts.data, {
depth: new Array((h.depth - 1) * 2 + 1).join(' '),
bullet: bullet ? bullet : '* ',
heading: h.heading,
url: h.id,
});
tocArray.push(data);
toc += _.template(opts.template || defaultTemplate)(data);
});
return {
data: tocArray,
toc: opts.strip ? utils.strip(toc, opts) : toc,
};
}
/**
* toc
*/
function toc(str: string, options: Object) {
return generate(str, options).toc;
}
toc.raw = function(str, options) {
return generate(str, options);
};
toc.insert = function(content, options) {
var start = '<!-- toc -->';
var stop = '<!-- tocstop -->';
var re = /<!-- toc -->([\s\S]+?)<!-- tocstop -->/;
// remove the existing TOC
content = content.replace(re, start);
// generate new TOC
var newtoc =
'\n\n' + start + '\n\n' + toc(content, options) + '\n' + stop + '\n';
// If front-matter existed, put it back
return content.replace(start, newtoc);
};

View File

@@ -1,83 +0,0 @@
/* eslint-disable */
/*!
* marked-toc <https://github.com/jonschlinkert/marked-toc>
*
* Copyright (c) 2014 Jon Schlinkert, contributors.
* Licensed under the MIT license.
*/
'use strict';
var _ = require('lodash');
var utils = (module.exports = {});
utils.arrayify = function(arr) {
return !Array.isArray(arr) ? [arr] : arr;
};
utils.escapeRegex = function(re) {
return re.replace(/(\[|\]|\(|\)|\/|\.|\^|\$|\*|\+|\?)/g, '\\$1');
};
utils.isDest = function(dest) {
return !dest || dest === 'undefined' || typeof dest === 'object';
};
utils.isMatch = function(keys, str) {
keys = utils.arrayify(keys);
keys = keys.length > 0 ? keys.join('|') : '.*';
// Escape certain characters, like '[', '('
var k = utils.escapeRegex(String(keys));
// Build up the regex to use for replacement patterns
var re = new RegExp('(?:' + k + ')', 'g');
if (String(str).match(re)) {
return true;
} else {
return false;
}
};
utils.sanitize = function(src) {
src = src.replace(/(\s*\[!|(?:\[.+ →\]\()).+/g, '');
src = src.replace(/\s*\*\s*\[\].+/g, '');
return src;
};
utils.slugify = function(str) {
str = str.replace(/\/\//g, '-');
str = str.replace(/\//g, '-');
str = str.replace(/\./g, '-');
str = _.str.slugify(str);
str = str.replace(/^-/, '');
str = str.replace(/-$/, '');
return str;
};
/**
* Strip certain words from headings. These can be
* overridden. Might seem strange but it makes
* sense in context.
*/
var omit = [
'grunt',
'helper',
'handlebars-helper',
'mixin',
'filter',
'assemble-contrib',
'assemble',
];
utils.strip = function(name, options) {
var opts = _.extend({}, options);
if (opts.omit === false) {
omit = [];
}
var exclusions = _.union(omit, utils.arrayify(opts.strip || []));
var re = new RegExp('^(?:' + exclusions.join('|') + ')[-_]?', 'g');
return name.replace(re, '');
};