Merge master
This commit is contained in:
@@ -1,36 +1,37 @@
|
||||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Flex from 'components/Flex';
|
||||
import classNames from 'classnames/bind';
|
||||
import styles from './Alert.scss';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
const cx = classNames.bind(styles);
|
||||
type Props = {
|
||||
children: React.Element<*>,
|
||||
type?: 'info' | 'success' | 'warning' | 'danger' | 'offline',
|
||||
};
|
||||
|
||||
class Alert extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
danger: PropTypes.bool,
|
||||
warning: PropTypes.bool,
|
||||
success: PropTypes.bool,
|
||||
@observer class Alert extends React.Component {
|
||||
props: Props;
|
||||
defaultProps = {
|
||||
type: 'info',
|
||||
};
|
||||
|
||||
render() {
|
||||
let alertType;
|
||||
if (this.props.danger) alertType = 'danger';
|
||||
if (this.props.warning) alertType = 'warning';
|
||||
if (this.props.success) alertType = 'success';
|
||||
if (!alertType) alertType = 'info'; // default
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
className={cx(styles.container, styles[alertType])}
|
||||
>
|
||||
<Container align="center" justify="center" type={this.props.type}>
|
||||
{this.props.children}
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Container = styled(Flex)`
|
||||
height: $headerHeight;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
|
||||
background-color: ${({ type }) => color[type]};
|
||||
`;
|
||||
|
||||
export default Alert;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
@import '~styles/constants.scss';
|
||||
|
||||
.container {
|
||||
height: $headerHeight;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: #f04124;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: #f08a24;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #43AC6A;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: #a0d3e8;
|
||||
}
|
||||
|
||||
.offline {
|
||||
background-color: #000000;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import Alert from '.';
|
||||
|
||||
test('renders default as info', () => {
|
||||
snap(<Alert>default</Alert>);
|
||||
});
|
||||
|
||||
test('renders success', () => {
|
||||
snap(<Alert success>success</Alert>);
|
||||
});
|
||||
|
||||
test('renders info', () => {
|
||||
snap(<Alert info>info</Alert>);
|
||||
});
|
||||
|
||||
test('renders warning', () => {
|
||||
snap(<Alert warning>warning</Alert>);
|
||||
});
|
||||
|
||||
test('renders danger', () => {
|
||||
snap(<Alert danger>danger</Alert>);
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders danger 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container danger"
|
||||
justify="center"
|
||||
>
|
||||
danger
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`renders default as info 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container info"
|
||||
justify="center"
|
||||
>
|
||||
default
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`renders info 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container info"
|
||||
justify="center"
|
||||
>
|
||||
info
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`renders success 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container success"
|
||||
justify="center"
|
||||
>
|
||||
success
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`renders warning 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container warning"
|
||||
justify="center"
|
||||
>
|
||||
warning
|
||||
</Flex>
|
||||
`;
|
||||
@@ -25,15 +25,20 @@ const Collaborators = function({ document }: { document: Document }) {
|
||||
|
||||
return (
|
||||
<Avatars>
|
||||
<Tooltip tooltip={tooltip} placement="bottom">
|
||||
<StyledTooltip tooltip={tooltip} placement="bottom">
|
||||
{collaborators.map(user => (
|
||||
<Avatar key={user.id} src={user.avatarUrl} />
|
||||
))}
|
||||
</Tooltip>
|
||||
</StyledTooltip>
|
||||
</Avatars>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
`;
|
||||
|
||||
const Avatars = styled(Flex)`
|
||||
flex-direction: row-reverse;
|
||||
height: 26px;
|
||||
@@ -45,7 +50,7 @@ const Avatar = styled.img`
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${color.white};
|
||||
margin-right: -13px;
|
||||
margin-right: -10px;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 0;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import moment from 'moment';
|
||||
|
||||
import styles from './Collection.scss';
|
||||
|
||||
@observer class Collection extends React.Component {
|
||||
static propTypes = {
|
||||
data: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const data = this.props.data;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h2>
|
||||
<Link to={data.url} className={styles.atlasLink}>{data.name}</Link>
|
||||
</h2>
|
||||
{data.recentDocuments.length > 0
|
||||
? data.recentDocuments.map(document => {
|
||||
return (
|
||||
<Link
|
||||
key={document.id}
|
||||
to={document.url}
|
||||
className={styles.link}
|
||||
>
|
||||
<h3 className={styles.title}>{document.title}</h3>
|
||||
<span className={styles.timestamp}>
|
||||
{moment(document.updatedAt).fromNow()}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})
|
||||
: <div className={styles.description}>
|
||||
No documents. Why not
|
||||
{' '}
|
||||
<Link to={`${data.url}/new`}>create one</Link>
|
||||
?
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Collection;
|
||||
@@ -1,41 +0,0 @@
|
||||
@import '~styles/constants.scss';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding-bottom: 40px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.atlasLink {
|
||||
text-decoration: none;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: $textColor;
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import moment from 'moment';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './DocumentLink.scss';
|
||||
|
||||
const DocumentLink = observer(props => {
|
||||
return (
|
||||
<Link to={props.document.url} className={styles.link}>
|
||||
<h3 className={styles.title}>{props.document.title}</h3>
|
||||
<span className={styles.timestamp}>
|
||||
{moment(props.document.updatedAt).fromNow()}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
export default DocumentLink;
|
||||
@@ -1,23 +0,0 @@
|
||||
@import '~styles/constants.scss';
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: $textColor;
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import DocumentLink from './DocumentLink';
|
||||
export default DocumentLink;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Collection from './Collection';
|
||||
export default Collection;
|
||||
@@ -1,10 +1,17 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Divider.scss';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
const Divider = () => {
|
||||
return <div className={styles.divider}><span /></div>;
|
||||
return <Flex auto justify="center"><Content /></Flex>;
|
||||
};
|
||||
|
||||
const Content = styled.span`
|
||||
display: flex;
|
||||
width: 50%;
|
||||
margin: 20px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
`;
|
||||
|
||||
export default Divider;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.divider {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
margin: 20px 0;
|
||||
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,18 @@ type Props = {
|
||||
innerRef?: Function,
|
||||
};
|
||||
|
||||
const StyledStar = styled(Icon).attrs({
|
||||
const StyledStar = styled(({ solid, ...props }) => <Icon {...props} />).attrs({
|
||||
type: 'Star',
|
||||
color: color.text,
|
||||
})`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 1px;
|
||||
margin-left: 4px;
|
||||
opacity: ${props => (props.solid ? '1 !important' : 0)};
|
||||
transition: opacity 100ms ease-in-out;
|
||||
|
||||
${props => props.solid && 'polygon { fill: #000};'}
|
||||
`;
|
||||
|
||||
const DocumentLink = styled(Link)`
|
||||
|
||||
@@ -3,20 +3,17 @@ import React, { Component } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Editor, Plain } from 'slate';
|
||||
import keydown from 'react-keydown';
|
||||
import classnames from 'classnames/bind';
|
||||
import type { Document, State, Editor as EditorType } from './types';
|
||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||
import Flex from 'components/Flex';
|
||||
import ClickablePadding from './components/ClickablePadding';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import Placeholder from './components/Placeholder';
|
||||
import Markdown from './serializer';
|
||||
import createSchema from './schema';
|
||||
import createPlugins from './plugins';
|
||||
import insertImage from './insertImage';
|
||||
import styled from 'styled-components';
|
||||
import styles from './Editor.scss';
|
||||
|
||||
const cx = classnames.bind(styles);
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
@@ -188,11 +185,10 @@ type KeyData = {
|
||||
<MaxWidth column auto>
|
||||
<Header onClick={this.focusAtStart} readOnly={this.props.readOnly} />
|
||||
<Toolbar state={this.state.state} onChange={this.onChange} />
|
||||
<Editor
|
||||
ref={ref => (this.editor = ref)}
|
||||
<StyledEditor
|
||||
innerRef={ref => (this.editor = ref)}
|
||||
placeholder="Start with a title…"
|
||||
bodyPlaceholder="Insert witty platitude here"
|
||||
className={cx(styles.editor, { readOnly: this.props.readOnly })}
|
||||
schema={this.schema}
|
||||
plugins={this.plugins}
|
||||
emoji={this.props.emoji}
|
||||
@@ -225,4 +221,106 @@ const Header = styled(Flex)`
|
||||
${({ readOnly }) => !readOnly && 'cursor: text;'}
|
||||
`;
|
||||
|
||||
const StyledEditor = styled(Editor)`
|
||||
font-weight: 400;
|
||||
font-size: 1em;
|
||||
line-height: 1.7em;
|
||||
width: 100%;
|
||||
color: #1b2830;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
|
||||
.anchor {
|
||||
visibility: hidden;
|
||||
color: #dedede;
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.anchor {
|
||||
visibility: visible;
|
||||
|
||||
&:hover {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
p:first-of-type {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 1em 0.1em;
|
||||
padding-left: 1em;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
li p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.todoList {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
||||
.todoList {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.todo {
|
||||
span:last-child:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid #efefef;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 5px 20px 5px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default MarkdownEditor;
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
.editor {
|
||||
font-weight: 400;
|
||||
font-size: 1em;
|
||||
line-height: 1.7em;
|
||||
width: 100%;
|
||||
color: #1b2830;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
|
||||
.anchor {
|
||||
visibility: hidden;
|
||||
color: #dedede;
|
||||
padding-left: .25em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.anchor {
|
||||
visibility: visible;
|
||||
|
||||
&:hover {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
.placeholder {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
p:first-of-type {
|
||||
.placeholder {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 1em .1em;
|
||||
padding-left: 1em;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: .1em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
li p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.todoList {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
||||
.todoList {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.todo {
|
||||
span:last-child:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid #efefef;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 5px 20px 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.readOnly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #B1BECC;
|
||||
}
|
||||
|
||||
@media all and (max-width: 2000px) and (min-width: 960px) {
|
||||
.container {
|
||||
// margin-top: 48px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px) {
|
||||
.container {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import styled from 'styled-components';
|
||||
import _ from 'lodash';
|
||||
import slug from 'slug';
|
||||
import type { Node, Editor } from '../types';
|
||||
import styles from '../Editor.scss';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
type Props = {
|
||||
children: React$Element<any>,
|
||||
@@ -22,6 +22,27 @@ const Wrapper = styled.div`
|
||||
margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)}
|
||||
`;
|
||||
|
||||
const Anchor = styled.a`
|
||||
visibility: hidden;
|
||||
padding-left: .25em;
|
||||
color: #dedede;
|
||||
|
||||
&:hover {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
`;
|
||||
|
||||
// $FlowIssue I don't know
|
||||
const titleStyles = component => styled(component)`
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
${Anchor} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function Heading(props: Props) {
|
||||
const {
|
||||
parent,
|
||||
@@ -37,21 +58,20 @@ function Heading(props: Props) {
|
||||
const showPlaceholder = placeholder && firstHeading && !node.text;
|
||||
const slugish = _.escape(`${component}-${slug(node.text)}`);
|
||||
const showHash = readOnly && !!slugish;
|
||||
const Component = component;
|
||||
const Component = titleStyles(component);
|
||||
const emoji = editor.props.emoji || '';
|
||||
const title = node.text.trim();
|
||||
const startsWithEmojiAndSpace =
|
||||
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||
|
||||
return (
|
||||
<Component className={styles.title}>
|
||||
<Component>
|
||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>{children}</Wrapper>
|
||||
{showPlaceholder &&
|
||||
<span className={styles.placeholder} contentEditable={false}>
|
||||
<Placeholder contentEditable={false}>
|
||||
{editor.props.placeholder}
|
||||
</span>}
|
||||
{showHash &&
|
||||
<a name={slugish} className={styles.anchor} href={`#${slugish}`}>#</a>}
|
||||
</Placeholder>}
|
||||
{showHash && <Anchor name={slugish} href={`#${slugish}`}>#</Anchor>}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import { Document } from 'slate';
|
||||
import type { Props } from '../types';
|
||||
import styles from '../Editor.scss';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
export default function Link({
|
||||
attributes,
|
||||
@@ -26,9 +26,9 @@ export default function Link({
|
||||
<p>
|
||||
{children}
|
||||
{showPlaceholder &&
|
||||
<span className={styles.placeholder} contentEditable={false}>
|
||||
<Placeholder contentEditable={false}>
|
||||
{editor.props.bodyPlaceholder}
|
||||
</span>}
|
||||
</Placeholder>}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
11
frontend/components/Editor/components/Placeholder.js
Normal file
11
frontend/components/Editor/components/Placeholder.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
export default styled.span`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #B1BECC;
|
||||
`;
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { Props } from '../types';
|
||||
import styles from '../Editor.scss';
|
||||
|
||||
export default class TodoItem extends Component {
|
||||
props: Props & { checked: boolean };
|
||||
@@ -22,7 +22,7 @@ export default class TodoItem extends Component {
|
||||
const { children, checked, readOnly } = this.props;
|
||||
|
||||
return (
|
||||
<li contentEditable={false} className={styles.todo}>
|
||||
<StyledLi contentEditable={false}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
@@ -33,7 +33,17 @@ export default class TodoItem extends Component {
|
||||
<span contentEditable={!readOnly} suppressContentEditableWarning>
|
||||
{children}
|
||||
</span>
|
||||
</li>
|
||||
</StyledLi>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledLi = styled.li`
|
||||
input {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&:last-child:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import Portal from 'react-portal';
|
||||
import classnames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
import _ from 'lodash';
|
||||
import type { State } from '../../types';
|
||||
import FormattingToolbar from './components/FormattingToolbar';
|
||||
import LinkToolbar from './components/LinkToolbar';
|
||||
import styles from './Toolbar.scss';
|
||||
|
||||
export default class Toolbar extends Component {
|
||||
props: {
|
||||
@@ -112,9 +111,6 @@ export default class Toolbar extends Component {
|
||||
|
||||
render() {
|
||||
const link = this.state.link;
|
||||
const classes = classnames(styles.menu, {
|
||||
[styles.active]: this.state.active,
|
||||
});
|
||||
|
||||
const style = {
|
||||
top: this.state.top,
|
||||
@@ -123,7 +119,7 @@ export default class Toolbar extends Component {
|
||||
|
||||
return (
|
||||
<Portal isOpened>
|
||||
<div className={classes} style={style} ref={this.setRef}>
|
||||
<Menu active={this.state.active} innerRef={this.setRef} style={style}>
|
||||
{link &&
|
||||
<LinkToolbar
|
||||
{...this.props}
|
||||
@@ -135,8 +131,28 @@ export default class Toolbar extends Component {
|
||||
onCreateLink={this.handleFocus}
|
||||
{...this.props}
|
||||
/>}
|
||||
</div>
|
||||
</Menu>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Menu = styled.div`
|
||||
padding: 8px 16px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
opacity: 0;
|
||||
background-color: #222;
|
||||
border-radius: 4px;
|
||||
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
||||
line-height: 0;
|
||||
height: 40px;
|
||||
min-width: 260px;
|
||||
|
||||
${({ active }) => active && `
|
||||
transform: translateY(-6px);
|
||||
opacity: 1;
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
.menu {
|
||||
padding: 8px 16px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
opacity: 0;
|
||||
background-color: #222;
|
||||
border-radius: 4px;
|
||||
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
||||
line-height: 0;
|
||||
height: 40px;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.active {
|
||||
transform: translateY(-6px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.linkEditor {
|
||||
display: flex;
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
|
||||
input {
|
||||
background: rgba(255,255,255,.1);
|
||||
border-radius: 2px;
|
||||
padding: 5px 8px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
flex: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
padding: 0;
|
||||
opacity: .7;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&[data-active="true"] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styles from '../Toolbar.scss';
|
||||
import type { State } from '../../../types';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import BoldIcon from 'components/Icon/BoldIcon';
|
||||
import CodeIcon from 'components/Icon/CodeIcon';
|
||||
import Heading1Icon from 'components/Icon/Heading1Icon';
|
||||
@@ -68,13 +68,9 @@ export default class FormattingToolbar extends Component {
|
||||
const onMouseDown = ev => this.onClickMark(ev, type);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={styles.button}
|
||||
onMouseDown={onMouseDown}
|
||||
data-active={isActive}
|
||||
>
|
||||
<ToolbarButton onMouseDown={onMouseDown} active={isActive}>
|
||||
<IconClass light />
|
||||
</button>
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,13 +80,9 @@ export default class FormattingToolbar extends Component {
|
||||
this.onClickBlock(ev, isActive ? 'paragraph' : type);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={styles.button}
|
||||
onMouseDown={onMouseDown}
|
||||
data-active={isActive}
|
||||
>
|
||||
<ToolbarButton onMouseDown={onMouseDown} active={isActive}>
|
||||
<IconClass light />
|
||||
</button>
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -103,9 +95,9 @@ export default class FormattingToolbar extends Component {
|
||||
{this.renderBlockButton('heading2', Heading2Icon)}
|
||||
{this.renderBlockButton('bulleted-list', BulletedListIcon)}
|
||||
{this.renderMarkButton('code', CodeIcon)}
|
||||
<button className={styles.button} onMouseDown={this.onCreateLink}>
|
||||
<ToolbarButton onMouseDown={this.onCreateLink}>
|
||||
<LinkIcon light />
|
||||
</button>
|
||||
</ToolbarButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import type { State } from '../../../types';
|
||||
import keydown from 'react-keydown';
|
||||
import styles from '../Toolbar.scss';
|
||||
import Icon from 'components/Icon';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
@keydown
|
||||
export default class LinkToolbar extends Component {
|
||||
@@ -20,7 +22,7 @@ export default class LinkToolbar extends Component {
|
||||
case 13: // enter
|
||||
ev.preventDefault();
|
||||
return this.save(ev.target.value);
|
||||
case 26: // escape
|
||||
case 27: // escape
|
||||
return this.input.blur();
|
||||
default:
|
||||
}
|
||||
@@ -48,19 +50,35 @@ export default class LinkToolbar extends Component {
|
||||
render() {
|
||||
const href = this.props.link.data.get('href');
|
||||
return (
|
||||
<span className={styles.linkEditor}>
|
||||
<input
|
||||
ref={ref => (this.input = ref)}
|
||||
<LinkEditor>
|
||||
<Input
|
||||
innerRef={ref => (this.input = ref)}
|
||||
defaultValue={href}
|
||||
placeholder="http://"
|
||||
onBlur={this.props.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
<button className={styles.button} onMouseDown={this.removeLink}>
|
||||
<ToolbarButton onMouseDown={this.removeLink}>
|
||||
<Icon type="X" light />
|
||||
</button>
|
||||
</span>
|
||||
</ToolbarButton>
|
||||
</LinkEditor>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LinkEditor = styled(Flex)`
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
background: rgba(255,255,255,.1);
|
||||
border-radius: 2px;
|
||||
padding: 5px 8px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
export default styled.button`
|
||||
display: inline-block;
|
||||
flex: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
padding: 0;
|
||||
opacity: .7;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
${({ active }) => active && 'opacity: 1;'}
|
||||
`;
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Code from './components/Code';
|
||||
import InlineCode from './components/InlineCode';
|
||||
import Image from './components/Image';
|
||||
@@ -8,7 +9,15 @@ import ListItem from './components/ListItem';
|
||||
import Heading from './components/Heading';
|
||||
import Paragraph from './components/Paragraph';
|
||||
import type { Props, Node, Transform } from './types';
|
||||
import styles from './Editor.scss';
|
||||
|
||||
const TodoList = styled.ul`
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
||||
ul {
|
||||
padding-left: 1em;
|
||||
}
|
||||
`;
|
||||
|
||||
const createSchema = () => {
|
||||
return {
|
||||
@@ -29,9 +38,7 @@ const createSchema = () => {
|
||||
'horizontal-rule': (props: Props) => <hr />,
|
||||
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
|
||||
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
|
||||
'todo-list': (props: Props) => (
|
||||
<ul className={styles.todoList}>{props.children}</ul>
|
||||
),
|
||||
'todo-list': (props: Props) => <TodoList>{props.children}</TodoList>,
|
||||
table: (props: Props) => <table>{props.children}</table>,
|
||||
'table-row': (props: Props) => <tr>{props.children}</tr>,
|
||||
'table-head': (props: Props) => <th>{props.children}</th>,
|
||||
|
||||
@@ -3,7 +3,6 @@ import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
const HelpText = styled.p`
|
||||
user-select: none;
|
||||
color: ${color.slateDark};
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import styled from 'styled-components';
|
||||
import Mask from './components/Mask';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
export default (props: Object) => {
|
||||
type Props = {
|
||||
count?: number,
|
||||
};
|
||||
|
||||
const ListPlaceHolder = ({ count }: Props) => {
|
||||
return (
|
||||
<ReactCSSTransitionGroup
|
||||
transitionName="fadeIn"
|
||||
@@ -16,14 +21,12 @@ export default (props: Object) => {
|
||||
transitionEnter
|
||||
transitionLeave
|
||||
>
|
||||
<Item column auto>
|
||||
<Mask header />
|
||||
<Mask />
|
||||
</Item>
|
||||
<Item column auto>
|
||||
<Mask header />
|
||||
<Mask />
|
||||
</Item>
|
||||
{_.times(count || 2, index => (
|
||||
<Item key={index} column auto>
|
||||
<Mask header />
|
||||
<Mask />
|
||||
</Item>
|
||||
))}
|
||||
</ReactCSSTransitionGroup>
|
||||
);
|
||||
};
|
||||
@@ -31,3 +34,5 @@ export default (props: Object) => {
|
||||
const Item = styled(Flex)`
|
||||
padding: 18px 0;
|
||||
`;
|
||||
|
||||
export default ListPlaceHolder;
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { State, Document, Editor } from 'slate';
|
||||
import MarkdownSerializer from '../Editor/serializer';
|
||||
import type { State as StateType } from '../Editor/types';
|
||||
import schema from '../Editor/schema';
|
||||
import styles from '../Editor/Editor.scss';
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
className: string,
|
||||
limit: number,
|
||||
};
|
||||
|
||||
function filterDocumentState({ state, characterLimit, nodeLimit }) {
|
||||
const { document } = state;
|
||||
if (document.text.length <= characterLimit) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let totalCharacters = 0;
|
||||
let totalNodes = 0;
|
||||
const nodes = document.nodes.filter(childNode => {
|
||||
if (childNode.text.length + totalCharacters <= characterLimit) {
|
||||
totalCharacters += childNode.text.length;
|
||||
|
||||
if (totalNodes++ <= nodeLimit) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return State.create({
|
||||
document: Document.create({
|
||||
...document,
|
||||
nodes: nodes,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
class Markdown extends React.Component {
|
||||
props: Props;
|
||||
|
||||
state: {
|
||||
state: StateType,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const state = MarkdownSerializer.deserialize(props.text);
|
||||
const options = {
|
||||
state,
|
||||
characterLimit: props.limit,
|
||||
nodeLimit: 5,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
state: filterDocumentState(options),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className={this.props.className}>
|
||||
<Editor
|
||||
className={styles.editor}
|
||||
schema={schema}
|
||||
state={this.state.state}
|
||||
readOnly
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Markdown;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Markdown from './Markdown';
|
||||
export default Markdown;
|
||||
@@ -16,11 +16,11 @@ import CollectionsStore from 'stores/CollectionsStore';
|
||||
import CacheStore from 'stores/CacheStore';
|
||||
|
||||
import 'normalize.css/normalize.css';
|
||||
import 'styles/base.scss';
|
||||
import 'styles/base.css';
|
||||
import 'styles/fonts.css';
|
||||
import 'styles/transitions.scss';
|
||||
import 'styles/prism-tomorrow.scss';
|
||||
import 'styles/hljs-github-gist.scss';
|
||||
import 'styles/transitions.css';
|
||||
import 'styles/prism-tomorrow.css';
|
||||
import 'styles/hljs-github-gist.css';
|
||||
|
||||
import Home from 'scenes/Home';
|
||||
import Dashboard from 'scenes/Dashboard';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import Flex from 'components/Flex';
|
||||
import DocumentList from 'components/DocumentList';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
@@ -26,29 +28,33 @@ type Props = {
|
||||
|
||||
@observer class Dashboard extends React.Component {
|
||||
props: Props;
|
||||
@observable isLoaded = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.documents.fetchAll();
|
||||
this.props.documents.fetchRecentlyViewed();
|
||||
this.loadContent();
|
||||
}
|
||||
|
||||
get showPlaceholder() {
|
||||
const { isLoaded, isFetching } = this.props.documents;
|
||||
return !isLoaded && isFetching;
|
||||
}
|
||||
loadContent = async () => {
|
||||
await Promise.all([
|
||||
this.props.documents.fetchRecentlyModified({ limit: 5 }),
|
||||
this.props.documents.fetchRecentlyViewed({ limit: 5 }),
|
||||
]);
|
||||
this.isLoaded = true;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Home" />
|
||||
<h1>Home</h1>
|
||||
<Subheading>Recently viewed</Subheading>
|
||||
{this.showPlaceholder && <ListPlaceholder />}
|
||||
<DocumentList documents={this.props.documents.recentlyViewed} />
|
||||
|
||||
<Subheading>Recently edited</Subheading>
|
||||
<DocumentList documents={this.props.documents.recentlyEdited} />
|
||||
{this.showPlaceholder && <ListPlaceholder />}
|
||||
{this.isLoaded
|
||||
? <Flex column>
|
||||
<Subheading>Recently viewed</Subheading>
|
||||
<DocumentList documents={this.props.documents.recentlyViewed} />
|
||||
<Subheading>Recently edited</Subheading>
|
||||
<DocumentList documents={this.props.documents.recentlyEdited} />
|
||||
</Flex>
|
||||
: <ListPlaceholder count={5} />}
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { observer, inject } from 'mobx-react';
|
||||
import { withRouter, Prompt } from 'react-router';
|
||||
import Flex from 'components/Flex';
|
||||
import { color, layout } from 'styles/constants';
|
||||
import { collectionUrl } from 'utils/routeHelpers';
|
||||
|
||||
import Document from 'models/Document';
|
||||
import UiStore from 'stores/UiStore';
|
||||
@@ -19,6 +20,7 @@ import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import Collaborators from 'components/Collaborators';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Search from 'scenes/Search';
|
||||
|
||||
const DISCARD_CHANGES = `
|
||||
You have unsaved changes.
|
||||
@@ -46,6 +48,7 @@ type Props = {
|
||||
isSaving: false,
|
||||
newDocument: undefined,
|
||||
showAsSaved: false,
|
||||
notFound: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -57,6 +60,7 @@ type Props = {
|
||||
nextProps.match.params.documentSlug !==
|
||||
this.props.match.params.documentSlug
|
||||
) {
|
||||
this.setState({ notFound: false });
|
||||
this.loadDocument(nextProps);
|
||||
}
|
||||
}
|
||||
@@ -86,6 +90,9 @@ type Props = {
|
||||
if (document) {
|
||||
this.props.ui.setActiveDocument(document);
|
||||
document.view();
|
||||
} else {
|
||||
// Render 404 with search
|
||||
this.setState({ notFound: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -146,7 +153,13 @@ type Props = {
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
this.props.history.goBack();
|
||||
let url;
|
||||
if (this.document && this.document.url) {
|
||||
url = this.document.url;
|
||||
} else {
|
||||
url = collectionUrl(this.props.match.params.id);
|
||||
}
|
||||
this.props.history.push(url);
|
||||
};
|
||||
|
||||
onStartDragging = () => {
|
||||
@@ -157,6 +170,10 @@ type Props = {
|
||||
this.setState({ isDragging: false });
|
||||
};
|
||||
|
||||
renderNotFound() {
|
||||
return <Search notFound />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const isNew = this.props.newDocument;
|
||||
const isEditing = !!this.props.match.params.edit || isNew;
|
||||
@@ -164,6 +181,10 @@ type Props = {
|
||||
const titleText = get(this.document, 'title', '');
|
||||
const document = this.document;
|
||||
|
||||
if (this.state.notFound) {
|
||||
return this.renderNotFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<Container column auto>
|
||||
{this.state.isDragging &&
|
||||
|
||||
@@ -26,9 +26,7 @@ type Props = {
|
||||
if (state && state.nextPathname) {
|
||||
sessionStorage.removeItem('redirectTo');
|
||||
sessionStorage.setItem('redirectTo', state.nextPathname);
|
||||
notifications.push(
|
||||
<Alert key="login" info>Please login to continue</Alert>
|
||||
);
|
||||
notifications.push(<Alert key="login">Please login to continue</Alert>);
|
||||
}
|
||||
|
||||
return notifications;
|
||||
|
||||
@@ -1,48 +1,49 @@
|
||||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
import styles from './ApiKeyRow.scss';
|
||||
import classNames from 'classnames/bind';
|
||||
const cx = classNames.bind(styles);
|
||||
type Props = {
|
||||
id: string,
|
||||
name: ?string,
|
||||
secret: string,
|
||||
onDelete: Function,
|
||||
};
|
||||
|
||||
class ApiKeyRow extends React.Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
secret: PropTypes.string.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
disabled: false,
|
||||
};
|
||||
@observer class ApiKeyRow extends React.Component {
|
||||
props: Props;
|
||||
@observable disabled: boolean;
|
||||
|
||||
onClick = () => {
|
||||
this.props.onDelete(this.props.id);
|
||||
this.setState({ disabled: true });
|
||||
this.disabled = true;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name, secret } = this.props;
|
||||
|
||||
const { disabled } = this.state;
|
||||
const { disabled } = this;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td><code>{secret}</code></td>
|
||||
<td>
|
||||
<span
|
||||
role="button"
|
||||
onClick={this.onClick}
|
||||
className={cx(styles.deleteAction, { disabled })}
|
||||
>
|
||||
Delete
|
||||
</span>
|
||||
<Action role="button" onClick={this.onClick} disabled={disabled}>
|
||||
Action
|
||||
</Action>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Action = styled.span`
|
||||
font-size: 14px;
|
||||
color: ${color.text};
|
||||
|
||||
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
|
||||
`;
|
||||
|
||||
export default ApiKeyRow;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
@import '~styles/constants.scss';
|
||||
|
||||
.deleteAction {
|
||||
font-size: 14px;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -38,14 +38,17 @@ class DocumentsStore extends BaseStore {
|
||||
/* Computed */
|
||||
|
||||
@computed get recentlyViewed(): Array<Document> {
|
||||
return _.filter(this.data.values(), ({ id }) =>
|
||||
this.recentlyViewedIds.includes(id)
|
||||
return _.take(
|
||||
_.filter(this.data.values(), ({ id }) =>
|
||||
this.recentlyViewedIds.includes(id)
|
||||
),
|
||||
5
|
||||
);
|
||||
}
|
||||
|
||||
@computed get recentlyEdited(): Array<Document> {
|
||||
// $FlowIssue
|
||||
return this.data.values();
|
||||
return _.take(this.data.values(), 5);
|
||||
}
|
||||
|
||||
@computed get starred(): Array<Document> {
|
||||
@@ -60,11 +63,14 @@ class DocumentsStore extends BaseStore {
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action fetchAll = async (request: string = 'list'): Promise<*> => {
|
||||
@action fetchAll = async (
|
||||
request: string = 'list',
|
||||
options: ?Object
|
||||
): Promise<*> => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/documents.${request}`);
|
||||
const res = await client.post(`/documents.${request}`, options);
|
||||
invariant(res && res.data, 'Document list not available');
|
||||
const { data } = res;
|
||||
runInAction('DocumentsStore#fetchAll', () => {
|
||||
@@ -81,12 +87,17 @@ class DocumentsStore extends BaseStore {
|
||||
}
|
||||
};
|
||||
|
||||
@action fetchRecentlyViewed = async (): Promise<*> => {
|
||||
const data = await this.fetchAll('viewed');
|
||||
@action fetchRecentlyModified = async (options: ?Object): Promise<*> => {
|
||||
return await this.fetchAll('list', options);
|
||||
};
|
||||
|
||||
@action fetchRecentlyViewed = async (options: ?Object): Promise<*> => {
|
||||
const data = await this.fetchAll('viewed', options);
|
||||
|
||||
runInAction('DocumentsStore#fetchRecentlyViewed', () => {
|
||||
this.recentlyViewedIds = _.map(data, 'id');
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
@action fetchStarred = async (): Promise<*> => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@import './constants.scss';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -10,18 +8,21 @@
|
||||
--line-height-3: 1.25;
|
||||
--line-height-4: 1.5;
|
||||
--letter-spacing: 1;
|
||||
--caps-letter-spacing: .2em;
|
||||
--caps-letter-spacing: 0.2em;
|
||||
--bold-font-weight: bold;
|
||||
}
|
||||
|
||||
html, body, .viewport {
|
||||
html,
|
||||
body,
|
||||
.viewport {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
@@ -41,42 +42,59 @@ svg {
|
||||
max-height: 100%;
|
||||
}
|
||||
a {
|
||||
color: #005AA6;
|
||||
color: #005aa6;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
h1, h2, h3,
|
||||
h4, h5, h6 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
margin-top: 1em;
|
||||
margin-bottom: .5em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #1f2429;
|
||||
}
|
||||
h1 { font-size: 2em }
|
||||
h2 { font-size: 1.5em }
|
||||
h3 { font-size: 1.25em }
|
||||
h4 { font-size: 1em }
|
||||
h5 { font-size: .875em }
|
||||
h6 { font-size: .75em }
|
||||
p, dl, ol, ul, pre, blockquote {
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
h5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
h6 {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
p,
|
||||
dl,
|
||||
ol,
|
||||
ul,
|
||||
pre,
|
||||
blockquote {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
code,
|
||||
pre,
|
||||
samp {
|
||||
font-family:
|
||||
'Atlas Typewriter',
|
||||
'Source Code Pro',
|
||||
Menlo,
|
||||
Consolas,
|
||||
'Liberation Mono',
|
||||
monospace;
|
||||
font-family: 'Atlas Typewriter', 'Source Code Pro', Menlo, Consolas,
|
||||
'Liberation Mono', monospace;
|
||||
}
|
||||
code, samp {
|
||||
code,
|
||||
samp {
|
||||
font-size: 87.5%;
|
||||
padding: .125em;
|
||||
padding: 0.125em;
|
||||
}
|
||||
pre {
|
||||
font-size: 87.5%;
|
||||
@@ -94,14 +112,12 @@ hr {
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #dedede;
|
||||
}
|
||||
*[role=button] {
|
||||
*[role='button'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global {
|
||||
.hljs {
|
||||
border: 1px solid rgba(0,0,0,.0625);
|
||||
padding: 1em;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
.hljs {
|
||||
border: 1px solid rgba(0, 0, 0, 0.0625);
|
||||
padding: 1em;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
@@ -42,6 +42,10 @@ export const color = {
|
||||
/* Brand */
|
||||
primary: '#2B8FBF',
|
||||
danger: '#D0021B',
|
||||
warning: '#f08a24' /* replace */,
|
||||
success: '#43AC6A' /* replace */,
|
||||
info: '#a0d3e8' /* replace */,
|
||||
offline: '#000000',
|
||||
|
||||
/* Dark Grays */
|
||||
slate: '#9BA6B2',
|
||||
@@ -56,4 +60,6 @@ export const color = {
|
||||
/* Misc */
|
||||
white: '#FFFFFF',
|
||||
black: '#000000',
|
||||
|
||||
/* Alert colors */
|
||||
};
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
$textColor: #171B35;
|
||||
$actionColor: #3AA3E3;
|
||||
|
||||
$darkGray: #333;
|
||||
$gray: #ccc;
|
||||
$lightGray: #eee;
|
||||
|
||||
$headerHeight: 42px;
|
||||
|
||||
:export {
|
||||
textColor: $textColor;
|
||||
actionColor: $actionColor;
|
||||
|
||||
headerHeight: $headerHeight;
|
||||
}
|
||||
71
frontend/styles/hljs-github-gist.css
Normal file
71
frontend/styles/hljs-github-gist.css
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* GitHub Gist Theme
|
||||
* Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
:global {
|
||||
/**
|
||||
* GitHub Gist Theme
|
||||
* Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
120
frontend/styles/prism-tomorrow.css
Normal file
120
frontend/styles/prism-tomorrow.css
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
|
||||
* Based on https://github.com/chriskempson/tomorrow-theme
|
||||
* @author Rose Pritchard
|
||||
*/
|
||||
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
color: #ccc;
|
||||
background: none;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*='language-'] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*='language-'] {
|
||||
padding: 0.1em;
|
||||
border-radius: 0.3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.attr-name,
|
||||
.token.namespace,
|
||||
.token.deleted {
|
||||
color: #e2777a;
|
||||
}
|
||||
|
||||
.token.function-name {
|
||||
color: #6196cc;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.function {
|
||||
color: #f08d49;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.class-name,
|
||||
.token.constant,
|
||||
.token.symbol {
|
||||
color: #f8c555;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.important,
|
||||
.token.atrule,
|
||||
.token.keyword,
|
||||
.token.builtin {
|
||||
color: #cc99cd;
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.attr-value,
|
||||
.token.regex,
|
||||
.token.variable {
|
||||
color: #7ec699;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url {
|
||||
color: #67cdcc;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: green;
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
|
||||
* Based on https://github.com/chriskempson/tomorrow-theme
|
||||
* @author Rose Pritchard
|
||||
*/
|
||||
|
||||
:global {
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #ccc;
|
||||
background: none;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.attr-name,
|
||||
.token.namespace,
|
||||
.token.deleted {
|
||||
color: #e2777a;
|
||||
}
|
||||
|
||||
.token.function-name {
|
||||
color: #6196cc;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.function {
|
||||
color: #f08d49;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.class-name,
|
||||
.token.constant,
|
||||
.token.symbol {
|
||||
color: #f8c555;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.important,
|
||||
.token.atrule,
|
||||
.token.keyword,
|
||||
.token.builtin {
|
||||
color: #cc99cd;
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.attr-value,
|
||||
.token.regex,
|
||||
.token.variable {
|
||||
color: #7ec699;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url {
|
||||
color: #67cdcc;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
27
frontend/styles/transitions.css
Normal file
27
frontend/styles/transitions.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.fadeIn-appear {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
.fadeIn-appear.fadeIn-appear-active {
|
||||
opacity: 1;
|
||||
transition: opacity 250ms ease-in;
|
||||
transition-delay: 0.35s;
|
||||
}
|
||||
|
||||
.fadeIn-enter {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
.fadeIn-enter.fadeIn-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 200ms ease-in;
|
||||
}
|
||||
|
||||
.fadeIn-leave {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fadeIn-leave.fadeIn-leave-active {
|
||||
opacity: 0.01;
|
||||
transition: opacity 200ms ease-in;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
:global {
|
||||
.fadeIn-appear {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
.fadeIn-appear.fadeIn-appear-active {
|
||||
opacity: 1;
|
||||
transition: opacity 250ms ease-in;
|
||||
transition-delay: 0.35s;
|
||||
}
|
||||
|
||||
.fadeIn-enter {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
.fadeIn-enter.fadeIn-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 200ms ease-in;
|
||||
}
|
||||
|
||||
.fadeIn-leave {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fadeIn-leave.fadeIn-leave-active {
|
||||
opacity: 0.01;
|
||||
transition: opacity 200ms ease-in;
|
||||
}
|
||||
}
|
||||
@@ -89,11 +89,11 @@ class ApiClient {
|
||||
});
|
||||
};
|
||||
|
||||
get = (path: string, data?: Object, options?: Object) => {
|
||||
get = (path: string, data: ?Object, options?: Object) => {
|
||||
return this.fetch(path, 'GET', data, options);
|
||||
};
|
||||
|
||||
post = (path: string, data?: Object, options?: Object) => {
|
||||
post = (path: string, data: ?Object, options?: Object) => {
|
||||
return this.fetch(path, 'POST', data, options);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ export function newCollectionUrl(): string {
|
||||
return '/collections/new';
|
||||
}
|
||||
|
||||
export function collectionUrl(collectionId: string): string {
|
||||
return `/collections/${collectionId}`;
|
||||
}
|
||||
|
||||
export function documentUrl(doc: Document): string {
|
||||
return doc.url;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user