Merge master
This commit is contained in:
commit
4d53c4aa00
20
.eslintrc
20
.eslintrc
@ -6,10 +6,7 @@
|
||||
"plugin:import/warnings",
|
||||
"plugin:flowtype/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"flowtype"
|
||||
],
|
||||
"plugins": ["prettier", "flowtype"],
|
||||
"rules": {
|
||||
"eqeqeq": 2,
|
||||
"no-unused-vars": 2,
|
||||
@ -22,7 +19,7 @@
|
||||
"import/no-unresolved": [
|
||||
"error",
|
||||
{
|
||||
"ignore": [ "slate-drop-or-paste-images" ]
|
||||
"ignore": ["slate-drop-or-paste-images"]
|
||||
}
|
||||
],
|
||||
// Flow
|
||||
@ -33,14 +30,8 @@
|
||||
"annotationStyle": "line"
|
||||
}
|
||||
],
|
||||
"flowtype/space-after-type-colon": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"flowtype/space-before-type-colon": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"flowtype/space-after-type-colon": [2, "always"],
|
||||
"flowtype/space-before-type-colon": [2, "never"],
|
||||
// Enforce that code is formatted with prettier.
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
@ -65,7 +56,8 @@
|
||||
"SLACK_REDIRECT_URI": true,
|
||||
"DEPLOYMENT": true,
|
||||
"BASE_URL": true,
|
||||
"BUGSNAG_KEY": true,
|
||||
"afterAll": true,
|
||||
"Bugsnag": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
1. Install dependencies with `yarn`
|
||||
1. Register a Slack app at https://api.slack.com/apps
|
||||
1. Copy the file `.env.sample` to `.env` and fill out the keys
|
||||
1. Run DB migrations `yarn run sequelize -- db:migrate`
|
||||
1. Run DB migrations `yarn sequelize -- db:migrate`
|
||||
1. Start the development server `yarn start`
|
||||
|
||||
|
||||
@ -16,12 +16,12 @@
|
||||
Sequelize is used to create and run migrations, for example:
|
||||
|
||||
```
|
||||
yarn run sequelize migration:create
|
||||
yarn run sequelize db:migrate
|
||||
yarn sequelize migration:create
|
||||
yarn sequelize db:migrate
|
||||
```
|
||||
|
||||
Or to run migrations on test database:
|
||||
|
||||
```
|
||||
yarn run sequelize db:migrate -- --env test
|
||||
yarn sequelize db:migrate --env test
|
||||
```
|
||||
|
3
app.json
3
app.json
@ -45,6 +45,9 @@
|
||||
},
|
||||
"URL": {
|
||||
"required": true
|
||||
},
|
||||
"BUGSNAG_KEY": {
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"formation": {},
|
||||
|
1
flow-typed/globals.js
vendored
1
flow-typed/globals.js
vendored
@ -3,5 +3,6 @@ declare var __DEV__: string;
|
||||
declare var SLACK_REDIRECT_URI: string;
|
||||
declare var SLACK_KEY: string;
|
||||
declare var BASE_URL: string;
|
||||
declare var BUGSNAG_KEY: ?string;
|
||||
declare var DEPLOYMENT: string;
|
||||
declare var Bugsnag: any;
|
||||
|
@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
|
||||
import Document from 'models/Document';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import Icon from 'components/Icon';
|
||||
import StarredIcon from 'components/Icon/StarredIcon';
|
||||
import PublishingInfo from './components/PublishingInfo';
|
||||
|
||||
type Props = {
|
||||
@ -15,18 +15,13 @@ type Props = {
|
||||
innerRef?: Function,
|
||||
};
|
||||
|
||||
const StyledStar = styled(({ solid, ...props }) => <Icon {...props} />).attrs({
|
||||
type: 'Star',
|
||||
color: color.text,
|
||||
})`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 1px;
|
||||
margin-left: 4px;
|
||||
const StyledStar = styled(({ solid, ...props }) => (
|
||||
<StarredIcon color={solid ? color.black : color.text} {...props} />
|
||||
))`
|
||||
position: absolute;
|
||||
opacity: ${props => (props.solid ? '1 !important' : 0)};
|
||||
transition: all 100ms ease-in-out;
|
||||
|
||||
${props => props.solid && 'polygon { fill: #000};'}
|
||||
margin-left: 2px;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
|
@ -40,11 +40,7 @@ class DropToImport extends Component {
|
||||
text,
|
||||
};
|
||||
|
||||
if (documentId) {
|
||||
data.parentDocument = {
|
||||
id: documentId,
|
||||
};
|
||||
}
|
||||
if (documentId) data.parentDocument = documentId;
|
||||
|
||||
let document = new Document(data);
|
||||
document = await document.save();
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@ -85,7 +85,7 @@ const Label = styled(Flex).attrs({
|
||||
`;
|
||||
|
||||
const Menu = styled.div`
|
||||
animation: ${fadeAndScaleIn} 250ms ease;
|
||||
animation: ${fadeAndScaleIn} 200ms ease;
|
||||
transform-origin: 75% 0;
|
||||
|
||||
position: absolute;
|
||||
|
@ -1,13 +1,14 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'components/Flex';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
const DropdownMenuItem = ({
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
onClick?: () => void,
|
||||
onClick?: SyntheticEvent => void,
|
||||
children?: React.Element<any>,
|
||||
}) => {
|
||||
return (
|
||||
@ -17,18 +18,21 @@ const DropdownMenuItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItem = styled.div`
|
||||
const MenuItem = styled(Flex)`
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
height: 32px;
|
||||
|
||||
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%;
|
||||
@ -37,6 +41,10 @@ const MenuItem = styled.div`
|
||||
&:hover {
|
||||
color: ${color.white};
|
||||
background: ${color.primary};
|
||||
|
||||
svg {
|
||||
fill: ${color.white};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -8,6 +8,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 Markdown from './serializer';
|
||||
import createSchema from './schema';
|
||||
@ -22,7 +23,7 @@ type Props = {
|
||||
onCancel: Function,
|
||||
onImageUploadStart: Function,
|
||||
onImageUploadStop: Function,
|
||||
emoji: string,
|
||||
emoji?: string,
|
||||
readOnly: boolean,
|
||||
};
|
||||
|
||||
@ -173,6 +174,8 @@ type KeyData = {
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const { readOnly, emoji, onSave } = this.props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onDrop={this.handleDrop}
|
||||
@ -183,24 +186,31 @@ type KeyData = {
|
||||
auto
|
||||
>
|
||||
<MaxWidth column auto>
|
||||
<Header onClick={this.focusAtStart} readOnly={this.props.readOnly} />
|
||||
<Toolbar state={this.state.state} onChange={this.onChange} />
|
||||
<Header onClick={this.focusAtStart} readOnly={readOnly} />
|
||||
{!readOnly &&
|
||||
<Toolbar state={this.state.state} onChange={this.onChange} />}
|
||||
{!readOnly &&
|
||||
<BlockInsert
|
||||
state={this.state.state}
|
||||
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.state.state}
|
||||
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;
|
||||
|
196
frontend/components/Editor/components/BlockInsert.js
Normal file
196
frontend/components/Editor/components/BlockInsert.js
Normal file
@ -0,0 +1,196 @@
|
||||
// @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 PlusIcon from 'components/Icon/PlusIcon';
|
||||
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={<PlusIcon />}
|
||||
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};
|
||||
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
||||
line-height: 0;
|
||||
margin-top: -2px;
|
||||
margin-left: -4px;
|
||||
transform: scale(.9);
|
||||
|
||||
${({ active }) => active && `
|
||||
transform: scale(1);
|
||||
opacity: .9;
|
||||
`}
|
||||
`;
|
17
frontend/components/Editor/components/HorizontalRule.js
Normal file
17
frontend/components/Editor/components/HorizontalRule.js
Normal 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;
|
@ -144,15 +144,16 @@ const Menu = styled.div`
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
opacity: 0;
|
||||
background-color: #222;
|
||||
background-color: #2F3336;
|
||||
border-radius: 4px;
|
||||
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
||||
transform: scale(.95);
|
||||
transition: opacity 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275), transform 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
line-height: 0;
|
||||
height: 40px;
|
||||
min-width: 260px;
|
||||
|
||||
${({ active }) => active && `
|
||||
transform: translateY(-6px);
|
||||
transform: translateY(-6px) scale(1);
|
||||
opacity: 1;
|
||||
`}
|
||||
`;
|
||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { fontWeight, color } from 'styles/constants';
|
||||
import Document from 'models/Document';
|
||||
import Icon from 'components/Icon';
|
||||
import GoToIcon from 'components/Icon/GoToIcon';
|
||||
|
||||
type Props = {
|
||||
innerRef?: Function,
|
||||
@ -14,7 +14,7 @@ type Props = {
|
||||
function DocumentResult({ document, ...rest }: Props) {
|
||||
return (
|
||||
<ListItem {...rest} href="">
|
||||
<i><Icon type="ChevronRight" light /></i>
|
||||
<i><GoToIcon light /></i>
|
||||
{document.title}
|
||||
</ListItem>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { State } from '../../../types';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import BoldIcon from 'components/Icon/BoldIcon';
|
||||
@ -9,9 +10,8 @@ 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 {
|
||||
class FormattingToolbar extends Component {
|
||||
props: {
|
||||
state: State,
|
||||
onChange: Function,
|
||||
@ -93,10 +93,11 @@ export default class FormattingToolbar extends Component {
|
||||
{this.renderMarkButton('bold', BoldIcon)}
|
||||
{this.renderMarkButton('italic', ItalicIcon)}
|
||||
{this.renderMarkButton('deleted', StrikethroughIcon)}
|
||||
{this.renderMarkButton('code', CodeIcon)}
|
||||
<Separator />
|
||||
{this.renderBlockButton('heading1', Heading1Icon)}
|
||||
{this.renderBlockButton('heading2', Heading2Icon)}
|
||||
{this.renderBlockButton('bulleted-list', BulletedListIcon)}
|
||||
{this.renderMarkButton('code', CodeIcon)}
|
||||
<Separator />
|
||||
<ToolbarButton onMouseDown={this.onCreateLink}>
|
||||
<LinkIcon light />
|
||||
</ToolbarButton>
|
||||
@ -104,3 +105,14 @@ export default class FormattingToolbar extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Separator = styled.div`
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: #FFF;
|
||||
opacity: .2;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
export default FormattingToolbar;
|
||||
|
@ -11,7 +11,9 @@ import DocumentResult from './DocumentResult';
|
||||
import type { State } from '../../../types';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import keydown from 'react-keydown';
|
||||
import Icon from 'components/Icon';
|
||||
import CloseIcon from 'components/Icon/CloseIcon';
|
||||
import OpenIcon from 'components/Icon/OpenIcon';
|
||||
import TrashIcon from 'components/Icon/TrashIcon';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
@keydown
|
||||
@ -109,16 +111,15 @@ class LinkToolbar extends Component {
|
||||
|
||||
save = (href: string) => {
|
||||
href = href.trim();
|
||||
const transform = this.props.state.transform();
|
||||
transform.unwrapInline('link');
|
||||
const { state } = this.props;
|
||||
const transform = state.transform();
|
||||
|
||||
if (href) {
|
||||
const data = { href };
|
||||
transform.wrapInline({ type: 'link', data });
|
||||
if (state.selection.isExpanded) {
|
||||
transform.unwrapInline('link');
|
||||
if (href) transform.wrapInline({ type: 'link', data: { href } });
|
||||
}
|
||||
|
||||
const state = transform.apply();
|
||||
this.props.onChange(state);
|
||||
this.props.onChange(transform.apply());
|
||||
this.props.onBlur();
|
||||
};
|
||||
|
||||
@ -144,12 +145,10 @@ class LinkToolbar extends Component {
|
||||
/>
|
||||
{this.isEditing &&
|
||||
<ToolbarButton onMouseDown={this.openLink}>
|
||||
<Icon type="ExternalLink" light />
|
||||
<OpenIcon light />
|
||||
</ToolbarButton>}
|
||||
<ToolbarButton onMouseDown={this.removeLink}>
|
||||
{this.isEditing
|
||||
? <Icon type="Trash2" light />
|
||||
: <Icon type="XCircle" light />}
|
||||
{this.isEditing ? <TrashIcon light /> : <CloseIcon light />}
|
||||
</ToolbarButton>
|
||||
</LinkEditor>
|
||||
{hasResults &&
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
7
frontend/components/Editor/plugins/EditList.js
Normal file
7
frontend/components/Editor/plugins/EditList.js
Normal 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',
|
||||
});
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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>,
|
||||
|
21
frontend/components/Empty/Empty.js
Normal file
21
frontend/components/Empty/Empty.js
Normal 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;
|
3
frontend/components/Empty/index.js
Normal file
3
frontend/components/Empty/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Empty from './Empty';
|
||||
export default Empty;
|
@ -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;
|
@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import HtmlContent from './HtmlContent';
|
||||
export default HtmlContent;
|
15
frontend/components/Icon/BackIcon.js
Normal file
15
frontend/components/Icon/BackIcon.js
Normal file
@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function BackIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M7.20710678,8.79289322 C6.81658249,8.40236893 6.18341751,8.40236893 5.79289322,8.79289322 C5.40236893,9.18341751 5.40236893,9.81658249 5.79289322,10.2071068 L10.7928932,15.2071068 C11.1834175,15.5976311 11.8165825,15.5976311 12.2071068,15.2071068 L17.2071068,10.2071068 C17.5976311,9.81658249 17.5976311,9.18341751 17.2071068,8.79289322 C16.8165825,8.40236893 16.1834175,8.40236893 15.7928932,8.79289322 L11.5,13.0857864 L7.20710678,8.79289322 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -6,16 +6,7 @@ import type { Props } from './Icon';
|
||||
export default function BoldIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z" />
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
</svg>
|
||||
<path d="M18,15 C18,17.209139 16.209139,19 14,19 L8,19 C7.44771525,19 7,18.5522847 7,18 L7,6 C7,5.44771525 7.44771525,5 8,5 L13,5 C15.209139,5 17,6.790861 17,9 C17,9.9796381 16.6478342,10.8770235 16.0631951,11.5724638 C17.2238614,12.2726251 18,13.5456741 18,15 Z M9,17 L14,17 C15.1045695,17 16,16.1045695 16,15 C16,13.8954305 15.1045695,13 14,13 L9,13 L9,17 Z M9,11 L13,11 C14.1045695,11 15,10.1045695 15,9 C15,7.8954305 14.1045695,7 13,7 L9,7 L9,11 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
@ -6,16 +6,7 @@ import type { Props } from './Icon';
|
||||
export default function BulletedListIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z" />
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
</svg>
|
||||
<path d="M10,6 L19,6 C19.5522847,6 20,6.44771525 20,7 L20,7 C20,7.55228475 19.5522847,8 19,8 L10,8 C9.44771525,8 9,7.55228475 9,7 L9,7 L9,7 C9,6.44771525 9.44771525,6 10,6 Z M10,16 L19,16 C19.5522847,16 20,16.4477153 20,17 C20,17.5522847 19.5522847,18 19,18 L10,18 C9.44771525,18 9,17.5522847 9,17 C9,16.4477153 9.44771525,16 10,16 Z M10,11 L19,11 C19.5522847,11 20,11.4477153 20,12 C20,12.5522847 19.5522847,13 19,13 L10,13 C9.44771525,13 9,12.5522847 9,12 C9,11.4477153 9.44771525,11 10,11 Z M5,10.5 L5,10.5 C5.82842712,10.5 6.5,11.1715729 6.5,12 C6.5,12.8284271 5.82842712,13.5 5,13.5 C4.17157288,13.5 3.5,12.8284271 3.5,12 C3.5,11.1715729 4.17157288,10.5 5,10.5 L5,10.5 Z M5,5.5 L5,5.5 C5.82842712,5.5 6.5,6.17157288 6.5,7 L6.5,7 C6.5,7.82842712 5.82842712,8.5 5,8.5 C4.17157288,8.5 3.5,7.82842712 3.5,7 L3.5,7 L3.5,7 C3.5,6.17157288 4.17157288,5.5 5,5.5 L5,5.5 Z M5,15.5 L5,15.5 C5.82842712,15.5 6.5,16.1715729 6.5,17 C6.5,17.8284271 5.82842712,18.5 5,18.5 C4.17157288,18.5 3.5,17.8284271 3.5,17 C3.5,16.1715729 4.17157288,15.5 5,15.5 L5,15.5 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
20
frontend/components/Icon/CheckboxIcon.js
Normal file
20
frontend/components/Icon/CheckboxIcon.js
Normal file
@ -0,0 +1,20 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CheckboxIcon({
|
||||
checked,
|
||||
...rest
|
||||
}: Props & { checked: boolean }) {
|
||||
return (
|
||||
<Icon {...rest}>
|
||||
{checked
|
||||
? <path d="M8,5 L16,5 L16,5 C17.6568542,5 19,6.34314575 19,8 L19,16 C19,17.6568542 17.6568542,19 16,19 L8,19 L8,19 C6.34314575,19 5,17.6568542 5,16 L5,8 L5,8 C5,6.34314575 6.34314575,5 8,5 L8,5 Z M10.958729,12.8883948 L9.26824635,10.8598156 C8.91468227,10.4355387 8.28411757,10.3782146 7.85984067,10.7317787 C7.43556378,11.0853428 7.37823971,11.7159075 7.73180379,12.1401844 L10.2318038,15.1401844 C10.6450125,15.6360348 11.4127535,15.616362 11.8000251,15.1 L16.3000251,9.1 C16.6313959,8.6581722 16.5418529,8.03137085 16.1000251,7.7 C15.6581973,7.36862915 15.0313959,7.4581722 14.7000251,7.9 L10.958729,12.8883948 Z" />
|
||||
: <path
|
||||
d="M8,5 L16,5 L16,5 C17.6568542,5 19,6.34314575 19,8 L19,16 C19,17.6568542 17.6568542,19 16,19 L8,19 L8,19 C6.34314575,19 5,17.6568542 5,16 L5,8 L5,8 C5,6.34314575 6.34314575,5 8,5 L8,5 Z M8,7 C7.44771525,7 7,7.44771525 7,8 L7,16 C7,16.5522847 7.44771525,17 8,17 L16,17 C16.5522847,17 17,16.5522847 17,16 L17,8 C17,7.44771525 16.5522847,7 16,7 L8,7 Z"
|
||||
id="path-1"
|
||||
/>}
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function NextIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
</svg>
|
||||
</Icon>
|
||||
);
|
||||
}
|
15
frontend/components/Icon/CloseIcon.js
Normal file
15
frontend/components/Icon/CloseIcon.js
Normal file
@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CloseIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M12,10.5857864 L8.70710678,7.29289322 C8.31658249,6.90236893 7.68341751,6.90236893 7.29289322,7.29289322 C6.90236893,7.68341751 6.90236893,8.31658249 7.29289322,8.70710678 L10.5857864,12 L7.29289322,15.2928932 C6.90236893,15.6834175 6.90236893,16.3165825 7.29289322,16.7071068 C7.68341751,17.0976311 8.31658249,17.0976311 8.70710678,16.7071068 L12,13.4142136 L15.2928932,16.7071068 C15.6834175,17.0976311 16.3165825,17.0976311 16.7071068,16.7071068 C17.0976311,16.3165825 17.0976311,15.6834175 16.7071068,15.2928932 L13.4142136,12 L16.7071068,8.70710678 C17.0976311,8.31658249 17.0976311,7.68341751 16.7071068,7.29289322 C16.3165825,6.90236893 15.6834175,6.90236893 15.2928932,7.29289322 L12,10.5857864 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -6,16 +6,7 @@ import type { Props } from './Icon';
|
||||
export default function CodeIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" />
|
||||
</svg>
|
||||
<path d="M11.9805807,17.1961161 C11.8722687,17.7376759 11.3454436,18.0888926 10.8038839,17.9805807 C10.2623241,17.8722687 9.91110737,17.3454436 10.0194193,16.8038839 L12.0194193,6.80388386 C12.1277313,6.26232411 12.6545564,5.91110737 13.1961161,6.01941932 C13.7376759,6.12773127 14.0888926,6.65455638 13.9805807,7.19611614 L11.9805807,17.1961161 Z M6.41421356,12 L8.70710678,14.2928932 C9.09763107,14.6834175 9.09763107,15.3165825 8.70710678,15.7071068 C8.31658249,16.0976311 7.68341751,16.0976311 7.29289322,15.7071068 L4.29289322,12.7071068 C3.90236893,12.3165825 3.90236893,11.6834175 4.29289322,11.2928932 L7.29289322,8.29289322 C7.68341751,7.90236893 8.31658249,7.90236893 8.70710678,8.29289322 C9.09763107,8.68341751 9.09763107,9.31658249 8.70710678,9.70710678 L6.41421356,12 Z M15.2928932,14.2928932 L17.5857864,12 L15.2928932,9.70710678 C14.9023689,9.31658249 14.9023689,8.68341751 15.2928932,8.29289322 C15.6834175,7.90236893 16.3165825,7.90236893 16.7071068,8.29289322 L19.7071068,11.2928932 C20.0976311,11.6834175 20.0976311,12.3165825 19.7071068,12.7071068 L16.7071068,15.7071068 C16.3165825,16.0976311 15.6834175,16.0976311 15.2928932,15.7071068 C14.9023689,15.3165825 14.9023689,14.6834175 15.2928932,14.2928932 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
12
frontend/components/Icon/CollapsedIcon.js
Normal file
12
frontend/components/Icon/CollapsedIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CollapsedIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M8.23823905,10.6097108 L11.207376,14.4695888 L11.207376,14.4695888 C11.54411,14.907343 12.1719566,14.989236 12.6097108,14.652502 C12.6783439,14.5997073 12.7398293,14.538222 12.792624,14.4695888 L15.761761,10.6097108 L15.761761,10.6097108 C16.0984949,10.1719566 16.0166019,9.54410997 15.5788477,9.20737601 C15.4040391,9.07290785 15.1896811,9 14.969137,9 L9.03086304,9 L9.03086304,9 C8.47857829,9 8.03086304,9.44771525 8.03086304,10 C8.03086304,10.2205442 8.10377089,10.4349022 8.23823905,10.6097108 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
17
frontend/components/Icon/CollectionIcon.js
Normal file
17
frontend/components/Icon/CollectionIcon.js
Normal file
@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CollectionIcon({
|
||||
expanded,
|
||||
...rest
|
||||
}: Props & { expanded: boolean }) {
|
||||
return (
|
||||
<Icon {...rest}>
|
||||
{expanded
|
||||
? <path d="M14,3.28571429 C15.1045695,3.12791864 16,3.8954305 16,5 L16,19 C16,20.1045695 15.1045695,20.8720814 14,20.7142857 L7,19.2857143 C5.8954305,19.1279186 5,18.3284271 5,17.5 L5,6.5 C5,5.67157288 5.8954305,4.87208136 7,4.71428571 L14,3.28571429 Z M7.5,6.47598949 L8.5,6.37337629 C8.77614237,6.34504044 9,6.49817875 9,6.71542029 L9,17.2845797 C9,17.5018212 8.77614237,17.6549596 8.5,17.6266237 L7.5,17.5240105 C7.22385763,17.4956747 7,17.3042518 7,17.0964555 L7,6.90354448 C7,6.69574823 7.22385763,6.50432534 7.5,6.47598949 Z M17,4 C18.1045695,4 19,4.8954305 19,6 L19,18 C19,19.1045695 18.1045695,20 17,20 L17,4 Z" />
|
||||
: <path d="M7,4 L17,4 C18.1045695,4 19,4.8954305 19,6 L19,18 C19,19.1045695 18.1045695,20 17,20 L7,20 C5.8954305,20 5,19.1045695 5,18 L5,6 L5,6 C5,4.8954305 5.8954305,4 7,4 L7,4 Z M7.5,6 C7.22385763,6 7,6.22385763 7,6.5 L7,17.5 C7,17.7761424 7.22385763,18 7.5,18 L8.5,18 C8.77614237,18 9,17.7761424 9,17.5 L9,6.5 C9,6.22385763 8.77614237,6 8.5,6 L7.5,6 Z" />}
|
||||
</Icon>
|
||||
);
|
||||
}
|
15
frontend/components/Icon/DocumentIcon.js
Normal file
15
frontend/components/Icon/DocumentIcon.js
Normal file
@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function DocumentIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M13.5,7.5 L16.5,10.5 L9.66719477,17.3328052 L9.66719477,17.3328052 C9.55724743,17.4427526 9.42317605,17.5255464 9.27563076,17.5746098 L6.42375483,18.5229479 L6.42375483,18.5229479 C6.03070288,18.65365 5.60611633,18.4409735 5.47541424,18.0479215 C5.42427386,17.8941303 5.42432697,17.727911 5.47556562,17.5741526 L5.47556562,17.5741526 L6.42535348,14.7240015 L6.42535348,14.7240015 C6.47444294,14.5766924 6.55716155,14.4428385 6.66695621,14.3330438 L13.5,7.5 Z M14.5,6.5 L15.7928932,5.20710678 L15.7928932,5.20710678 C16.1834175,4.81658249 16.8165825,4.81658249 17.2071068,5.20710678 L18.7928932,6.79289322 L18.7928932,6.79289322 C19.1834175,7.18341751 19.1834175,7.81658249 18.7928932,8.20710678 L17.5,9.5 L14.5,6.5 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
12
frontend/components/Icon/EditIcon.js
Normal file
12
frontend/components/Icon/EditIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function EditIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M13.5,7.5 L16.5,10.5 L9.66719477,17.3328052 L9.66719477,17.3328052 C9.55724743,17.4427526 9.42317605,17.5255464 9.27563076,17.5746098 L6.42375483,18.5229479 L6.42375483,18.5229479 C6.03070288,18.65365 5.60611633,18.4409735 5.47541424,18.0479215 C5.42427386,17.8941303 5.42432697,17.727911 5.47556562,17.5741526 L5.47556562,17.5741526 L6.42535348,14.7240015 L6.42535348,14.7240015 C6.47444294,14.5766924 6.55716155,14.4428385 6.66695621,14.3330438 L13.5,7.5 Z M14.5,6.5 L15.7928932,5.20710678 L15.7928932,5.20710678 C16.1834175,4.81658249 16.8165825,4.81658249 17.2071068,5.20710678 L18.7928932,6.79289322 L18.7928932,6.79289322 C19.1834175,7.18341751 19.1834175,7.81658249 18.7928932,8.20710678 L17.5,9.5 L14.5,6.5 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
15
frontend/components/Icon/GoToIcon.js
Normal file
15
frontend/components/Icon/GoToIcon.js
Normal file
@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function GoToIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M14.080855,4.6060807 L8.08085497,18.6060807 C7.86329935,19.1137105 8.09845092,19.7015894 8.6060807,19.919145 C9.11371048,20.1367007 9.70158941,19.9015491 9.91914503,19.3939193 L15.919145,5.3939193 C16.1367007,4.88628952 15.9015491,4.29841059 15.3939193,4.08085497 C14.8862895,3.86329935 14.2984106,4.09845092 14.080855,4.6060807 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -6,16 +6,7 @@ import type { Props } from './Icon';
|
||||
export default function Heading1Icon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14h-2V9h-2V7h4v10z" />
|
||||
</svg>
|
||||
<path d="M7,4 L7,4 C7.55228475,4 8,4.44771525 8,5 L8,19 C8,19.5522847 7.55228475,20 7,20 C6.44771525,20 6,19.5522847 6,19 L6,5 L6,5 C6,4.44771525 6.44771525,4 7,4 L7,4 Z M8,11 L16,11 L16,13 L8,13 L8,11 Z M17,4 C17.5522847,4 18,4.44771525 18,5 L18,19 C18,19.5522847 17.5522847,20 17,20 C16.4477153,20 16,19.5522847 16,19 L16,5 L16,5 C16,4.44771525 16.4477153,4 17,4 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
@ -6,16 +6,7 @@ import type { Props } from './Icon';
|
||||
export default function Heading2Icon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4 8c0 1.11-.9 2-2 2h-2v2h4v2H9v-4c0-1.11.9-2 2-2h2V9H9V7h4c1.1 0 2 .89 2 2v2z" />
|
||||
</svg>
|
||||
<path d="M14,13 L10,13 L10,16 C10,16.5522847 9.55228475,17 9,17 C8.44771525,17 8,16.5522847 8,16 L8,8 L8,8 C8,7.44771525 8.44771525,7 9,7 L9,7 L9,7 C9.55228475,7 10,7.44771525 10,8 L10,11 L14,11 L14,8 L14,8 C14,7.44771525 14.4477153,7 15,7 C15.5522847,7 16,7.44771525 16,8 L16,16 C16,16.5522847 15.5522847,17 15,17 C14.4477153,17 14,16.5522847 14,16 L14,13 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
12
frontend/components/Icon/HomeIcon.js
Normal file
12
frontend/components/Icon/HomeIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function HomeIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M7,18 L9,18 L9,18 C9.55228475,18 10,17.5522847 10,17 L10,13.5 L10,13.5 C10,12.3954305 10.8954305,11.5 12,11.5 L12,11.5 L12,11.5 C13.1045695,11.5 14,12.3954305 14,13.5 L14,17 L14,17 C14,17.5522847 14.4477153,18 15,18 L17,18 L17,18 C17.5522847,18 18,17.5522847 18,17 L18,10.9367499 L18,10.9367499 C18,10.3431902 17.736354,9.78029498 17.2803688,9.40030733 L12.6401844,5.533487 L12.6401844,5.533487 C12.2693384,5.22444871 11.7306616,5.22444871 11.3598156,5.533487 L6.7196312,9.40030733 L6.7196312,9.40030733 C6.26364602,9.78029498 6,10.3431902 6,10.9367499 L6,17 L6,17 C6,17.5522847 6.44771525,18 7,18 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
15
frontend/components/Icon/HorizontalRuleIcon.js
Normal file
15
frontend/components/Icon/HorizontalRuleIcon.js
Normal file
@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function HorizontalRuleIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M5,11 L19,11 C19.5522847,11 20,11.4477153 20,12 C20,12.5522847 19.5522847,13 19,13 L5,13 C4.44771525,13 4,12.5522847 4,12 C4,11.4477153 4.44771525,11 5,11 L5,11 Z M7,6 L17,6 C17.5522847,6 18,6.44771525 18,7 L18,8 C18,8.55228475 17.5522847,9 17,9 L7,9 C6.44771525,9 6,8.55228475 6,8 L6,7 L6,7 C6,6.44771525 6.44771525,6 7,6 Z M7,15 L17,15 C17.5522847,15 18,15.4477153 18,16 L18,17 C18,17.5522847 17.5522847,18 17,18 L7,18 C6.44771525,18 6,17.5522847 6,17 L6,16 C6,15.4477153 6.44771525,15 7,15 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -1,53 +1,43 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import * as Icons from 'react-feather';
|
||||
|
||||
export type Props = {
|
||||
className?: string,
|
||||
type?: string,
|
||||
light?: boolean,
|
||||
black?: boolean,
|
||||
primary?: boolean,
|
||||
color?: string,
|
||||
size?: number,
|
||||
};
|
||||
|
||||
type BaseProps = {
|
||||
children?: React$Element<any>,
|
||||
children?: React$Element<*>,
|
||||
};
|
||||
|
||||
export default function Icon({
|
||||
children,
|
||||
light,
|
||||
type,
|
||||
className,
|
||||
...rest
|
||||
}: Props & BaseProps) {
|
||||
if (type) {
|
||||
children = React.createElement(Icons[type], {
|
||||
size: '1em',
|
||||
color: light ? color.white : undefined,
|
||||
...rest,
|
||||
});
|
||||
const size = rest.size ? rest.size + 'px' : '24px';
|
||||
|
||||
return (
|
||||
<FeatherWrapper {...rest}>
|
||||
{children}
|
||||
</FeatherWrapper>
|
||||
);
|
||||
}
|
||||
let fill = color.slateDark;
|
||||
if (rest.color) fill = rest.color;
|
||||
if (rest.light) fill = color.white;
|
||||
if (rest.black) fill = color.black;
|
||||
if (rest.primary) fill = color.primary;
|
||||
|
||||
return (
|
||||
<Wrapper light={light} {...rest}>
|
||||
<svg
|
||||
fill={fill}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</Wrapper>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const FeatherWrapper = styled.span`
|
||||
position: relative;
|
||||
top: .1em;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.span`
|
||||
svg {
|
||||
fill: ${props => (props.light ? color.white : color.black)}
|
||||
}
|
||||
`;
|
||||
|
12
frontend/components/Icon/ImageIcon.js
Normal file
12
frontend/components/Icon/ImageIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function ImageIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M19,14.5857864 L13.7071068,9.29289322 C13.3165825,8.90236893 12.6834175,8.90236893 12.2928932,9.29289322 L8,13.5857864 L5,10.5857864 L5,7 L5,7 C5,5.8954305 5.8954305,5 7,5 L7,5 L17,5 L17,5 C18.1045695,5 19,5.8954305 19,7 L19,14.5857864 Z M18.9642423,17.3784559 C18.7873485,18.3020643 17.9751801,19 17,19 L7,19 L7,19 C5.8954305,19 5,18.1045695 5,17 L5,13.4142136 L7.29289322,15.7071068 C7.68341751,16.0976311 8.31658249,16.0976311 8.70710678,15.7071068 L13,11.4142136 L18.9642423,17.3784559 Z M8.5,10 C9.32842712,10 10,9.32842712 10,8.5 C10,7.67157288 9.32842712,7 8.5,7 C7.67157288,7 7,7.67157288 7,8.5 C7,9.32842712 7.67157288,10 8.5,10 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -6,16 +6,7 @@ import type { Props } from './Icon';
|
||||
export default function ItalicIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z" />
|
||||
</svg>
|
||||
<path d="M12.5,6 C11.6715729,6 11,5.32842712 11,4.5 C11,3.67157288 11.6715729,3 12.5,3 C13.3284271,3 14,3.67157288 14,4.5 C14,5.32842712 13.3284271,6 12.5,6 Z M10.7801961,10 L10,10 C9.44771525,10 9,9.55228475 9,9 C9,8.44771525 9.44771525,8 10,8 L12,8 C12.6310464,8 13.1043391,8.57732421 12.9805807,9.19611614 L11.4590271,16.8038839 C11.3507152,17.3454436 11.7019319,17.8722687 12.2434917,17.9805807 C12.3080649,17.9934953 12.3737558,18 12.4396078,18 L13,18 C13.5522847,18 14,18.4477153 14,19 C14,19.5522847 13.5522847,20 13,20 L12.4396078,20 C12.2420518,20 12.044979,19.9804859 11.8512594,19.941742 C10.2265801,19.6168062 9.17292993,18.0363309 9.49786578,16.4116516 L10.7801961,10 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
@ -6,16 +6,7 @@ import type { Props } from './Icon';
|
||||
export default function LinkIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" />
|
||||
</svg>
|
||||
<path d="M11.0745387,14.3927712 C10.6944229,13.9921087 10.7110794,13.3591628 11.1117419,12.9790471 C11.5124045,12.5989313 12.1453503,12.6155878 12.5254661,13.0162503 C12.9108834,13.422501 13.5526571,13.4393898 13.9780169,13.0353595 L17.3813571,9.63201937 C18.2122318,8.80114469 18.2122318,7.45403068 17.3813571,6.62315601 C16.5504824,5.79228133 15.2033684,5.79228133 14.3724938,6.62315601 L13.5071091,7.48854062 C13.1165848,7.87906491 12.4834199,7.87906491 12.0928956,7.48854062 C11.7023713,7.09801633 11.7023713,6.46485135 12.0928956,6.07432706 L12.9582802,5.20894244 C14.5702035,3.59701919 17.1836474,3.59701919 18.7955707,5.20894244 C20.4074939,6.8208657 20.4074939,9.43430967 18.7955707,11.0462329 L15.3922305,14.4495731 C15.3640722,14.4774856 15.3640722,14.4774856 15.3354286,14.5048999 C14.1278529,15.6505487 12.2201874,15.6003469 11.0745387,14.3927712 Z M12.9299745,9.60955606 C13.3100902,10.0102186 13.2934337,10.6431644 12.8927712,11.0232802 C12.4921087,11.4033959 11.8591628,11.3867395 11.4790471,10.9860769 C11.0936298,10.5798262 10.4518561,10.5629375 10.0264962,10.9669677 L6.62315601,14.3703079 C5.79228133,15.2011826 5.79228133,16.5482966 6.62315601,17.3791712 C7.45403068,18.2100459 8.80114469,18.2100459 9.63201937,17.3791712 L10.497404,16.5137866 C10.8879283,16.1232623 11.5210933,16.1232623 11.9116175,16.5137866 C12.3021418,16.9043109 12.3021418,17.5374759 11.9116175,17.9280002 L11.0462329,18.7933848 C9.43430967,20.4053081 6.8208657,20.4053081 5.20894244,18.7933848 C3.59701919,17.1814616 3.59701919,14.5680176 5.20894244,12.9560943 L8.61228261,9.55275416 C8.64044095,9.52484169 8.64044095,9.52484169 8.66908451,9.49742738 C9.87666026,8.35177859 11.7843257,8.40198031 12.9299745,9.60955606 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
15
frontend/components/Icon/MoreIcon.js
Normal file
15
frontend/components/Icon/MoreIcon.js
Normal file
@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function MoreIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M12,14 C10.8954305,14 10,13.1045695 10,12 C10,10.8954305 10.8954305,10 12,10 C13.1045695,10 14,10.8954305 14,12 C14,13.1045695 13.1045695,14 12,14 Z M18,14 C16.8954305,14 16,13.1045695 16,12 C16,10.8954305 16.8954305,10 18,10 C19.1045695,10 20,10.8954305 20,12 C20,13.1045695 19.1045695,14 18,14 Z M6,14 C4.8954305,14 4,13.1045695 4,12 C4,10.8954305 4.8954305,10 6,10 C7.1045695,10 8,10.8954305 8,12 C8,13.1045695 7.1045695,14 6,14 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
15
frontend/components/Icon/NewDocumentIcon.js
Normal file
15
frontend/components/Icon/NewDocumentIcon.js
Normal file
@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function NewDocumentIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M19,18 L20,18 C20.5522847,18 21,18.4477153 21,19 C21,19.5522847 20.5522847,20 20,20 L19,20 L19,21 C19,21.5522847 18.5522847,22 18,22 C17.4477153,22 17,21.5522847 17,21 L17,20 L16,20 C15.4477153,20 15,19.5522847 15,19 C15,18.4477153 15.4477153,18 16,18 L17,18 L17,17 C17,16.4477153 17.4477153,16 18,16 C18.5522847,16 19,16.4477153 19,17 L19,18 Z M13.1000181,20 L7,20 C5.8954305,20 5,19.1045695 5,18 L5,6 L5,6 C5,4.8954305 5.8954305,4 7,4 L7,4 L14.5,4 L12,4 L12,9 C12,10.1045695 12.8954305,11 14,11 L19,11 L19,8.5 L19,14.1000181 C18.6768901,14.0344303 18.3424658,14 18,14 C15.2385763,14 13,16.2385763 13,19 C13,19.3424658 13.0344303,19.6768901 13.1000181,20 Z M14,4 L19,9 L14,9 L14,4 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
12
frontend/components/Icon/OpenIcon.js
Normal file
12
frontend/components/Icon/OpenIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function OpenIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M18,7.41421356 L11.7071068,13.7071068 C11.3165825,14.0976311 10.6834175,14.0976311 10.2928932,13.7071068 C9.90236893,13.3165825 9.90236893,12.6834175 10.2928932,12.2928932 L16.5857864,6 L14,6 C13.4477153,6 13,5.55228475 13,5 C13,4.44771525 13.4477153,4 14,4 L19,4 C19.5522847,4 20,4.44771525 20,5 L20,10 C20,10.5522847 19.5522847,11 19,11 C18.4477153,11 18,10.5522847 18,10 L18,7.41421356 Z M9,6 C9.55228475,6 10,6.44771525 10,7 C10,7.55228475 9.55228475,8 9,8 L6,8 L6,18 L16,18 L16,15 C16,14.4477153 16.4477153,14 17,14 C17.5522847,14 18,14.4477153 18,15 L18,18 C18,19.1045695 17.1045695,20 16,20 L6,20 C4.8954305,20 4,19.1045695 4,18 L4,8 C4,6.8954305 4.8954305,6 6,6 L9,6 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -6,16 +6,7 @@ import type { Props } from './Icon';
|
||||
export default function OrderedListIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z" />
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
</svg>
|
||||
<path d="M1,3.99978522 L1,2.70798687 L0.853553391,2.85442299 C0.658291245,3.04967116 0.341708755,3.04967116 0.146446609,2.85442299 C-0.0488155365,2.65917483 -0.0488155365,2.342615 0.146446609,2.14736684 L1.14644661,1.14743843 C1.46142904,0.83247855 2,1.05554597 2,1.50096651 L2,3.99978522 L2.5000358,3.99978522 L2.5000358,3.99978522 C2.7761584,3.99978522 3,4.22362682 3,4.49974942 C3,4.77587203 2.7761584,4.99971363 2.5000358,4.99971363 L1.53191883,4.99971363 C1.52136474,5.00037848 1.51072178,5.00071593 1.5,5.00071593 C1.48927822,5.00071593 1.47863526,5.00037848 1.46808117,4.99971363 L0.499964203,4.99971363 L0.499964203,4.99971363 C0.223841598,4.99971363 3.38152664e-17,4.77587203 0,4.49974942 C-3.38152664e-17,4.22362682 0.223841598,3.99978522 0.499964203,3.99978522 L0.499964203,3.99978522 L1,3.99978522 Z M5.99992841,1.99992841 L15.0000716,1.99992841 L15.0000716,1.99992841 C15.5523168,1.99992841 16,2.4476116 16,2.99985681 L16,2.99985681 L16,2.99985681 C16,3.55210202 15.5523168,3.99978522 15.0000716,3.99978522 L5.99992841,3.99978522 L5.99992841,3.99978522 C5.4476832,3.99978522 5,3.55210202 5,2.99985681 L5,2.99985681 L5,2.99985681 C5,2.4476116 5.4476832,1.99992841 5.99992841,1.99992841 Z M5.99992841,11.9992125 L15.0000716,11.9992125 L15.0000716,11.9992125 C15.5523168,11.9992125 16,12.4468957 16,12.9991409 L16,12.9991409 L16,12.9991409 C16,13.5513861 15.5523168,13.9990693 15.0000716,13.9990693 L5.99992841,13.9990693 C5.4476832,13.9990693 5,13.5513861 5,12.9991409 C5,12.4468957 5.4476832,11.9992125 5.99992841,11.9992125 Z M5.99992841,6.99957044 L15.0000716,6.99957044 L15.0000716,6.99957044 C15.5523168,6.99957044 16,7.44725364 16,7.99949885 L16,7.99949885 C16,8.55174406 15.5523168,8.99942725 15.0000716,8.99942725 L5.99992841,8.99942725 L5.99992841,8.99942725 C5.4476832,8.99942725 5,8.55174406 5,7.99949885 C5,7.44725364 5.4476832,6.99957044 5.99992841,6.99957044 Z M0.646446609,12.6466151 L1.29289322,12.0002148 L0.5,12.0002148 C0.223857625,12.0002148 0,11.7763732 0,11.5002506 C0,11.224128 0.223857625,11.0002864 0.5,11.0002864 L2.5,11.0002864 C2.94545243,11.0002864 3.16853582,11.5388188 2.85355339,11.8537787 L2.14380887,12.5634724 C2.64120863,12.728439 3,13.1973672 3,13.7500895 C3,14.440396 2.44035594,15 1.75,15 L0.5,15 C0.223857625,15 0,14.7761584 0,14.5000358 C0,14.2239132 0.223857625,14.0000716 0.5,14.0000716 L1.75,14.0000716 C1.88807119,14.0000716 2,13.8881508 2,13.7500895 C2,13.6120282 1.88807119,13.5001074 1.75,13.5001074 L1,13.5001074 C0.554547575,13.5001074 0.331464179,12.961575 0.646446609,12.6466151 Z M2.40096969,8.70045104 L2.00096969,9.00042956 L2.50096969,9.00042956 C2.77711207,9.00042956 3.00096969,9.22427116 3.00096969,9.50039376 C3.00096969,9.77651637 2.77711207,10.000358 2.50096969,10.000358 L0.500969693,10.000358 C0.0204635467,10.000358 -0.183435224,9.38870545 0.200969693,9.1004224 L1.80096969,7.90050831 C1.92687261,7.80608788 2.00096969,7.65790433 2.00096969,7.50053695 L2.00096969,7.25055485 C2.00096969,7.11249355 1.88904088,7.00057275 1.75096969,7.00057275 L1.50096969,7.00057275 C1.22482732,7.00057275 1.00096969,7.22441434 1.00096969,7.50053695 C1.00096969,7.77665955 0.777112068,8.00050115 0.500969693,8.00050115 C0.224827319,8.00050115 0.000969693445,7.77665955 0.000969693445,7.50053695 C0.000969693445,6.67216913 0.672542569,6.00064434 1.50096969,6.00064434 L1.75096969,6.00064434 C2.44132563,6.00064434 3.00096969,6.56024834 3.00096969,7.25055485 L3.00096969,7.50053695 C3.00096969,7.9726391 2.77867846,8.41718975 2.40096969,8.70045104 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
12
frontend/components/Icon/PlusIcon.js
Normal file
12
frontend/components/Icon/PlusIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function PlusIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M13,11 L13,6 C13,5.44771525 12.5522847,5 12,5 C11.4477153,5 11,5.44771525 11,6 L11,6 L11,11 L6,11 C5.44771525,11 5,11.4477153 5,12 C5,12.5522847 5.44771525,13 6,13 L11,13 L11,18 C11,18.5522847 11.4477153,19 12,19 C12.5522847,19 13,18.5522847 13,18 L13,13 L18,13 C18.5522847,13 19,12.5522847 19,12 C19,11.4477153 18.5522847,11 18,11 L13,11 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
12
frontend/components/Icon/SearchIcon.js
Normal file
12
frontend/components/Icon/SearchIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function SearchIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M16.1692714,14.047951 L19.0606602,16.9393398 C19.6464466,17.5251263 19.6464466,18.4748737 19.0606602,19.0606602 C18.4748737,19.6464466 17.5251263,19.6464466 16.9393398,19.0606602 L14.047951,16.1692714 C13.1546811,16.6971059 12.1127129,17 11,17 C7.6862915,17 5,14.3137085 5,11 C5,7.6862915 7.6862915,5 11,5 C14.3137085,5 17,7.6862915 17,11 C17,12.1127129 16.6971059,13.1546811 16.1692714,14.047951 Z M11,8 C9.34314575,8 8,9.34314575 8,11 C8,12.6568542 9.34314575,14 11,14 C12.6568542,14 14,12.6568542 14,11 C14,9.34314575 12.6568542,8 11,8 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
12
frontend/components/Icon/StarredIcon.js
Normal file
12
frontend/components/Icon/StarredIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function StarredIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M12,16.1500001 L8.79729751,17.8337604 L8.79729751,17.8337604 C8.30845292,18.0907612 7.70382577,17.9028147 7.44682496,17.4139701 C7.34448589,17.2193097 7.30917121,16.9963416 7.34634806,16.779584 L7.95800981,13.2133223 L5.36696906,10.6876818 L5.36696906,10.6876818 C4.97148548,10.3021806 4.96339318,9.66906733 5.34889439,9.27358375 C5.50240299,9.11610012 5.70354541,9.01361294 5.92118244,8.98198843 L9.50191268,8.46167787 L11.1032639,5.21698585 L11.1032639,5.21698585 C11.3476862,4.72173219 11.9473121,4.51839319 12.4425657,4.76281548 C12.6397783,4.86014572 12.7994058,5.01977324 12.8967361,5.21698585 L14.4980873,8.46167787 L18.0788176,8.98198843 L18.0788176,8.98198843 C18.6253624,9.06140605 19.0040439,9.5688489 18.9246263,10.1153938 C18.8930018,10.3330308 18.7905146,10.5341732 18.6330309,10.6876818 L16.0419902,13.2133223 L16.6536519,16.779584 L16.6536519,16.779584 C16.747013,17.3239204 16.3814251,17.8408763 15.8370887,17.9342373 C15.620331,17.9714142 15.397363,17.9360995 15.2027025,17.8337604 L12,16.1500001 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -6,16 +6,7 @@ import type { Props } from './Icon';
|
||||
export default function StrikethroughIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z" />
|
||||
</svg>
|
||||
<path d="M9.26756439,10 C9.09739429,9.70582663 9,9.36428714 9,9 C9,7.8954305 9.8954305,7 11,7 L16,7 C16.5522847,7 17,6.55228475 17,6 C17,5.44771525 16.5522847,5 16,5 L11,5 C8.790861,5 7,6.790861 7,9 C7,9.34529957 7.043753,9.68038008 7.12601749,10 L9.26756439,10 Z M16.8739825,14 C16.956247,14.3196199 17,14.6547004 17,15 C17,17.209139 15.209139,19 13,19 L8,19 C7.44771525,19 7,18.5522847 7,18 C7,17.4477153 7.44771525,17 8,17 L13,17 C14.1045695,17 15,16.1045695 15,15 C15,14.6357129 14.9026057,14.2941734 14.7324356,14 L16.8739825,14 Z M5.5,11.5 L18.5,11.5 C18.7761424,11.5 19,11.7238576 19,12 C19,12.2761424 18.7761424,12.5 18.5,12.5 L5.5,12.5 C5.22385763,12.5 5,12.2761424 5,12 C5,11.7238576 5.22385763,11.5 5.5,11.5 L5.5,11.5 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
15
frontend/components/Icon/TableIcon.js
Normal file
15
frontend/components/Icon/TableIcon.js
Normal file
@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function TableIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M6,5 L18,5 C19.1045695,5 20,5.8954305 20,7 L20,17 C20,18.1045695 19.1045695,19 18,19 L6,19 C4.8954305,19 4,18.1045695 4,17 L4,7 C4,5.8954305 4.8954305,5 6,5 Z M6,7 L6,9 L11,9 L11,7 L6,7 Z M13,7 L13,9 L18,9 L18,7 L13,7 Z M6,11 L6,13 L11,13 L11,11 L6,11 Z M13,11 L13,13 L18,13 L18,11 L13,11 Z M6,15 L6,17 L11,17 L11,15 L6,15 Z M13,15 L13,17 L18,17 L18,15 L13,15 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
12
frontend/components/Icon/TodoListIcon.js
Normal file
12
frontend/components/Icon/TodoListIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function TodoListIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M9.99992841,5.99992841 L19.0000716,5.99992841 L19.0000716,5.99992841 C19.5523168,5.99992841 20,6.4476116 20,6.99985681 L20,6.99985681 C20,7.55210202 19.5523168,7.99978522 19.0000716,7.99978522 L9.99992841,7.99978522 L9.99992841,7.99978522 C9.4476832,7.99978522 9,7.55210202 9,6.99985681 C9,6.4476116 9.4476832,5.99992841 9.99992841,5.99992841 L9.99992841,5.99992841 Z M9.99992841,15.9992125 L19.0000716,15.9992125 L19.0000716,15.9992125 C19.5523168,15.9992125 20,16.4468957 20,16.9991409 L20,16.9991409 L20,16.9991409 C20,17.5513861 19.5523168,17.9990693 19.0000716,17.9990693 L9.99992841,17.9990693 C9.4476832,17.9990693 9,17.5513861 9,16.9991409 C9,16.4468957 9.4476832,15.9992125 9.99992841,15.9992125 Z M9.99992841,10.9995704 L19.0000716,10.9995704 L19.0000716,10.9995704 C19.5523168,10.9995704 20,11.4472536 20,11.9994988 L20,11.9994988 C20,12.5517441 19.5523168,12.9994273 19.0000716,12.9994273 L9.99992841,12.9994273 C9.4476832,12.9994273 9,12.5517441 9,11.9994988 C9,11.4472536 9.4476832,10.9995704 9.99992841,10.9995704 Z M5.22935099,7.69420576 L7.09998441,5.20002786 C7.26566855,4.97911569 7.57906677,4.93434451 7.79997895,5.10002864 C8.02089112,5.26571278 8.0656623,5.579111 7.89997817,5.80002318 L5.64999574,8.79999974 C5.45636149,9.05817875 5.07249394,9.06801504 4.86589123,8.82009178 L3.61590099,7.3201035 C3.43912033,7.10796671 3.46778214,6.79268682 3.67991893,6.61590616 C3.89205572,6.4391255 4.20733561,6.46778731 4.38411627,6.6799241 L5.22935099,7.69420576 Z M5.22935099,12.6942058 L7.09998441,10.2000279 C7.26566855,9.97911569 7.57906677,9.93434451 7.79997895,10.1000286 C8.02089112,10.2657128 8.0656623,10.579111 7.89997817,10.8000232 L5.64999574,13.7999997 C5.45636149,14.0581787 5.07249394,14.068015 4.86589123,13.8200918 L3.61590099,12.3201035 C3.43912033,12.1079667 3.46778214,11.7926868 3.67991893,11.6159062 C3.89205572,11.4391255 4.20733561,11.4677873 4.38411627,11.6799241 L5.22935099,12.6942058 Z M5.22935099,17.6942058 L7.09998441,15.2000279 C7.26566855,14.9791157 7.57906677,14.9343445 7.79997895,15.1000286 C8.02089112,15.2657128 8.0656623,15.579111 7.89997817,15.8000232 L5.64999574,18.7999997 C5.45636149,19.0581787 5.07249394,19.068015 4.86589123,18.8200918 L3.61590099,17.3201035 C3.43912033,17.1079667 3.46778214,16.7926868 3.67991893,16.6159062 C3.89205572,16.4391255 4.20733561,16.4677873 4.38411627,16.6799241 L5.22935099,17.6942058 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
12
frontend/components/Icon/TrashIcon.js
Normal file
12
frontend/components/Icon/TrashIcon.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function TrashIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M10,6 L10,5 L10,5 C10,4.44771525 10.4477153,4 11,4 L13,4 C13.5522847,4 14,4.44771525 14,5 L14,6 L18,6 C18.5522847,6 19,6.44771525 19,7 C19,7.55228475 18.5522847,8 18,8 L17.8571429,8 L17.132679,18.1424941 C17.0579211,19.1891049 16.1870389,20 15.1377616,20 L8.86223841,20 C7.81296107,20 6.94207892,19.1891049 6.86732101,18.1424941 L6.14285714,8 L6,8 C5.44771525,8 5,7.55228475 5,7 C5,6.44771525 5.44771525,6 6,6 L6,6 L10,6 Z M8.86223841,18 L15.1377616,18 L15.8520473,8 L8.14795269,8 L8.86223841,18 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function UnderlinedIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z" />
|
||||
</svg>
|
||||
</Icon>
|
||||
);
|
||||
}
|
@ -12,7 +12,9 @@ import { documentEditUrl, homeUrl, searchUrl } from 'utils/routeHelpers';
|
||||
import Avatar from 'components/Avatar';
|
||||
import { LoadingIndicatorBar } from 'components/LoadingIndicator';
|
||||
import Scrollable from 'components/Scrollable';
|
||||
import Icon from 'components/Icon';
|
||||
import HomeIcon from 'components/Icon/HomeIcon';
|
||||
import SearchIcon from 'components/Icon/SearchIcon';
|
||||
import StarredIcon from 'components/Icon/StarredIcon';
|
||||
import Toasts from 'components/Toasts';
|
||||
import AccountMenu from 'menus/AccountMenu';
|
||||
|
||||
@ -35,16 +37,12 @@ type Props = {
|
||||
title?: ?React.Element<any>,
|
||||
auth: AuthStore,
|
||||
ui: UiStore,
|
||||
search: ?boolean,
|
||||
notifications?: React.Element<any>,
|
||||
};
|
||||
|
||||
@observer class Layout extends React.Component {
|
||||
props: Props;
|
||||
|
||||
static defaultProps = {
|
||||
search: true,
|
||||
};
|
||||
scrollable: ?HTMLDivElement;
|
||||
|
||||
@keydown(['/', 't'])
|
||||
goToSearch(ev) {
|
||||
@ -81,6 +79,20 @@ type Props = {
|
||||
this.props.ui.setActiveModal('collection-edit');
|
||||
};
|
||||
|
||||
setScrollableRef = ref => {
|
||||
this.scrollable = ref;
|
||||
};
|
||||
|
||||
scrollToActiveDocument = ref => {
|
||||
const scrollable = this.scrollable;
|
||||
if (!ref || !scrollable) return;
|
||||
|
||||
const container = scrollable.getBoundingClientRect();
|
||||
const bounds = ref.getBoundingClientRect();
|
||||
const scrollTop = bounds.top + container.top;
|
||||
scrollable.scrollTop = scrollTop;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { auth, documents, ui } = this.props;
|
||||
const { user, team } = auth;
|
||||
@ -115,16 +127,16 @@ type Props = {
|
||||
/>
|
||||
|
||||
<Flex auto column>
|
||||
<Scrollable>
|
||||
<Scrollable innerRef={this.setScrollableRef}>
|
||||
<LinkSection>
|
||||
<SidebarLink to="/dashboard">
|
||||
<Icon type="Home" /> Home
|
||||
<SidebarLink to="/dashboard" icon={<HomeIcon />}>
|
||||
Home
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/search">
|
||||
<Icon type="Search" /> Search
|
||||
<SidebarLink to="/search" icon={<SearchIcon />}>
|
||||
Search
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/starred">
|
||||
<Icon type="Star" /> Starred
|
||||
<SidebarLink to="/starred" icon={<StarredIcon />}>
|
||||
Starred
|
||||
</SidebarLink>
|
||||
</LinkSection>
|
||||
<LinkSection>
|
||||
@ -132,6 +144,7 @@ type Props = {
|
||||
history={this.props.history}
|
||||
activeDocument={documents.active}
|
||||
onCreateCollection={this.handleCreateCollection}
|
||||
activeDocumentRef={this.scrollToActiveDocument}
|
||||
/>
|
||||
</LinkSection>
|
||||
</Scrollable>
|
||||
|
@ -8,19 +8,23 @@ import { color, fontWeight } from 'styles/constants';
|
||||
|
||||
import SidebarLink from './SidebarLink';
|
||||
import DropToImport from 'components/DropToImport';
|
||||
import Icon from 'components/Icon';
|
||||
import PlusIcon from 'components/Icon/PlusIcon';
|
||||
import CollectionIcon from 'components/Icon/CollectionIcon';
|
||||
import CollectionMenu from 'menus/CollectionMenu';
|
||||
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import Document from 'models/Document';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import { type NavigationNode } from 'types';
|
||||
|
||||
type Props = {
|
||||
history: Object,
|
||||
collections: CollectionsStore,
|
||||
documents: DocumentsStore,
|
||||
activeDocument: ?Document,
|
||||
onCreateCollection: Function,
|
||||
onCreateCollection: () => void,
|
||||
activeDocumentRef: HTMLElement => void,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
@ -28,7 +32,14 @@ type Props = {
|
||||
props: Props;
|
||||
|
||||
render() {
|
||||
const { history, collections, activeDocument, ui } = this.props;
|
||||
const {
|
||||
history,
|
||||
collections,
|
||||
activeDocument,
|
||||
ui,
|
||||
activeDocumentRef,
|
||||
documents,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
@ -39,13 +50,18 @@ type Props = {
|
||||
history={history}
|
||||
collection={collection}
|
||||
activeDocument={activeDocument}
|
||||
activeDocumentRef={activeDocumentRef}
|
||||
prefetchDocument={documents.prefetchDocument}
|
||||
ui={ui}
|
||||
/>
|
||||
))}
|
||||
|
||||
{collections.isLoaded &&
|
||||
<SidebarLink onClick={this.props.onCreateCollection}>
|
||||
<Icon type="Plus" /> Add new collection
|
||||
<SidebarLink
|
||||
onClick={this.props.onCreateCollection}
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
New collection…
|
||||
</SidebarLink>}
|
||||
</Flex>
|
||||
);
|
||||
@ -62,7 +78,15 @@ type Props = {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { history, collection, activeDocument, ui } = this.props;
|
||||
const {
|
||||
history,
|
||||
collection,
|
||||
activeDocument,
|
||||
ui,
|
||||
activeDocumentRef,
|
||||
prefetchDocument,
|
||||
} = this.props;
|
||||
const expanded = collection.id === ui.activeCollectionId;
|
||||
|
||||
return (
|
||||
<StyledDropToImport
|
||||
@ -73,7 +97,11 @@ type Props = {
|
||||
menuOpen={this.menuOpen}
|
||||
dropzoneRef={ref => (this.dropzoneRef = ref)}
|
||||
>
|
||||
<SidebarLink key={collection.id} to={collection.url}>
|
||||
<SidebarLink
|
||||
key={collection.id}
|
||||
to={collection.url}
|
||||
icon={<CollectionIcon expanded={expanded} />}
|
||||
>
|
||||
<Flex justify="space-between">
|
||||
{collection.name}
|
||||
|
||||
@ -81,7 +109,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}
|
||||
@ -89,14 +117,16 @@ type Props = {
|
||||
</CollectionAction>
|
||||
</Flex>
|
||||
|
||||
{collection.id === ui.activeCollectionId &&
|
||||
{expanded &&
|
||||
<Children column>
|
||||
{collection.documents.map(document => (
|
||||
<DocumentLink
|
||||
key={document.id}
|
||||
activeDocumentRef={activeDocumentRef}
|
||||
history={history}
|
||||
document={document}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
depth={0}
|
||||
/>
|
||||
))}
|
||||
@ -111,53 +141,76 @@ type DocumentLinkProps = {
|
||||
document: NavigationNode,
|
||||
history: Object,
|
||||
activeDocument: ?Document,
|
||||
activeDocumentRef: HTMLElement => void,
|
||||
prefetchDocument: (documentId: string) => void,
|
||||
depth: number,
|
||||
};
|
||||
|
||||
const DocumentLink = observer((props: DocumentLinkProps) => {
|
||||
const { document, activeDocument, depth } = props;
|
||||
const DocumentLink = observer(
|
||||
({
|
||||
document,
|
||||
activeDocument,
|
||||
activeDocumentRef,
|
||||
prefetchDocument,
|
||||
depth,
|
||||
}: DocumentLinkProps) => {
|
||||
const isActiveDocument =
|
||||
activeDocument && activeDocument.id === document.id;
|
||||
const showChildren = !!(activeDocument &&
|
||||
(activeDocument.pathToDocument
|
||||
.map(entry => entry.id)
|
||||
.includes(document.id) ||
|
||||
isActiveDocument));
|
||||
|
||||
const showChildren =
|
||||
activeDocument &&
|
||||
(activeDocument.pathToDocument
|
||||
.map(entry => entry.id)
|
||||
.includes(document.id) ||
|
||||
activeDocument.id === document.id);
|
||||
const handleMouseEnter = (event: SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
prefetchDocument(document.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex column key={document.id}>
|
||||
<DropToImport
|
||||
history={history}
|
||||
documentId={document.id}
|
||||
activeStyle="activeDropZone"
|
||||
return (
|
||||
<Flex
|
||||
column
|
||||
key={document.id}
|
||||
innerRef={isActiveDocument ? activeDocumentRef : undefined}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
<SidebarLink
|
||||
to={document.url}
|
||||
hasChildren={document.children.length > 0}
|
||||
expanded={showChildren}
|
||||
<DropToImport
|
||||
history={history}
|
||||
documentId={document.id}
|
||||
activeStyle="activeDropZone"
|
||||
>
|
||||
{document.title}
|
||||
</SidebarLink>
|
||||
</DropToImport>
|
||||
<SidebarLink
|
||||
to={document.url}
|
||||
hasChildren={document.children.length > 0}
|
||||
expanded={showChildren}
|
||||
>
|
||||
{document.title}
|
||||
</SidebarLink>
|
||||
</DropToImport>
|
||||
|
||||
{showChildren &&
|
||||
<Children column>
|
||||
{document.children &&
|
||||
document.children.map(childDocument => (
|
||||
<DocumentLink
|
||||
key={childDocument.id}
|
||||
history={history}
|
||||
document={childDocument}
|
||||
activeDocument={activeDocument}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</Children>}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
{showChildren &&
|
||||
<Children column>
|
||||
{document.children &&
|
||||
document.children.map(childDocument => (
|
||||
<DocumentLink
|
||||
key={childDocument.id}
|
||||
history={history}
|
||||
document={childDocument}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</Children>}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const CollectionAction = styled.a`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
color: ${color.slate};
|
||||
svg { opacity: .75; }
|
||||
|
||||
@ -179,15 +232,16 @@ const StyledDropToImport = styled(DropToImport)`
|
||||
`;
|
||||
|
||||
const Header = styled(Flex)`
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: ${fontWeight.semiBold};
|
||||
text-transform: uppercase;
|
||||
color: ${color.slate};
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const Children = styled(Flex)`
|
||||
margin-left: 20px;
|
||||
`;
|
||||
|
||||
export default inject('collections', 'ui')(SidebarCollections);
|
||||
export default inject('collections', 'ui', 'documents')(SidebarCollections);
|
||||
|
@ -3,9 +3,8 @@ import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { color, fontWeight } from 'styles/constants';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Flex from 'components/Flex';
|
||||
import ChevronIcon from 'components/Icon/ChevronIcon';
|
||||
import CollapsedIcon from 'components/Icon/CollapsedIcon';
|
||||
|
||||
const activeStyle = {
|
||||
color: color.black,
|
||||
@ -15,12 +14,25 @@ const activeStyle = {
|
||||
// This is a hack for `styleComponent()` as NavLink fails to render without `to` prop
|
||||
const StyleableDiv = props => <div {...props} />;
|
||||
|
||||
const StyledGoTo = styled(CollapsedIcon)`
|
||||
margin-bottom: -4px;
|
||||
margin-right: 0;
|
||||
${({ expanded }) => !expanded && 'transform: rotate(-90deg);'}
|
||||
`;
|
||||
|
||||
const IconWrapper = styled.span`
|
||||
margin-left: -4px;
|
||||
margin-right: 4px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const styleComponent = component => styled(component)`
|
||||
display: block;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 5px 0;
|
||||
padding: 4px 0;
|
||||
margin-left: ${({ hasChildren }) => (hasChildren ? '-20px;' : '0')};
|
||||
color: ${color.slateDark};
|
||||
font-size: 15px;
|
||||
@ -30,36 +42,38 @@ const styleComponent = component => styled(component)`
|
||||
color: ${color.text};
|
||||
}
|
||||
|
||||
&.active ${StyledChevron} svg {
|
||||
fill: ${activeStyle.color};
|
||||
&.active {
|
||||
svg {
|
||||
fill: ${activeStyle.color}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function SidebarLink(props: Object) {
|
||||
const Component = styleComponent(props.to ? NavLink : StyleableDiv);
|
||||
type Props = {
|
||||
to?: string,
|
||||
onClick?: SyntheticEvent => *,
|
||||
children?: React$Element<*>,
|
||||
icon?: React$Element<*>,
|
||||
hasChildren?: boolean,
|
||||
expanded?: boolean,
|
||||
};
|
||||
|
||||
function SidebarLink({ icon, children, expanded, ...rest }: Props) {
|
||||
const Component = styleComponent(rest.to ? NavLink : StyleableDiv);
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<Component exact activeStyle={activeStyle} {...props}>
|
||||
{props.hasChildren && <StyledChevron expanded={props.expanded} />}
|
||||
{props.children}
|
||||
<Component exact activeStyle={activeStyle} {...rest}>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
{rest.hasChildren && <StyledGoTo expanded={expanded} />}
|
||||
<Content>{children}</Content>
|
||||
</Component>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledChevron = styled(ChevronIcon)`
|
||||
margin-right: -10px;
|
||||
|
||||
svg {
|
||||
height: 18px;
|
||||
margin-bottom: -4px;
|
||||
margin-right: 6px;
|
||||
|
||||
fill: ${color.slateDark};
|
||||
|
||||
${({ expanded }) => expanded && 'transform: rotate(90deg);'}
|
||||
}
|
||||
const Content = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default SidebarLink;
|
||||
|
@ -5,7 +5,7 @@ import styled from 'styled-components';
|
||||
import ReactModal from 'react-modal';
|
||||
import { color } from 'styles/constants';
|
||||
import { fadeAndScaleIn } from 'styles/animations';
|
||||
import Icon from 'components/Icon';
|
||||
import CloseIcon from 'components/Icon/CloseIcon';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
type Props = {
|
||||
@ -33,7 +33,7 @@ const Modal = ({
|
||||
>
|
||||
<Content column>
|
||||
{title && <h1>{title}</h1>}
|
||||
<Close onClick={onRequestClose}><Icon type="X" size={32} /></Close>
|
||||
<Close onClick={onRequestClose}><CloseIcon size={32} /></Close>
|
||||
{children}
|
||||
</Content>
|
||||
</StyledModal>
|
||||
|
@ -1,18 +1,11 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Scroll = styled.div`
|
||||
const Scrollable = styled.div`
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
`;
|
||||
|
||||
class Scrollable extends Component {
|
||||
render() {
|
||||
return <Scroll {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default Scrollable;
|
||||
|
@ -4,7 +4,6 @@ import styled from 'styled-components';
|
||||
import { darken } from 'polished';
|
||||
import { color } from 'styles/constants';
|
||||
import { fadeAndScaleIn } from 'styles/animations';
|
||||
import Icon from 'components/Icon';
|
||||
|
||||
type Props = {
|
||||
onRequestClose: () => void,
|
||||
@ -38,9 +37,6 @@ class Toast extends Component {
|
||||
|
||||
return (
|
||||
<Container onClick={onRequestClose} type={type}>
|
||||
{type === 'info'
|
||||
? <Icon type="Info" light />
|
||||
: <Icon type="AlertCircle" light />}
|
||||
<Message>{message}</Message>
|
||||
</Container>
|
||||
);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
52
frontend/menus/BlockMenu.js
Normal file
52
frontend/menus/BlockMenu.js
Normal file
@ -0,0 +1,52 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import ImageIcon from 'components/Icon/ImageIcon';
|
||||
import BulletedListIcon from 'components/Icon/BulletedListIcon';
|
||||
import HorizontalRuleIcon from 'components/Icon/HorizontalRuleIcon';
|
||||
import TodoListIcon from 'components/Icon/TodoListIcon';
|
||||
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}>
|
||||
<ImageIcon /> <span>Add images</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onInsertList}>
|
||||
<BulletedListIcon /> Start list
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onInsertTodoList}>
|
||||
<TodoListIcon /> Start checklist
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onInsertBreak}>
|
||||
<HorizontalRuleIcon /> Add break
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BlockMenu;
|
@ -1,18 +1,17 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Collection from 'models/Collection';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import Icon from 'components/Icon';
|
||||
import MoreIcon from 'components/Icon/MoreIcon';
|
||||
import Flex from 'components/Flex';
|
||||
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
|
||||
@observer class CollectionMenu extends Component {
|
||||
props: {
|
||||
label?: React$Element<any>,
|
||||
onShow?: () => void,
|
||||
label?: React$Element<*>,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
onImport?: () => void,
|
||||
history: Object,
|
||||
@ -36,13 +35,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}
|
||||
label={label || <MoreIcon />}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
{collection &&
|
||||
@ -53,17 +52,13 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
<DropdownMenuItem onClick={onImport}>
|
||||
Import document
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={this.onEdit}>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={this.onEdit}>Edit…</DropdownMenuItem>
|
||||
</Flex>}
|
||||
{allowDelete &&
|
||||
<DropdownMenuItem onClick={this.onDelete}>Delete</DropdownMenuItem>}
|
||||
<DropdownMenuItem onClick={this.onDelete}>Delete…</DropdownMenuItem>}
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MoreIcon = styled(Icon)`
|
||||
width: 22px;
|
||||
`;
|
||||
|
||||
export default inject('ui')(CollectionMenu);
|
||||
|
@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import Document from 'models/Document';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import Icon from 'components/Icon';
|
||||
import MoreIcon from 'components/Icon/MoreIcon';
|
||||
import { documentMoveUrl } from 'utils/routeHelpers';
|
||||
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
|
||||
@ -49,7 +49,7 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
const { allowDelete } = document;
|
||||
|
||||
return (
|
||||
<DropdownMenu label={label || <Icon type="MoreHorizontal" />}>
|
||||
<DropdownMenu label={label || <MoreIcon />}>
|
||||
{document.starred
|
||||
? <DropdownMenuItem onClick={this.handleUnstar}>
|
||||
Unstar
|
||||
@ -61,11 +61,13 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
>
|
||||
New child
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={this.handleMove}>Move</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={this.handleExport}>Export</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={this.handleExport}>
|
||||
Download
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={this.handleMove}>Move…</DropdownMenuItem>
|
||||
{allowDelete &&
|
||||
<DropdownMenuItem onClick={this.handleDelete}>
|
||||
Delete
|
||||
Delete…
|
||||
</DropdownMenuItem>}
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
@ -26,9 +26,9 @@ type Props = {
|
||||
documents: DocumentsStore,
|
||||
};
|
||||
|
||||
@observer class Dashboard extends React.Component {
|
||||
@observer class Dashboard extends Component {
|
||||
props: Props;
|
||||
@observable isLoaded = false;
|
||||
@observable isLoaded: boolean = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.loadContent();
|
||||
@ -43,25 +43,27 @@ type Props = {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { documents } = this.props;
|
||||
const recentlyViewedLoaded = documents.recentlyViewed.length > 0;
|
||||
const recentlyEditedLoaded = documents.recentlyEdited.length > 0;
|
||||
const showContent =
|
||||
this.isLoaded || (recentlyViewedLoaded && recentlyEditedLoaded);
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Home" />
|
||||
<h1>Home</h1>
|
||||
{this.isLoaded
|
||||
{showContent
|
||||
? <Flex column>
|
||||
{this.props.documents.recentlyViewed.length > 0 &&
|
||||
{recentlyViewedLoaded &&
|
||||
<Flex column>
|
||||
<Subheading>Recently viewed</Subheading>
|
||||
<DocumentList
|
||||
documents={this.props.documents.recentlyViewed}
|
||||
/>
|
||||
<DocumentList documents={documents.recentlyViewed} />
|
||||
</Flex>}
|
||||
{this.props.documents.recentlyEdited.length > 0 &&
|
||||
{recentlyEditedLoaded &&
|
||||
<Flex column>
|
||||
<Subheading>Recently edited</Subheading>
|
||||
<DocumentList
|
||||
documents={this.props.documents.recentlyEdited}
|
||||
/>
|
||||
<DocumentList documents={documents.recentlyEdited} />
|
||||
</Flex>}
|
||||
</Flex>
|
||||
: <ListPlaceholder count={5} />}
|
||||
|
@ -31,6 +31,7 @@ import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import Collaborators from 'components/Collaborators';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import NewDocumentIcon from 'components/Icon/NewDocumentIcon';
|
||||
import Search from 'scenes/Search';
|
||||
|
||||
const DISCARD_CHANGES = `
|
||||
@ -57,7 +58,6 @@ type Props = {
|
||||
@observable isDragging = false;
|
||||
@observable isLoading = false;
|
||||
@observable isSaving = false;
|
||||
@observable showAsSaved = false;
|
||||
@observable notFound = false;
|
||||
@observable moveModalOpen: boolean = false;
|
||||
|
||||
@ -169,17 +169,9 @@ type Props = {
|
||||
|
||||
if (redirect || this.props.newDocument) {
|
||||
this.props.history.push(document.url);
|
||||
} else {
|
||||
this.toggleShowAsSaved();
|
||||
}
|
||||
};
|
||||
|
||||
toggleShowAsSaved() {
|
||||
this.showAsSaved = true;
|
||||
this.isSaving = false;
|
||||
this.savedTimeout = setTimeout(() => (this.showAsSaved = false), 2000);
|
||||
}
|
||||
|
||||
onImageUploadStart = () => {
|
||||
this.isLoading = true;
|
||||
};
|
||||
@ -193,7 +185,7 @@ type Props = {
|
||||
this.document.updateData({ text }, true);
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
onDiscard = () => {
|
||||
let url;
|
||||
if (this.document && this.document.url) {
|
||||
url = this.document.url;
|
||||
@ -264,7 +256,7 @@ type Props = {
|
||||
onImageUploadStop={this.onImageUploadStop}
|
||||
onChange={this.onChange}
|
||||
onSave={this.onSave}
|
||||
onCancel={this.onCancel}
|
||||
onCancel={this.onDiscard}
|
||||
readOnly={!this.isEditing}
|
||||
/>
|
||||
<Meta
|
||||
@ -293,7 +285,7 @@ type Props = {
|
||||
</HeaderAction>
|
||||
{this.isEditing &&
|
||||
<HeaderAction>
|
||||
<a onClick={this.onCancel}>Cancel</a>
|
||||
<a onClick={this.onDiscard}>Discard</a>
|
||||
</HeaderAction>}
|
||||
{!this.isEditing &&
|
||||
<HeaderAction>
|
||||
@ -303,7 +295,7 @@ type Props = {
|
||||
<HeaderAction>
|
||||
{!this.isEditing &&
|
||||
<a onClick={this.onClickNew}>
|
||||
New
|
||||
<NewDocumentIcon />
|
||||
</a>}
|
||||
</HeaderAction>
|
||||
</Flex>
|
||||
@ -325,19 +317,11 @@ const Separator = styled.div`
|
||||
const HeaderAction = styled(Flex)`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 43px;
|
||||
color: ${color.text};
|
||||
padding: 0 0 0 14px;
|
||||
padding: 0 0 0 10px;
|
||||
|
||||
a,
|
||||
svg {
|
||||
a {
|
||||
color: ${color.text};
|
||||
opacity: .8;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -7,7 +7,7 @@ import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
import Flex from 'components/Flex';
|
||||
import ChevronIcon from 'components/Icon/ChevronIcon';
|
||||
import GoToIcon from 'components/Icon/GoToIcon';
|
||||
|
||||
import Document from 'models/Document';
|
||||
|
||||
@ -19,10 +19,7 @@ const ResultWrapper = styled.div`
|
||||
cursor: default;
|
||||
`;
|
||||
|
||||
const StyledChevronIcon = styled(ChevronIcon)`
|
||||
padding-top: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
const StyledGoToIcon = styled(GoToIcon)`
|
||||
`;
|
||||
|
||||
const ResultWrapperLink = ResultWrapper.withComponent('a').extend`
|
||||
@ -40,8 +37,8 @@ const ResultWrapperLink = ResultWrapper.withComponent('a').extend`
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
${StyledChevronIcon} svg {
|
||||
fill: ${color.smokeLight};
|
||||
${StyledGoToIcon} {
|
||||
fill: ${color.white};
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -82,14 +79,14 @@ type Props = {
|
||||
if (!result) return <div />;
|
||||
|
||||
return (
|
||||
<Component innerRef={ref} selectable href onClick={this.handleClick}>
|
||||
<Component innerRef={ref} onClick={this.handleClick} selectable href>
|
||||
{result.path
|
||||
.map(doc => <span key={doc.id}>{doc.title}</span>)
|
||||
.reduce((prev, curr) => [prev, <StyledChevronIcon />, curr])}
|
||||
.reduce((prev, curr) => [prev, <StyledGoToIcon />, curr])}
|
||||
{document &&
|
||||
<Flex>
|
||||
{' '}
|
||||
<ChevronIcon />
|
||||
<StyledGoToIcon />
|
||||
{' '}{document.title}
|
||||
</Flex>}
|
||||
</Component>
|
||||
|
@ -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;
|
||||
|
@ -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>We’re unable to find the page you’re 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}
|
||||
|
@ -1,26 +1,12 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'components/Icon';
|
||||
import SearchIcon from 'components/Icon/SearchIcon';
|
||||
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,24 +19,25 @@ 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}
|
||||
placeholder="Search…"
|
||||
spellCheck="false"
|
||||
placeholder="search…"
|
||||
autoFocus
|
||||
/>
|
||||
</Flex>
|
||||
@ -58,4 +45,23 @@ 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(SearchIcon)`
|
||||
position: relative;
|
||||
top: 4px;
|
||||
`;
|
||||
|
||||
export default SearchField;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -111,8 +111,12 @@ class DocumentsStore extends BaseStore {
|
||||
return data.map(documentData => documentData.id);
|
||||
};
|
||||
|
||||
@action fetch = async (id: string): Promise<*> => {
|
||||
this.isFetching = true;
|
||||
@action prefetchDocument = async (id: string) => {
|
||||
if (!this.getById(id)) this.fetch(id, true);
|
||||
};
|
||||
|
||||
@action fetch = async (id: string, prefetch?: boolean): Promise<*> => {
|
||||
if (!prefetch) this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post('/documents.info', { id });
|
||||
|
@ -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;
|
||||
|
@ -50,7 +50,7 @@ svg {
|
||||
max-height: 100%;
|
||||
}
|
||||
a {
|
||||
color: #005aa6;
|
||||
color: #16B3FF;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -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;
|
@ -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 };
|
@ -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);
|
||||
};
|
@ -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, '');
|
||||
};
|
@ -61,6 +61,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tommoor/slate-drop-or-paste-images": "0.5.1",
|
||||
"aws-sdk": "^2.135.0",
|
||||
"babel-core": "^6.24.1",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"babel-loader": "6.2.5",
|
||||
@ -85,7 +86,6 @@
|
||||
"css-loader": "^0.28.7",
|
||||
"debug": "2.2.0",
|
||||
"dotenv": "^4.0.0",
|
||||
"emoji-name-map": "1.1.2",
|
||||
"emoji-regex": "^6.5.1",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-react-app": "^0.6.2",
|
||||
@ -100,7 +100,6 @@
|
||||
"fbemitter": "^2.1.1",
|
||||
"file-loader": "0.9.0",
|
||||
"flow-typed": "^2.1.2",
|
||||
"highlight.js": "9.4.0",
|
||||
"history": "3.0.0",
|
||||
"html-webpack-plugin": "2.17.0",
|
||||
"http-errors": "1.4.0",
|
||||
@ -109,7 +108,6 @@
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"js-cookie": "^2.1.4",
|
||||
"js-search": "^1.4.2",
|
||||
"js-tree": "1.1.0",
|
||||
"json-loader": "0.5.4",
|
||||
"jsonwebtoken": "7.0.1",
|
||||
"koa": "^2.2.0",
|
||||
@ -126,8 +124,6 @@
|
||||
"localforage": "^1.5.0",
|
||||
"lodash": "^4.17.4",
|
||||
"lodash.orderby": "4.4.0",
|
||||
"marked": "0.3.6",
|
||||
"marked-sanitized": "^0.1.1",
|
||||
"mobx": "^3.1.9",
|
||||
"mobx-react": "^4.1.8",
|
||||
"mobx-react-devtools": "^4.2.11",
|
||||
@ -146,7 +142,6 @@
|
||||
"react-addons-css-transition-group": "15.3.2",
|
||||
"react-dom": "^15.6.1",
|
||||
"react-dropzone": "3.6.0",
|
||||
"react-feather": "^1.0.7",
|
||||
"react-helmet": "3.1.0",
|
||||
"react-keydown": "^1.7.3",
|
||||
"react-modal": "^2.2.1",
|
||||
@ -171,7 +166,6 @@
|
||||
"string-hash": "^1.1.0",
|
||||
"style-loader": "^0.18.2",
|
||||
"styled-components": "^2.0.0",
|
||||
"truncate-html": "https://github.com/jorilallo/truncate-html/tarball/master",
|
||||
"url-loader": "0.5.7",
|
||||
"uuid": "2.0.2",
|
||||
"validator": "5.2.0",
|
||||
|
@ -1,8 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var mapping = require('emoji-name-map');
|
||||
|
||||
fs.writeFile(
|
||||
path.join(__dirname, '../frontend/utils/emoji-mapping.json'),
|
||||
JSON.stringify(mapping.emoji)
|
||||
);
|
@ -55,6 +55,10 @@ router.post('auth.slack', async ctx => {
|
||||
expires: new Date('2100'),
|
||||
});
|
||||
|
||||
// Update user's avatar
|
||||
await user.updateAvatar();
|
||||
await user.save();
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
user: await presentUser(ctx, user),
|
||||
|
@ -4,10 +4,16 @@ import httpErrors from 'http-errors';
|
||||
|
||||
import auth from './middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentDocument } from '../presenters';
|
||||
import { Document, Collection, Star, View } from '../models';
|
||||
import { presentDocument, presentRevision } from '../presenters';
|
||||
import { Document, Collection, Star, View, Revision } from '../models';
|
||||
|
||||
const authDocumentForUser = (ctx, document) => {
|
||||
const user = ctx.state.user;
|
||||
if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound();
|
||||
};
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post('documents.list', auth(), pagination(), async ctx => {
|
||||
let { sort = 'updatedAt', direction } = ctx.body;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
@ -101,23 +107,38 @@ router.post('documents.info', auth(), async ctx => {
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
const document = await Document.findById(id);
|
||||
|
||||
if (!document) throw httpErrors.NotFound();
|
||||
|
||||
// Don't expose private documents outside the team
|
||||
if (document.private) {
|
||||
if (!ctx.state.user) throw httpErrors.NotFound();
|
||||
|
||||
const user = await ctx.state.user;
|
||||
if (document.teamId !== user.teamId) {
|
||||
throw httpErrors.NotFound();
|
||||
}
|
||||
}
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
ctx.body = {
|
||||
data: await presentDocument(ctx, document),
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.revisions', auth(), pagination(), async ctx => {
|
||||
let { id, sort = 'updatedAt', direction } = ctx.body;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
const document = await Document.findById(id);
|
||||
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
const revisions = await Revision.findAll({
|
||||
where: { documentId: id },
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
const data = await Promise.all(
|
||||
revisions.map(revision => presentRevision(ctx, revision))
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.search', auth(), async ctx => {
|
||||
const { query } = ctx.body;
|
||||
ctx.assertPresent(query, 'query is required');
|
||||
@ -142,8 +163,7 @@ router.post('documents.star', auth(), async ctx => {
|
||||
const user = await ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
|
||||
if (!document || document.teamId !== user.teamId)
|
||||
throw httpErrors.BadRequest();
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
await Star.findOrCreate({
|
||||
where: { documentId: document.id, userId: user.id },
|
||||
@ -156,8 +176,7 @@ router.post('documents.unstar', auth(), async ctx => {
|
||||
const user = await ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
|
||||
if (!document || document.teamId !== user.teamId)
|
||||
throw httpErrors.BadRequest();
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
await Star.destroy({
|
||||
where: { documentId: document.id, userId: user.id },
|
||||
@ -228,7 +247,7 @@ router.post('documents.update', auth(), async ctx => {
|
||||
const document = await Document.findById(id);
|
||||
const collection = document.collection;
|
||||
|
||||
if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound();
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
// Update document
|
||||
if (title) document.title = title;
|
||||
@ -254,15 +273,14 @@ router.post('documents.move', auth(), async ctx => {
|
||||
ctx.assertUuid(parentDocument, 'parentDocument must be an uuid');
|
||||
if (index) ctx.assertPositiveInteger(index, 'index must be an integer (>=0)');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
const collection = await Collection.findById(document.atlasId);
|
||||
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
if (collection.type !== 'atlas')
|
||||
throw httpErrors.BadRequest("This document can't be moved");
|
||||
|
||||
if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound();
|
||||
|
||||
// Set parent document
|
||||
if (parentDocument) {
|
||||
const parent = await Document.findById(parentDocument);
|
||||
@ -292,12 +310,10 @@ router.post('documents.delete', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
const collection = await Collection.findById(document.atlasId);
|
||||
|
||||
if (!document || document.teamId !== user.teamId)
|
||||
throw httpErrors.BadRequest();
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
if (collection.type === 'atlas') {
|
||||
// Don't allow deletion of root docs
|
||||
|
@ -43,6 +43,24 @@ describe('#documents.list', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#documents.revision', async () => {
|
||||
it("should return document's revisions", async () => {
|
||||
const { user, document } = await seed();
|
||||
const res = await server.post('/api/documents.revisions', {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).not.toEqual(document.id);
|
||||
expect(body.data[0].title).toEqual(document.title);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#documents.search', async () => {
|
||||
it('should return results', async () => {
|
||||
const { user } = await seed();
|
||||
|
@ -65,8 +65,8 @@ if (process.env.NODE_ENV === 'development') {
|
||||
app.use(logger());
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
bugsnag.register('ad7a85f99b1b9324a31e16732cdf3192');
|
||||
if (process.env.NODE_ENV === 'production' && process.env.BUGSNAG_KEY) {
|
||||
bugsnag.register(process.env.BUGSNAG_KEY);
|
||||
app.on('error', bugsnag.koaHandler);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
up: function(queryInterface, Sequelize) {
|
||||
queryInterface.removeColumn('collections', 'navigationTree');
|
||||
},
|
||||
|
||||
down: function(queryInterface, Sequelize) {
|
||||
queryInterface.addColumn('collections', 'navigationTree', {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
};
|
23
server/migrations/20171017055026-remove-document-html.js
Normal file
23
server/migrations/20171017055026-remove-document-html.js
Normal file
@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
up: function(queryInterface, Sequelize) {
|
||||
queryInterface.removeColumn('documents', 'html');
|
||||
queryInterface.removeColumn('documents', 'preview');
|
||||
queryInterface.removeColumn('revisions', 'html');
|
||||
queryInterface.removeColumn('revisions', 'preview');
|
||||
},
|
||||
|
||||
down: function(queryInterface, Sequelize) {
|
||||
queryInterface.addColumn('documents', 'html', {
|
||||
type: Sequelize.TEXT,
|
||||
});
|
||||
queryInterface.addColumn('documents', 'preview', {
|
||||
type: Sequelize.TEXT,
|
||||
});
|
||||
queryInterface.addColumn('revisions', 'html', {
|
||||
type: Sequelize.TEXT,
|
||||
});
|
||||
queryInterface.addColumn('revisions', 'preview', {
|
||||
type: Sequelize.TEXT,
|
||||
});
|
||||
},
|
||||
};
|
12
server/migrations/20171019071915-user-avatar-url.js
Normal file
12
server/migrations/20171019071915-user-avatar-url.js
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
up: function(queryInterface, Sequelize) {
|
||||
queryInterface.addColumn('users', 'avatarUrl', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: function(queryInterface, Sequelize) {
|
||||
queryInterface.removeColumn('users', 'avatarUrl');
|
||||
},
|
||||
};
|
@ -29,7 +29,6 @@ const Collection = sequelize.define(
|
||||
creatorId: DataTypes.UUID,
|
||||
|
||||
/* type: atlas */
|
||||
navigationTree: DataTypes.JSONB, // legacy
|
||||
documentStructure: DataTypes.JSONB,
|
||||
},
|
||||
{
|
||||
@ -98,28 +97,6 @@ Collection.prototype.getUrl = function() {
|
||||
return `/collections/${this.id}`;
|
||||
};
|
||||
|
||||
Collection.prototype.getDocumentsStructure = async function() {
|
||||
// Lazy fill this.documentStructure - TMP for internal release
|
||||
if (!this.documentStructure) {
|
||||
this.documentStructure = this.navigationTree.children;
|
||||
|
||||
// Remove parent references from all root documents
|
||||
await this.navigationTree.children.forEach(async ({ id }) => {
|
||||
const document = await Document.findById(id);
|
||||
document.parentDocumentId = null;
|
||||
await document.save();
|
||||
});
|
||||
|
||||
// Remove root document
|
||||
const rootDocument = await Document.findById(this.navigationTree.id);
|
||||
await rootDocument.destroy();
|
||||
|
||||
await this.save();
|
||||
}
|
||||
|
||||
return this.documentStructure;
|
||||
};
|
||||
|
||||
Collection.prototype.addDocumentToStructure = async function(
|
||||
document,
|
||||
index,
|
||||
|
@ -2,12 +2,9 @@
|
||||
import slug from 'slug';
|
||||
import _ from 'lodash';
|
||||
import randomstring from 'randomstring';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { convertToMarkdown } from '../../frontend/utils/markdown';
|
||||
import { truncateMarkdown } from '../utils/truncate';
|
||||
import parseTitle from '../../shared/parseTitle';
|
||||
import Revision from './Revision';
|
||||
|
||||
@ -25,8 +22,6 @@ const createRevision = doc => {
|
||||
return Revision.create({
|
||||
title: doc.title,
|
||||
text: doc.text,
|
||||
html: doc.html,
|
||||
preview: doc.preview,
|
||||
userId: doc.lastModifiedById,
|
||||
documentId: doc.id,
|
||||
});
|
||||
@ -40,8 +35,6 @@ const beforeSave = async doc => {
|
||||
const { emoji } = parseTitle(doc.text);
|
||||
|
||||
doc.emoji = emoji;
|
||||
doc.html = convertToMarkdown(doc.text);
|
||||
doc.preview = truncateMarkdown(doc.text, 160);
|
||||
doc.revisionCount += 1;
|
||||
|
||||
// Collaborators
|
||||
@ -74,8 +67,6 @@ const Document = sequelize.define(
|
||||
private: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||||
title: DataTypes.STRING,
|
||||
text: DataTypes.TEXT,
|
||||
html: DataTypes.TEXT,
|
||||
preview: DataTypes.TEXT,
|
||||
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||
parentDocumentId: DataTypes.UUID,
|
||||
createdById: {
|
||||
|
@ -9,8 +9,6 @@ const Revision = sequelize.define('revision', {
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
text: DataTypes.TEXT,
|
||||
html: DataTypes.TEXT,
|
||||
preview: DataTypes.TEXT,
|
||||
|
||||
userId: {
|
||||
type: 'UUID',
|
||||
|
@ -1,7 +1,9 @@
|
||||
// @flow
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import uuid from 'uuid';
|
||||
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
||||
import { uploadToS3FromUrl } from '../utils/s3';
|
||||
|
||||
import JWT from 'jsonwebtoken';
|
||||
|
||||
@ -18,6 +20,7 @@ const User = sequelize.define(
|
||||
email: { type: DataTypes.STRING },
|
||||
username: { type: DataTypes.STRING },
|
||||
name: DataTypes.STRING,
|
||||
avatarUrl: { type: DataTypes.STRING, allowNull: true },
|
||||
password: DataTypes.VIRTUAL,
|
||||
passwordDigest: DataTypes.STRING,
|
||||
isAdmin: DataTypes.BOOLEAN,
|
||||
@ -66,6 +69,12 @@ User.prototype.verifyPassword = function(password) {
|
||||
});
|
||||
});
|
||||
};
|
||||
User.prototype.updateAvatar = async function() {
|
||||
this.avatarUrl = await uploadToS3FromUrl(
|
||||
this.slackData.image_192,
|
||||
`avatars/${this.id}/${uuid.v4()}`
|
||||
);
|
||||
};
|
||||
|
||||
const setRandomJwtSecret = model => {
|
||||
model.jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
|
@ -19,7 +19,7 @@ async function present(ctx: Object, collection: Collection) {
|
||||
};
|
||||
|
||||
if (collection.type === 'atlas') {
|
||||
data.documents = await collection.getDocumentsStructure();
|
||||
data.documents = collection.documentStructure;
|
||||
}
|
||||
|
||||
if (collection.documents) {
|
||||
|
@ -21,8 +21,6 @@ async function present(ctx: Object, document: Document, options: ?Options) {
|
||||
private: document.private,
|
||||
title: document.title,
|
||||
text: document.text,
|
||||
html: document.html,
|
||||
preview: document.preview,
|
||||
emoji: document.emoji,
|
||||
createdAt: document.createdAt,
|
||||
createdBy: presentUser(ctx, document.createdBy),
|
||||
|
@ -2,6 +2,7 @@
|
||||
import presentUser from './user';
|
||||
import presentView from './view';
|
||||
import presentDocument from './document';
|
||||
import presentRevision from './revision';
|
||||
import presentCollection from './collection';
|
||||
import presentApiKey from './apiKey';
|
||||
import presentTeam from './team';
|
||||
@ -10,6 +11,7 @@ export {
|
||||
presentUser,
|
||||
presentView,
|
||||
presentDocument,
|
||||
presentRevision,
|
||||
presentCollection,
|
||||
presentApiKey,
|
||||
presentTeam,
|
||||
|
15
server/presenters/revision.js
Normal file
15
server/presenters/revision.js
Normal file
@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import _ from 'lodash';
|
||||
import { Revision } from '../models';
|
||||
|
||||
function present(ctx: Object, revision: Revision) {
|
||||
return {
|
||||
id: revision.id,
|
||||
title: revision.title,
|
||||
text: revision.text,
|
||||
createdAt: revision.createdAt,
|
||||
updatedAt: revision.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export default present;
|
@ -8,7 +8,8 @@ function present(ctx: Object, user: User) {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
avatarUrl: user.slackData ? user.slackData.image_192 : null,
|
||||
avatarUrl: user.avatarUrl ||
|
||||
(user.slackData ? user.slackData.image_192 : null),
|
||||
};
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user