Update master
This commit is contained in:
20
.eslintrc
20
.eslintrc
@ -6,10 +6,7 @@
|
|||||||
"plugin:import/warnings",
|
"plugin:import/warnings",
|
||||||
"plugin:flowtype/recommended"
|
"plugin:flowtype/recommended"
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": ["prettier", "flowtype"],
|
||||||
"prettier",
|
|
||||||
"flowtype"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"eqeqeq": 2,
|
"eqeqeq": 2,
|
||||||
"no-unused-vars": 2,
|
"no-unused-vars": 2,
|
||||||
@ -22,7 +19,7 @@
|
|||||||
"import/no-unresolved": [
|
"import/no-unresolved": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
"ignore": [ "slate-drop-or-paste-images" ]
|
"ignore": ["slate-drop-or-paste-images"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
// Flow
|
// Flow
|
||||||
@ -33,14 +30,8 @@
|
|||||||
"annotationStyle": "line"
|
"annotationStyle": "line"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"flowtype/space-after-type-colon": [
|
"flowtype/space-after-type-colon": [2, "always"],
|
||||||
2,
|
"flowtype/space-before-type-colon": [2, "never"],
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"flowtype/space-before-type-colon": [
|
|
||||||
2,
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
// Enforce that code is formatted with prettier.
|
// Enforce that code is formatted with prettier.
|
||||||
"prettier/prettier": [
|
"prettier/prettier": [
|
||||||
"error",
|
"error",
|
||||||
@ -65,7 +56,8 @@
|
|||||||
"SLACK_REDIRECT_URI": true,
|
"SLACK_REDIRECT_URI": true,
|
||||||
"DEPLOYMENT": true,
|
"DEPLOYMENT": true,
|
||||||
"BASE_URL": true,
|
"BASE_URL": true,
|
||||||
|
"BUGSNAG_KEY": true,
|
||||||
"afterAll": true,
|
"afterAll": true,
|
||||||
"Bugsnag": true
|
"Bugsnag": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
1. Install dependencies with `yarn`
|
1. Install dependencies with `yarn`
|
||||||
1. Register a Slack app at https://api.slack.com/apps
|
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. 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`
|
1. Start the development server `yarn start`
|
||||||
|
|
||||||
|
|
||||||
@ -16,12 +16,12 @@
|
|||||||
Sequelize is used to create and run migrations, for example:
|
Sequelize is used to create and run migrations, for example:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn run sequelize migration:create
|
yarn sequelize migration:create
|
||||||
yarn run sequelize db:migrate
|
yarn sequelize db:migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
Or to run migrations on test database:
|
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": {
|
"URL": {
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
"BUGSNAG_KEY": {
|
||||||
|
"required": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"formation": {},
|
"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_REDIRECT_URI: string;
|
||||||
declare var SLACK_KEY: string;
|
declare var SLACK_KEY: string;
|
||||||
declare var BASE_URL: string;
|
declare var BASE_URL: string;
|
||||||
|
declare var BUGSNAG_KEY: ?string;
|
||||||
declare var DEPLOYMENT: string;
|
declare var DEPLOYMENT: string;
|
||||||
declare var Bugsnag: any;
|
declare var Bugsnag: any;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import invariant from 'invariant';
|
import invariant from 'invariant';
|
||||||
import { observable } from 'mobx';
|
import { observable } from 'mobx';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
@ -10,14 +10,14 @@ import { color } from 'styles/constants';
|
|||||||
import { fadeAndScaleIn } from 'styles/animations';
|
import { fadeAndScaleIn } from 'styles/animations';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: React.Element<any>,
|
label: React.Element<*>,
|
||||||
onShow?: () => void,
|
onOpen?: () => void,
|
||||||
onClose?: () => void,
|
onClose?: () => void,
|
||||||
children?: React.Element<any>,
|
children?: React.Element<*>,
|
||||||
style?: Object,
|
style?: Object,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer class DropdownMenu extends React.Component {
|
@observer class DropdownMenu extends Component {
|
||||||
props: Props;
|
props: Props;
|
||||||
actionRef: Object;
|
actionRef: Object;
|
||||||
@observable open: boolean = false;
|
@observable open: boolean = false;
|
||||||
@ -37,7 +37,7 @@ type Props = {
|
|||||||
this.open = true;
|
this.open = true;
|
||||||
this.top = targetRect.bottom - bodyRect.top;
|
this.top = targetRect.bottom - bodyRect.top;
|
||||||
this.right = bodyRect.width - targetRect.left - targetRect.width;
|
this.right = bodyRect.width - targetRect.left - targetRect.width;
|
||||||
if (this.props.onShow) this.props.onShow();
|
if (this.props.onOpen) this.props.onOpen();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ const DropdownMenuItem = ({
|
|||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
onClick?: () => void,
|
onClick?: SyntheticEvent => void,
|
||||||
children?: React.Element<any>,
|
children?: React.Element<any>,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@ -24,11 +24,15 @@ const MenuItem = styled.div`
|
|||||||
|
|
||||||
color: ${color.slateDark};
|
color: ${color.slateDark};
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: left;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -9,6 +9,7 @@ import getDataTransferFiles from 'utils/getDataTransferFiles';
|
|||||||
import Flex from 'components/Flex';
|
import Flex from 'components/Flex';
|
||||||
import ClickablePadding from './components/ClickablePadding';
|
import ClickablePadding from './components/ClickablePadding';
|
||||||
import Toolbar from './components/Toolbar';
|
import Toolbar from './components/Toolbar';
|
||||||
|
import BlockInsert from './components/BlockInsert';
|
||||||
import Placeholder from './components/Placeholder';
|
import Placeholder from './components/Placeholder';
|
||||||
import Contents from './components/Contents';
|
import Contents from './components/Contents';
|
||||||
import Markdown from './serializer';
|
import Markdown from './serializer';
|
||||||
@ -24,7 +25,7 @@ type Props = {
|
|||||||
onCancel: Function,
|
onCancel: Function,
|
||||||
onImageUploadStart: Function,
|
onImageUploadStart: Function,
|
||||||
onImageUploadStop: Function,
|
onImageUploadStop: Function,
|
||||||
emoji: string,
|
emoji?: string,
|
||||||
readOnly: boolean,
|
readOnly: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,6 +173,8 @@ type KeyData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render = () => {
|
render = () => {
|
||||||
|
const { readOnly, emoji, onSave } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
onDrop={this.handleDrop}
|
onDrop={this.handleDrop}
|
||||||
@ -182,25 +185,32 @@ type KeyData = {
|
|||||||
auto
|
auto
|
||||||
>
|
>
|
||||||
<MaxWidth column auto>
|
<MaxWidth column auto>
|
||||||
<Header onClick={this.focusAtStart} readOnly={this.props.readOnly} />
|
<Header onClick={this.focusAtStart} readOnly={readOnly} />
|
||||||
<Toolbar state={this.editorState} onChange={this.onChange} />
|
|
||||||
<Contents state={this.editorState} />
|
<Contents state={this.editorState} />
|
||||||
|
{!readOnly &&
|
||||||
|
<Toolbar state={this.editorState} onChange={this.onChange} />}
|
||||||
|
{!readOnly &&
|
||||||
|
<BlockInsert
|
||||||
|
state={this.editorState}
|
||||||
|
onChange={this.onChange}
|
||||||
|
onInsertImage={this.insertImageFile}
|
||||||
|
/>}
|
||||||
<StyledEditor
|
<StyledEditor
|
||||||
innerRef={ref => (this.editor = ref)}
|
innerRef={ref => (this.editor = ref)}
|
||||||
placeholder="Start with a title…"
|
placeholder="Start with a title…"
|
||||||
bodyPlaceholder="…the rest is your canvas"
|
bodyPlaceholder="…the rest is your canvas"
|
||||||
schema={this.schema}
|
schema={this.schema}
|
||||||
plugins={this.plugins}
|
plugins={this.plugins}
|
||||||
emoji={this.props.emoji}
|
emoji={emoji}
|
||||||
state={this.editorState}
|
state={this.editorState}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onDocumentChange={this.onDocumentChange}
|
onDocumentChange={this.onDocumentChange}
|
||||||
onSave={this.props.onSave}
|
onSave={onSave}
|
||||||
readOnly={this.props.readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
<ClickablePadding
|
<ClickablePadding
|
||||||
onClick={!this.props.readOnly ? this.focusAtEnd : undefined}
|
onClick={!readOnly ? this.focusAtEnd : undefined}
|
||||||
grow
|
grow
|
||||||
/>
|
/>
|
||||||
</MaxWidth>
|
</MaxWidth>
|
||||||
@ -281,6 +291,10 @@ const StyledEditor = styled(Editor)`
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: ${({ readOnly }) => (readOnly ? 'underline' : 'none')};
|
||||||
|
}
|
||||||
|
|
||||||
li p {
|
li p {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -322,6 +336,10 @@ const StyledEditor = styled(Editor)`
|
|||||||
td {
|
td {
|
||||||
padding: 5px 20px 5px 0;
|
padding: 5px 20px 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b, strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default MarkdownEditor;
|
export default MarkdownEditor;
|
||||||
|
197
frontend/components/Editor/components/BlockInsert.js
Normal file
197
frontend/components/Editor/components/BlockInsert.js
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
// @flow
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import EditList from '../plugins/EditList';
|
||||||
|
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||||
|
import Portal from 'react-portal';
|
||||||
|
import { observable } from 'mobx';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { color } from 'styles/constants';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import BlockMenu from 'menus/BlockMenu';
|
||||||
|
import type { State } from '../types';
|
||||||
|
|
||||||
|
const { transforms } = EditList;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
state: State,
|
||||||
|
onChange: Function,
|
||||||
|
onInsertImage: File => Promise<*>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export default class BlockInsert extends Component {
|
||||||
|
props: Props;
|
||||||
|
mouseMoveTimeout: number;
|
||||||
|
file: HTMLInputElement;
|
||||||
|
|
||||||
|
@observable active: boolean = false;
|
||||||
|
@observable menuOpen: boolean = false;
|
||||||
|
@observable top: number;
|
||||||
|
@observable left: number;
|
||||||
|
@observable mouseX: number;
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this.update();
|
||||||
|
window.addEventListener('mousemove', this.handleMouseMove);
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillUpdate = (nextProps: Props) => {
|
||||||
|
this.update(nextProps);
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
|
};
|
||||||
|
|
||||||
|
setInactive = () => {
|
||||||
|
if (this.menuOpen) return;
|
||||||
|
this.active = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMouseMove = (ev: SyntheticMouseEvent) => {
|
||||||
|
const windowWidth = window.innerWidth / 3;
|
||||||
|
let active = ev.clientX < windowWidth;
|
||||||
|
|
||||||
|
if (active !== this.active) {
|
||||||
|
this.active = active || this.menuOpen;
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
clearTimeout(this.mouseMoveTimeout);
|
||||||
|
this.mouseMoveTimeout = setTimeout(this.setInactive, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMenuOpen = () => {
|
||||||
|
this.menuOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMenuClose = () => {
|
||||||
|
this.menuOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
update = (props?: Props) => {
|
||||||
|
if (!document.activeElement) return;
|
||||||
|
const { state } = props || this.props;
|
||||||
|
const boxRect = document.activeElement.getBoundingClientRect();
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection.focusNode) return;
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const rect = range.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (rect.top <= 0 || boxRect.left <= 0) return;
|
||||||
|
|
||||||
|
if (state.startBlock.type === 'heading1') {
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.top = Math.round(rect.top + window.scrollY);
|
||||||
|
this.left = Math.round(boxRect.left + window.scrollX - 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
insertBlock = (
|
||||||
|
ev: SyntheticEvent,
|
||||||
|
options: {
|
||||||
|
type: string | Object,
|
||||||
|
wrapper?: string | Object,
|
||||||
|
append?: string | Object,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const { type, wrapper, append } = options;
|
||||||
|
let { state } = this.props;
|
||||||
|
let transform = state.transform();
|
||||||
|
const { document } = state;
|
||||||
|
const parent = document.getParent(state.startBlock.key);
|
||||||
|
|
||||||
|
// lists get some special treatment
|
||||||
|
if (parent && parent.type === 'list-item') {
|
||||||
|
transform = transforms.unwrapList(
|
||||||
|
transforms
|
||||||
|
.splitListItem(transform.collapseToStart())
|
||||||
|
.collapseToEndOfPreviousBlock()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
transform = transform.insertBlock(type);
|
||||||
|
|
||||||
|
if (wrapper) transform = transform.wrapBlock(wrapper);
|
||||||
|
if (append) transform = transform.insertBlock(append);
|
||||||
|
|
||||||
|
state = transform.focus().apply();
|
||||||
|
this.props.onChange(state);
|
||||||
|
this.active = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onPickImage = (ev: SyntheticEvent) => {
|
||||||
|
// simulate a click on the file upload input element
|
||||||
|
this.file.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
onChooseImage = async (ev: SyntheticEvent) => {
|
||||||
|
const files = getDataTransferFiles(ev);
|
||||||
|
for (const file of files) {
|
||||||
|
await this.props.onInsertImage(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const style = { top: `${this.top}px`, left: `${this.left}px` };
|
||||||
|
const todo = { type: 'list-item', data: { checked: false } };
|
||||||
|
const rule = { type: 'horizontal-rule', isVoid: true };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal isOpened>
|
||||||
|
<Trigger active={this.active} style={style}>
|
||||||
|
<HiddenInput
|
||||||
|
type="file"
|
||||||
|
innerRef={ref => (this.file = ref)}
|
||||||
|
onChange={this.onChooseImage}
|
||||||
|
accept="image/*"
|
||||||
|
/>
|
||||||
|
<BlockMenu
|
||||||
|
label={<Icon type="PlusCircle" />}
|
||||||
|
onPickImage={this.onPickImage}
|
||||||
|
onInsertList={ev =>
|
||||||
|
this.insertBlock(ev, {
|
||||||
|
type: 'list-item',
|
||||||
|
wrapper: 'bulleted-list',
|
||||||
|
})}
|
||||||
|
onInsertTodoList={ev =>
|
||||||
|
this.insertBlock(ev, { type: todo, wrapper: 'todo-list' })}
|
||||||
|
onInsertBreak={ev =>
|
||||||
|
this.insertBlock(ev, { type: rule, append: 'paragraph' })}
|
||||||
|
onOpen={this.handleMenuOpen}
|
||||||
|
onClose={this.handleMenuClose}
|
||||||
|
/>
|
||||||
|
</Trigger>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HiddenInput = styled.input`
|
||||||
|
position: absolute;
|
||||||
|
top: -100px;
|
||||||
|
left: -100px;
|
||||||
|
visibility: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Trigger = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0;
|
||||||
|
background-color: ${color.white};
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
||||||
|
line-height: 0;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
transform: scale(.9);
|
||||||
|
|
||||||
|
${({ active }) => active && `
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: .9;
|
||||||
|
`}
|
||||||
|
`;
|
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;
|
@ -9,7 +9,6 @@ import Heading2Icon from 'components/Icon/Heading2Icon';
|
|||||||
import ItalicIcon from 'components/Icon/ItalicIcon';
|
import ItalicIcon from 'components/Icon/ItalicIcon';
|
||||||
import LinkIcon from 'components/Icon/LinkIcon';
|
import LinkIcon from 'components/Icon/LinkIcon';
|
||||||
import StrikethroughIcon from 'components/Icon/StrikethroughIcon';
|
import StrikethroughIcon from 'components/Icon/StrikethroughIcon';
|
||||||
import BulletedListIcon from 'components/Icon/BulletedListIcon';
|
|
||||||
|
|
||||||
export default class FormattingToolbar extends Component {
|
export default class FormattingToolbar extends Component {
|
||||||
props: {
|
props: {
|
||||||
@ -95,7 +94,6 @@ export default class FormattingToolbar extends Component {
|
|||||||
{this.renderMarkButton('deleted', StrikethroughIcon)}
|
{this.renderMarkButton('deleted', StrikethroughIcon)}
|
||||||
{this.renderBlockButton('heading1', Heading1Icon)}
|
{this.renderBlockButton('heading1', Heading1Icon)}
|
||||||
{this.renderBlockButton('heading2', Heading2Icon)}
|
{this.renderBlockButton('heading2', Heading2Icon)}
|
||||||
{this.renderBlockButton('bulleted-list', BulletedListIcon)}
|
|
||||||
{this.renderMarkButton('code', CodeIcon)}
|
{this.renderMarkButton('code', CodeIcon)}
|
||||||
<ToolbarButton onMouseDown={this.onCreateLink}>
|
<ToolbarButton onMouseDown={this.onCreateLink}>
|
||||||
<LinkIcon light />
|
<LinkIcon light />
|
||||||
|
@ -15,7 +15,6 @@ export default async function insertImageFile(
|
|||||||
try {
|
try {
|
||||||
// load the file as a data URL
|
// load the file as a data URL
|
||||||
const id = uuid.v4();
|
const id = uuid.v4();
|
||||||
const alt = file.name;
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.addEventListener('load', () => {
|
reader.addEventListener('load', () => {
|
||||||
const src = reader.result;
|
const src = reader.result;
|
||||||
@ -25,7 +24,7 @@ export default async function insertImageFile(
|
|||||||
.insertBlock({
|
.insertBlock({
|
||||||
type: 'image',
|
type: 'image',
|
||||||
isVoid: true,
|
isVoid: true,
|
||||||
data: { src, alt, id, loading: true },
|
data: { src, id, loading: true },
|
||||||
})
|
})
|
||||||
.apply();
|
.apply();
|
||||||
editor.onChange(state);
|
editor.onChange(state);
|
||||||
@ -46,7 +45,7 @@ export default async function insertImageFile(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return finalTransform.setNodeByKey(placeholder.key, {
|
return finalTransform.setNodeByKey(placeholder.key, {
|
||||||
data: { src, alt, loading: false },
|
data: { src, loading: false },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images';
|
import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images';
|
||||||
import PasteLinkify from 'slate-paste-linkify';
|
import PasteLinkify from 'slate-paste-linkify';
|
||||||
import EditList from 'slate-edit-list';
|
|
||||||
import CollapseOnEscape from 'slate-collapse-on-escape';
|
import CollapseOnEscape from 'slate-collapse-on-escape';
|
||||||
import TrailingBlock from 'slate-trailing-block';
|
import TrailingBlock from 'slate-trailing-block';
|
||||||
import EditCode from 'slate-edit-code';
|
import EditCode from 'slate-edit-code';
|
||||||
import Prism from 'slate-prism';
|
import Prism from 'slate-prism';
|
||||||
|
import EditList from './plugins/EditList';
|
||||||
import KeyboardShortcuts from './plugins/KeyboardShortcuts';
|
import KeyboardShortcuts from './plugins/KeyboardShortcuts';
|
||||||
import MarkdownShortcuts from './plugins/MarkdownShortcuts';
|
import MarkdownShortcuts from './plugins/MarkdownShortcuts';
|
||||||
import insertImage from './insertImage';
|
import insertImage from './insertImage';
|
||||||
@ -35,10 +35,7 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
EditList({
|
EditList,
|
||||||
types: ['ordered-list', 'bulleted-list', 'todo-list'],
|
|
||||||
typeItem: 'list-item',
|
|
||||||
}),
|
|
||||||
EditCode({
|
EditCode({
|
||||||
onlyIn: onlyInCode,
|
onlyIn: onlyInCode,
|
||||||
containerType: 'code',
|
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 === '--') {
|
if (chars === '--') {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const transform = state
|
return state
|
||||||
.transform()
|
.transform()
|
||||||
.extendToStartOf(startBlock)
|
.extendToStartOf(startBlock)
|
||||||
.delete()
|
.delete()
|
||||||
.setBlock({
|
.setBlock({
|
||||||
type: 'horizontal-rule',
|
type: 'horizontal-rule',
|
||||||
isVoid: true,
|
isVoid: true,
|
||||||
});
|
})
|
||||||
state = transform
|
|
||||||
.collapseToStartOfNextBlock()
|
.collapseToStartOfNextBlock()
|
||||||
.insertBlock('paragraph')
|
.insertBlock('paragraph')
|
||||||
.apply();
|
.apply();
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Code from './components/Code';
|
import Code from './components/Code';
|
||||||
|
import HorizontalRule from './components/HorizontalRule';
|
||||||
import InlineCode from './components/InlineCode';
|
import InlineCode from './components/InlineCode';
|
||||||
import Image from './components/Image';
|
import Image from './components/Image';
|
||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
@ -33,7 +34,7 @@ const createSchema = () => {
|
|||||||
'block-quote': (props: Props) => (
|
'block-quote': (props: Props) => (
|
||||||
<blockquote>{props.children}</blockquote>
|
<blockquote>{props.children}</blockquote>
|
||||||
),
|
),
|
||||||
'horizontal-rule': (props: Props) => <hr />,
|
'horizontal-rule': HorizontalRule,
|
||||||
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
|
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
|
||||||
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
|
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
|
||||||
'todo-list': (props: Props) => <TodoList>{props.children}</TodoList>,
|
'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;
|
|
@ -95,7 +95,7 @@ type Props = {
|
|||||||
<CollectionMenu
|
<CollectionMenu
|
||||||
history={history}
|
history={history}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
onShow={() => (this.menuOpen = true)}
|
onOpen={() => (this.menuOpen = true)}
|
||||||
onClose={() => (this.menuOpen = false)}
|
onClose={() => (this.menuOpen = false)}
|
||||||
onImport={this.handleImport}
|
onImport={this.handleImport}
|
||||||
open={this.menuOpen}
|
open={this.menuOpen}
|
||||||
|
@ -16,7 +16,7 @@ const activeStyle = {
|
|||||||
const StyleableDiv = props => <div {...props} />;
|
const StyleableDiv = props => <div {...props} />;
|
||||||
|
|
||||||
const styleComponent = component => styled(component)`
|
const styleComponent = component => styled(component)`
|
||||||
display: block;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -42,7 +42,7 @@ function SidebarLink(props: Object) {
|
|||||||
<Flex>
|
<Flex>
|
||||||
<Component exact activeStyle={activeStyle} {...props}>
|
<Component exact activeStyle={activeStyle} {...props}>
|
||||||
{props.hasChildren && <StyledChevron expanded={props.expanded} />}
|
{props.hasChildren && <StyledChevron expanded={props.expanded} />}
|
||||||
{props.children}
|
<Content>{props.children}</Content>
|
||||||
</Component>
|
</Component>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@ -62,4 +62,8 @@ const StyledChevron = styled(ChevronIcon)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Content = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
export default SidebarLink;
|
export default SidebarLink;
|
||||||
|
@ -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();
|
authenticatedStores.collections.fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
49
frontend/menus/BlockMenu.js
Normal file
49
frontend/menus/BlockMenu.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// @flow
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||||
|
|
||||||
|
@observer class BlockMenu extends Component {
|
||||||
|
props: {
|
||||||
|
label?: React$Element<*>,
|
||||||
|
onPickImage: SyntheticEvent => void,
|
||||||
|
onInsertList: SyntheticEvent => void,
|
||||||
|
onInsertTodoList: SyntheticEvent => void,
|
||||||
|
onInsertBreak: SyntheticEvent => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
onPickImage,
|
||||||
|
onInsertList,
|
||||||
|
onInsertTodoList,
|
||||||
|
onInsertBreak,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu
|
||||||
|
style={{ marginRight: -70, marginTop: 5 }}
|
||||||
|
label={label}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem onClick={onPickImage}>
|
||||||
|
<Icon type="Image" /> Add images
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={onInsertList}>
|
||||||
|
<Icon type="List" /> Start list
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={onInsertTodoList}>
|
||||||
|
<Icon type="CheckSquare" /> Start checklist
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={onInsertBreak}>
|
||||||
|
<Icon type="Minus" /> Add break
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlockMenu;
|
@ -12,7 +12,7 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
|||||||
@observer class CollectionMenu extends Component {
|
@observer class CollectionMenu extends Component {
|
||||||
props: {
|
props: {
|
||||||
label?: React$Element<any>,
|
label?: React$Element<any>,
|
||||||
onShow?: () => void,
|
onOpen?: () => void,
|
||||||
onClose?: () => void,
|
onClose?: () => void,
|
||||||
onImport?: () => void,
|
onImport?: () => void,
|
||||||
history: Object,
|
history: Object,
|
||||||
@ -36,13 +36,13 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { collection, label, onShow, onClose, onImport } = this.props;
|
const { collection, label, onOpen, onClose, onImport } = this.props;
|
||||||
const { allowDelete } = collection;
|
const { allowDelete } = collection;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
label={label || <MoreIcon type="MoreHorizontal" />}
|
label={label || <MoreIcon type="MoreHorizontal" />}
|
||||||
onShow={onShow}
|
onOpen={onOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
{collection &&
|
{collection &&
|
||||||
|
@ -2,30 +2,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
import HtmlContent from 'components/HtmlContent';
|
import Editor from 'components/Editor';
|
||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
|
|
||||||
import { convertToMarkdown } from 'utils/markdown';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string,
|
title: string,
|
||||||
content: string,
|
content: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer class Flatpage extends React.Component {
|
const Flatpage = observer((props: Props) => {
|
||||||
props: Props;
|
const { title, content } = props;
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { title, content } = this.props;
|
<CenteredContent>
|
||||||
const htmlContent = convertToMarkdown(content);
|
<PageTitle title={title} />
|
||||||
|
<Editor
|
||||||
return (
|
text={content}
|
||||||
<CenteredContent>
|
onChange={() => {}}
|
||||||
<PageTitle title={title} />
|
onSave={() => {}}
|
||||||
<HtmlContent dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
onCancel={() => {}}
|
||||||
</CenteredContent>
|
onImageUploadStart={() => {}}
|
||||||
);
|
onImageUploadStop={() => {}}
|
||||||
}
|
readOnly
|
||||||
}
|
/>
|
||||||
|
</CenteredContent>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default Flatpage;
|
export default Flatpage;
|
||||||
|
@ -12,6 +12,7 @@ import { searchUrl } from 'utils/routeHelpers';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||||
|
|
||||||
|
import Empty from 'components/Empty';
|
||||||
import Flex from 'components/Flex';
|
import Flex from 'components/Flex';
|
||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
import LoadingIndicator from 'components/LoadingIndicator';
|
import LoadingIndicator from 'components/LoadingIndicator';
|
||||||
@ -57,7 +58,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
|
|||||||
firstDocument: HTMLElement;
|
firstDocument: HTMLElement;
|
||||||
props: Props;
|
props: Props;
|
||||||
|
|
||||||
@observable resultIds: Array<string> = []; // Document IDs
|
@observable resultIds: string[] = []; // Document IDs
|
||||||
@observable searchTerm: ?string = null;
|
@observable searchTerm: ?string = null;
|
||||||
@observable isFetching = false;
|
@observable isFetching = false;
|
||||||
|
|
||||||
@ -131,18 +132,19 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { documents } = this.props;
|
const { documents, notFound } = this.props;
|
||||||
const query = this.props.match.params.query;
|
const query = this.props.match.params.query;
|
||||||
const hasResults = this.resultIds.length > 0;
|
const hasResults = this.resultIds.length > 0;
|
||||||
|
const showEmpty = !this.isFetching && this.searchTerm && !hasResults;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container auto>
|
<Container auto>
|
||||||
<PageTitle title={this.title} />
|
<PageTitle title={this.title} />
|
||||||
{this.isFetching && <LoadingIndicator />}
|
{this.isFetching && <LoadingIndicator />}
|
||||||
{this.props.notFound &&
|
{notFound &&
|
||||||
<div>
|
<div>
|
||||||
<h1>Not Found</h1>
|
<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>}
|
</div>}
|
||||||
<ResultsWrapper pinToTop={hasResults} column auto>
|
<ResultsWrapper pinToTop={hasResults} column auto>
|
||||||
<SearchField
|
<SearchField
|
||||||
@ -151,6 +153,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
|
|||||||
onChange={this.updateQuery}
|
onChange={this.updateQuery}
|
||||||
value={query || ''}
|
value={query || ''}
|
||||||
/>
|
/>
|
||||||
|
{showEmpty && <Empty>Oop, no matching documents.</Empty>}
|
||||||
<ResultList visible={hasResults}>
|
<ResultList visible={hasResults}>
|
||||||
<StyledArrowKeyNavigation
|
<StyledArrowKeyNavigation
|
||||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||||
|
@ -5,22 +5,8 @@ import Flex from 'components/Flex';
|
|||||||
import { color } from 'styles/constants';
|
import { color } from 'styles/constants';
|
||||||
import styled from 'styled-components';
|
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 {
|
class SearchField extends Component {
|
||||||
input: HTMLElement;
|
input: HTMLInputElement;
|
||||||
props: {
|
props: {
|
||||||
onChange: Function,
|
onChange: Function,
|
||||||
};
|
};
|
||||||
@ -33,23 +19,24 @@ class SearchField extends Component {
|
|||||||
this.input.focus();
|
this.input.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
setRef = (ref: HTMLElement) => {
|
setRef = (ref: HTMLInputElement) => {
|
||||||
this.input = ref;
|
this.input = ref;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
<Icon
|
<StyledIcon
|
||||||
type="Search"
|
type="Search"
|
||||||
size={48}
|
size={46}
|
||||||
color="#C9CFD6"
|
color={color.slateLight}
|
||||||
onClick={this.focusInput}
|
onClick={this.focusInput}
|
||||||
/>
|
/>
|
||||||
<Field
|
<StyledInput
|
||||||
{...this.props}
|
{...this.props}
|
||||||
innerRef={this.setRef}
|
innerRef={this.setRef}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
|
spellCheck="false"
|
||||||
placeholder="Search…"
|
placeholder="Search…"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
@ -58,4 +45,22 @@ class SearchField extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledInput = styled.input`
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 400;
|
||||||
|
outline: none;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
::-webkit-input-placeholder { color: ${color.slateLight}; }
|
||||||
|
:-moz-placeholder { color: ${color.slateLight}; }
|
||||||
|
::-moz-placeholder { color: ${color.slateLight}; }
|
||||||
|
:-ms-input-placeholder { color: ${color.slateLight}; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIcon = styled(Icon)`
|
||||||
|
top: 3px;
|
||||||
|
`;
|
||||||
|
|
||||||
export default SearchField;
|
export default SearchField;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect } from 'react-router';
|
import { Redirect } from 'react-router';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
|
import { observable } from 'mobx';
|
||||||
import { observer, inject } from 'mobx-react';
|
import { observer, inject } from 'mobx-react';
|
||||||
import { client } from 'utils/ApiClient';
|
import { client } from 'utils/ApiClient';
|
||||||
|
|
||||||
@ -12,17 +13,15 @@ type Props = {
|
|||||||
location: Object,
|
location: Object,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
|
||||||
redirectTo: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer class SlackAuth extends React.Component {
|
@observer class SlackAuth extends React.Component {
|
||||||
props: Props;
|
props: Props;
|
||||||
state: State;
|
@observable redirectTo: string;
|
||||||
state = {};
|
|
||||||
|
|
||||||
// $FlowIssue Flow doesn't like async lifecycle components https://github.com/facebook/flow/issues/1803
|
componentDidMount() {
|
||||||
async componentDidMount(): void {
|
this.redirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async redirect() {
|
||||||
const { error, code, state } = queryString.parse(
|
const { error, code, state } = queryString.parse(
|
||||||
this.props.location.search
|
this.props.location.search
|
||||||
);
|
);
|
||||||
@ -30,18 +29,18 @@ type State = {
|
|||||||
if (error) {
|
if (error) {
|
||||||
if (error === 'access_denied') {
|
if (error === 'access_denied') {
|
||||||
// User selected "Deny" access on Slack OAuth
|
// User selected "Deny" access on Slack OAuth
|
||||||
this.setState({ redirectTo: '/dashboard' });
|
this.redirectTo = '/dashboard';
|
||||||
} else {
|
} else {
|
||||||
this.setState({ redirectTo: '/auth/error' });
|
this.redirectTo = '/auth/error';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.props.location.pathname === '/auth/slack/commands') {
|
if (this.props.location.pathname === '/auth/slack/commands') {
|
||||||
// User adding webhook integrations
|
// User adding webhook integrations
|
||||||
try {
|
try {
|
||||||
await client.post('/auth.slackCommands', { code });
|
await client.post('/auth.slackCommands', { code });
|
||||||
this.setState({ redirectTo: '/dashboard' });
|
this.redirectTo = '/dashboard';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ redirectTo: '/auth/error' });
|
this.redirectTo = '/auth/error';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular Slack authentication
|
// Regular Slack authentication
|
||||||
@ -50,18 +49,15 @@ type State = {
|
|||||||
|
|
||||||
const { success } = await this.props.auth.authWithSlack(code, state);
|
const { success } = await this.props.auth.authWithSlack(code, state);
|
||||||
success
|
success
|
||||||
? this.setState({ redirectTo: redirectTo || '/dashboard' })
|
? (this.redirectTo = redirectTo || '/dashboard')
|
||||||
: this.setState({ redirectTo: '/auth/error' });
|
: (this.redirectTo = '/auth/error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
if (this.redirectTo) return <Redirect to={this.redirectTo} />;
|
||||||
<div>
|
return null;
|
||||||
{this.state.redirectTo && <Redirect to={this.state.redirectTo} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import { observer, inject } from 'mobx-react';
|
import { observer, inject } from 'mobx-react';
|
||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
||||||
|
import Empty from 'components/Empty';
|
||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import DocumentList from 'components/DocumentList';
|
import DocumentList from 'components/DocumentList';
|
||||||
import DocumentsStore from 'stores/DocumentsStore';
|
import DocumentsStore from 'stores/DocumentsStore';
|
||||||
@ -17,14 +18,17 @@ import DocumentsStore from 'stores/DocumentsStore';
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isLoaded, isFetching } = this.props.documents;
|
const { isLoaded, isFetching, starred } = this.props.documents;
|
||||||
|
const showLoading = !isLoaded && isFetching;
|
||||||
|
const showEmpty = isLoaded && !starred.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent column auto>
|
<CenteredContent column auto>
|
||||||
<PageTitle title="Starred" />
|
<PageTitle title="Starred" />
|
||||||
<h1>Starred</h1>
|
<h1>Starred</h1>
|
||||||
{!isLoaded && isFetching && <ListPlaceholder />}
|
{showLoading && <ListPlaceholder />}
|
||||||
<DocumentList documents={this.props.documents.starred} />
|
{showEmpty && <Empty>No starred documents yet.</Empty>}
|
||||||
|
<DocumentList documents={starred} />
|
||||||
</CenteredContent>
|
</CenteredContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { keyframes } from 'styled-components';
|
import { keyframes } from 'styled-components';
|
||||||
|
|
||||||
|
export const fadeIn = keyframes`
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
`;
|
||||||
|
|
||||||
export const fadeAndScaleIn = keyframes`
|
export const fadeAndScaleIn = keyframes`
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
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": {
|
"dependencies": {
|
||||||
"@tommoor/slate-drop-or-paste-images": "0.5.1",
|
"@tommoor/slate-drop-or-paste-images": "0.5.1",
|
||||||
|
"aws-sdk": "^2.135.0",
|
||||||
"babel-core": "^6.24.1",
|
"babel-core": "^6.24.1",
|
||||||
"babel-eslint": "^7.2.3",
|
"babel-eslint": "^7.2.3",
|
||||||
"babel-loader": "6.2.5",
|
"babel-loader": "6.2.5",
|
||||||
@ -85,7 +86,6 @@
|
|||||||
"css-loader": "^0.28.7",
|
"css-loader": "^0.28.7",
|
||||||
"debug": "2.2.0",
|
"debug": "2.2.0",
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
"emoji-name-map": "1.1.2",
|
|
||||||
"emoji-regex": "^6.5.1",
|
"emoji-regex": "^6.5.1",
|
||||||
"eslint": "^3.19.0",
|
"eslint": "^3.19.0",
|
||||||
"eslint-config-react-app": "^0.6.2",
|
"eslint-config-react-app": "^0.6.2",
|
||||||
@ -100,7 +100,6 @@
|
|||||||
"fbemitter": "^2.1.1",
|
"fbemitter": "^2.1.1",
|
||||||
"file-loader": "0.9.0",
|
"file-loader": "0.9.0",
|
||||||
"flow-typed": "^2.1.2",
|
"flow-typed": "^2.1.2",
|
||||||
"highlight.js": "9.4.0",
|
|
||||||
"history": "3.0.0",
|
"history": "3.0.0",
|
||||||
"html-webpack-plugin": "2.17.0",
|
"html-webpack-plugin": "2.17.0",
|
||||||
"http-errors": "1.4.0",
|
"http-errors": "1.4.0",
|
||||||
@ -108,7 +107,6 @@
|
|||||||
"invariant": "^2.2.2",
|
"invariant": "^2.2.2",
|
||||||
"isomorphic-fetch": "2.2.1",
|
"isomorphic-fetch": "2.2.1",
|
||||||
"js-search": "^1.4.2",
|
"js-search": "^1.4.2",
|
||||||
"js-tree": "1.1.0",
|
|
||||||
"json-loader": "0.5.4",
|
"json-loader": "0.5.4",
|
||||||
"jsonwebtoken": "7.0.1",
|
"jsonwebtoken": "7.0.1",
|
||||||
"koa": "^2.2.0",
|
"koa": "^2.2.0",
|
||||||
@ -125,8 +123,6 @@
|
|||||||
"localforage": "^1.5.0",
|
"localforage": "^1.5.0",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"lodash.orderby": "4.4.0",
|
"lodash.orderby": "4.4.0",
|
||||||
"marked": "0.3.6",
|
|
||||||
"marked-sanitized": "^0.1.1",
|
|
||||||
"mobx": "^3.1.9",
|
"mobx": "^3.1.9",
|
||||||
"mobx-react": "^4.1.8",
|
"mobx-react": "^4.1.8",
|
||||||
"mobx-react-devtools": "^4.2.11",
|
"mobx-react-devtools": "^4.2.11",
|
||||||
@ -170,7 +166,6 @@
|
|||||||
"string-hash": "^1.1.0",
|
"string-hash": "^1.1.0",
|
||||||
"style-loader": "^0.18.2",
|
"style-loader": "^0.18.2",
|
||||||
"styled-components": "^2.0.0",
|
"styled-components": "^2.0.0",
|
||||||
"truncate-html": "https://github.com/jorilallo/truncate-html/tarball/master",
|
|
||||||
"url-loader": "0.5.7",
|
"url-loader": "0.5.7",
|
||||||
"uuid": "2.0.2",
|
"uuid": "2.0.2",
|
||||||
"validator": "5.2.0",
|
"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)
|
|
||||||
);
|
|
@ -47,6 +47,10 @@ router.post('auth.slack', async ctx => {
|
|||||||
await team.createFirstCollection(user.id);
|
await team.createFirstCollection(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update user's avatar
|
||||||
|
await user.updateAvatar();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: {
|
data: {
|
||||||
user: await presentUser(ctx, user),
|
user: await presentUser(ctx, user),
|
||||||
|
@ -4,10 +4,16 @@ import httpErrors from 'http-errors';
|
|||||||
|
|
||||||
import auth from './middlewares/authentication';
|
import auth from './middlewares/authentication';
|
||||||
import pagination from './middlewares/pagination';
|
import pagination from './middlewares/pagination';
|
||||||
import { presentDocument } from '../presenters';
|
import { presentDocument, presentRevision } from '../presenters';
|
||||||
import { Document, Collection, Star, View } from '../models';
|
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();
|
const router = new Router();
|
||||||
|
|
||||||
router.post('documents.list', auth(), pagination(), async ctx => {
|
router.post('documents.list', auth(), pagination(), async ctx => {
|
||||||
let { sort = 'updatedAt', direction } = ctx.body;
|
let { sort = 'updatedAt', direction } = ctx.body;
|
||||||
if (direction !== 'ASC') direction = 'DESC';
|
if (direction !== 'ASC') direction = 'DESC';
|
||||||
@ -101,23 +107,38 @@ router.post('documents.info', auth(), async ctx => {
|
|||||||
ctx.assertPresent(id, 'id is required');
|
ctx.assertPresent(id, 'id is required');
|
||||||
const document = await Document.findById(id);
|
const document = await Document.findById(id);
|
||||||
|
|
||||||
if (!document) throw httpErrors.NotFound();
|
authDocumentForUser(ctx, document);
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await presentDocument(ctx, document),
|
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 => {
|
router.post('documents.search', auth(), async ctx => {
|
||||||
const { query } = ctx.body;
|
const { query } = ctx.body;
|
||||||
ctx.assertPresent(query, 'query is required');
|
ctx.assertPresent(query, 'query is required');
|
||||||
@ -142,8 +163,7 @@ router.post('documents.star', auth(), async ctx => {
|
|||||||
const user = await ctx.state.user;
|
const user = await ctx.state.user;
|
||||||
const document = await Document.findById(id);
|
const document = await Document.findById(id);
|
||||||
|
|
||||||
if (!document || document.teamId !== user.teamId)
|
authDocumentForUser(ctx, document);
|
||||||
throw httpErrors.BadRequest();
|
|
||||||
|
|
||||||
await Star.findOrCreate({
|
await Star.findOrCreate({
|
||||||
where: { documentId: document.id, userId: user.id },
|
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 user = await ctx.state.user;
|
||||||
const document = await Document.findById(id);
|
const document = await Document.findById(id);
|
||||||
|
|
||||||
if (!document || document.teamId !== user.teamId)
|
authDocumentForUser(ctx, document);
|
||||||
throw httpErrors.BadRequest();
|
|
||||||
|
|
||||||
await Star.destroy({
|
await Star.destroy({
|
||||||
where: { documentId: document.id, userId: user.id },
|
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 document = await Document.findById(id);
|
||||||
const collection = document.collection;
|
const collection = document.collection;
|
||||||
|
|
||||||
if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound();
|
authDocumentForUser(ctx, document);
|
||||||
|
|
||||||
// Update document
|
// Update document
|
||||||
if (title) document.title = title;
|
if (title) document.title = title;
|
||||||
@ -254,15 +273,14 @@ router.post('documents.move', auth(), async ctx => {
|
|||||||
ctx.assertUuid(parentDocument, 'parentDocument must be an uuid');
|
ctx.assertUuid(parentDocument, 'parentDocument must be an uuid');
|
||||||
if (index) ctx.assertPositiveInteger(index, 'index must be an integer (>=0)');
|
if (index) ctx.assertPositiveInteger(index, 'index must be an integer (>=0)');
|
||||||
|
|
||||||
const user = ctx.state.user;
|
|
||||||
const document = await Document.findById(id);
|
const document = await Document.findById(id);
|
||||||
const collection = await Collection.findById(document.atlasId);
|
const collection = await Collection.findById(document.atlasId);
|
||||||
|
|
||||||
|
authDocumentForUser(ctx, document);
|
||||||
|
|
||||||
if (collection.type !== 'atlas')
|
if (collection.type !== 'atlas')
|
||||||
throw httpErrors.BadRequest("This document can't be moved");
|
throw httpErrors.BadRequest("This document can't be moved");
|
||||||
|
|
||||||
if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound();
|
|
||||||
|
|
||||||
// Set parent document
|
// Set parent document
|
||||||
if (parentDocument) {
|
if (parentDocument) {
|
||||||
const parent = await Document.findById(parentDocument);
|
const parent = await Document.findById(parentDocument);
|
||||||
@ -292,12 +310,10 @@ router.post('documents.delete', auth(), async ctx => {
|
|||||||
const { id } = ctx.body;
|
const { id } = ctx.body;
|
||||||
ctx.assertPresent(id, 'id is required');
|
ctx.assertPresent(id, 'id is required');
|
||||||
|
|
||||||
const user = ctx.state.user;
|
|
||||||
const document = await Document.findById(id);
|
const document = await Document.findById(id);
|
||||||
const collection = await Collection.findById(document.atlasId);
|
const collection = await Collection.findById(document.atlasId);
|
||||||
|
|
||||||
if (!document || document.teamId !== user.teamId)
|
authDocumentForUser(ctx, document);
|
||||||
throw httpErrors.BadRequest();
|
|
||||||
|
|
||||||
if (collection.type === 'atlas') {
|
if (collection.type === 'atlas') {
|
||||||
// Don't allow deletion of root docs
|
// 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 () => {
|
describe('#documents.search', async () => {
|
||||||
it('should return results', async () => {
|
it('should return results', async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
|
@ -65,8 +65,8 @@ if (process.env.NODE_ENV === 'development') {
|
|||||||
app.use(logger());
|
app.use(logger());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production' && process.env.BUGSNAG_KEY) {
|
||||||
bugsnag.register('ad7a85f99b1b9324a31e16732cdf3192');
|
bugsnag.register(process.env.BUGSNAG_KEY);
|
||||||
app.on('error', bugsnag.koaHandler);
|
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,
|
creatorId: DataTypes.UUID,
|
||||||
|
|
||||||
/* type: atlas */
|
/* type: atlas */
|
||||||
navigationTree: DataTypes.JSONB, // legacy
|
|
||||||
documentStructure: DataTypes.JSONB,
|
documentStructure: DataTypes.JSONB,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -98,28 +97,6 @@ Collection.prototype.getUrl = function() {
|
|||||||
return `/collections/${this.id}`;
|
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(
|
Collection.prototype.addDocumentToStructure = async function(
|
||||||
document,
|
document,
|
||||||
index,
|
index,
|
||||||
|
@ -2,12 +2,9 @@
|
|||||||
import slug from 'slug';
|
import slug from 'slug';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import randomstring from 'randomstring';
|
import randomstring from 'randomstring';
|
||||||
import emojiRegex from 'emoji-regex';
|
|
||||||
|
|
||||||
import isUUID from 'validator/lib/isUUID';
|
import isUUID from 'validator/lib/isUUID';
|
||||||
import { DataTypes, sequelize } from '../sequelize';
|
import { DataTypes, sequelize } from '../sequelize';
|
||||||
import { convertToMarkdown } from '../../frontend/utils/markdown';
|
|
||||||
import { truncateMarkdown } from '../utils/truncate';
|
|
||||||
import parseTitle from '../../shared/parseTitle';
|
import parseTitle from '../../shared/parseTitle';
|
||||||
import Revision from './Revision';
|
import Revision from './Revision';
|
||||||
|
|
||||||
@ -25,8 +22,6 @@ const createRevision = doc => {
|
|||||||
return Revision.create({
|
return Revision.create({
|
||||||
title: doc.title,
|
title: doc.title,
|
||||||
text: doc.text,
|
text: doc.text,
|
||||||
html: doc.html,
|
|
||||||
preview: doc.preview,
|
|
||||||
userId: doc.lastModifiedById,
|
userId: doc.lastModifiedById,
|
||||||
documentId: doc.id,
|
documentId: doc.id,
|
||||||
});
|
});
|
||||||
@ -40,8 +35,6 @@ const beforeSave = async doc => {
|
|||||||
const { emoji } = parseTitle(doc.text);
|
const { emoji } = parseTitle(doc.text);
|
||||||
|
|
||||||
doc.emoji = emoji;
|
doc.emoji = emoji;
|
||||||
doc.html = convertToMarkdown(doc.text);
|
|
||||||
doc.preview = truncateMarkdown(doc.text, 160);
|
|
||||||
doc.revisionCount += 1;
|
doc.revisionCount += 1;
|
||||||
|
|
||||||
// Collaborators
|
// Collaborators
|
||||||
@ -74,8 +67,6 @@ const Document = sequelize.define(
|
|||||||
private: { type: DataTypes.BOOLEAN, defaultValue: true },
|
private: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||||||
title: DataTypes.STRING,
|
title: DataTypes.STRING,
|
||||||
text: DataTypes.TEXT,
|
text: DataTypes.TEXT,
|
||||||
html: DataTypes.TEXT,
|
|
||||||
preview: DataTypes.TEXT,
|
|
||||||
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||||
parentDocumentId: DataTypes.UUID,
|
parentDocumentId: DataTypes.UUID,
|
||||||
createdById: {
|
createdById: {
|
||||||
|
@ -9,8 +9,6 @@ const Revision = sequelize.define('revision', {
|
|||||||
},
|
},
|
||||||
title: DataTypes.STRING,
|
title: DataTypes.STRING,
|
||||||
text: DataTypes.TEXT,
|
text: DataTypes.TEXT,
|
||||||
html: DataTypes.TEXT,
|
|
||||||
preview: DataTypes.TEXT,
|
|
||||||
|
|
||||||
userId: {
|
userId: {
|
||||||
type: 'UUID',
|
type: 'UUID',
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
|
import uuid from 'uuid';
|
||||||
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
||||||
|
import { uploadToS3FromUrl } from '../utils/s3';
|
||||||
|
|
||||||
import JWT from 'jsonwebtoken';
|
import JWT from 'jsonwebtoken';
|
||||||
|
|
||||||
@ -18,6 +20,7 @@ const User = sequelize.define(
|
|||||||
email: { type: DataTypes.STRING },
|
email: { type: DataTypes.STRING },
|
||||||
username: { type: DataTypes.STRING },
|
username: { type: DataTypes.STRING },
|
||||||
name: DataTypes.STRING,
|
name: DataTypes.STRING,
|
||||||
|
avatarUrl: { type: DataTypes.STRING, allowNull: true },
|
||||||
password: DataTypes.VIRTUAL,
|
password: DataTypes.VIRTUAL,
|
||||||
passwordDigest: DataTypes.STRING,
|
passwordDigest: DataTypes.STRING,
|
||||||
isAdmin: DataTypes.BOOLEAN,
|
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 => {
|
const setRandomJwtSecret = model => {
|
||||||
model.jwtSecret = crypto.randomBytes(64).toString('hex');
|
model.jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||||
|
@ -19,7 +19,7 @@ async function present(ctx: Object, collection: Collection) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (collection.type === 'atlas') {
|
if (collection.type === 'atlas') {
|
||||||
data.documents = await collection.getDocumentsStructure();
|
data.documents = collection.documentStructure;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collection.documents) {
|
if (collection.documents) {
|
||||||
|
@ -21,8 +21,6 @@ async function present(ctx: Object, document: Document, options: ?Options) {
|
|||||||
private: document.private,
|
private: document.private,
|
||||||
title: document.title,
|
title: document.title,
|
||||||
text: document.text,
|
text: document.text,
|
||||||
html: document.html,
|
|
||||||
preview: document.preview,
|
|
||||||
emoji: document.emoji,
|
emoji: document.emoji,
|
||||||
createdAt: document.createdAt,
|
createdAt: document.createdAt,
|
||||||
createdBy: presentUser(ctx, document.createdBy),
|
createdBy: presentUser(ctx, document.createdBy),
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import presentUser from './user';
|
import presentUser from './user';
|
||||||
import presentView from './view';
|
import presentView from './view';
|
||||||
import presentDocument from './document';
|
import presentDocument from './document';
|
||||||
|
import presentRevision from './revision';
|
||||||
import presentCollection from './collection';
|
import presentCollection from './collection';
|
||||||
import presentApiKey from './apiKey';
|
import presentApiKey from './apiKey';
|
||||||
import presentTeam from './team';
|
import presentTeam from './team';
|
||||||
@ -10,6 +11,7 @@ export {
|
|||||||
presentUser,
|
presentUser,
|
||||||
presentView,
|
presentView,
|
||||||
presentDocument,
|
presentDocument,
|
||||||
|
presentRevision,
|
||||||
presentCollection,
|
presentCollection,
|
||||||
presentApiKey,
|
presentApiKey,
|
||||||
presentTeam,
|
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,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
avatarUrl: user.slackData ? user.slackData.image_192 : null,
|
avatarUrl: user.avatarUrl ||
|
||||||
|
(user.slackData ? user.slackData.image_192 : null),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ export async function request(endpoint: string, body: Object) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw httpErrors.BadRequest();
|
throw httpErrors.BadRequest();
|
||||||
}
|
}
|
||||||
console.log('DATA', data);
|
|
||||||
if (!data.ok) throw httpErrors.BadRequest(data.error);
|
if (!data.ok) throw httpErrors.BadRequest(data.error);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@ -28,7 +27,7 @@ export async function oauthAccess(
|
|||||||
return request('oauth.access', {
|
return request('oauth.access', {
|
||||||
client_id: process.env.SLACK_KEY,
|
client_id: process.env.SLACK_KEY,
|
||||||
client_secret: process.env.SLACK_SECRET,
|
client_secret: process.env.SLACK_SECRET,
|
||||||
redirect_uri: `${process.env.URL || ''}/auth/slack`,
|
redirect_uri,
|
||||||
code,
|
code,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,6 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
<script src="//d2wy8f7a9ursnm.cloudfront.net/bugsnag-3.min.js" data-apikey="8165e2069605bc20ccd0792dbbfae7bf"></script>
|
<script src="//d2wy8f7a9ursnm.cloudfront.net/bugsnag-3.min.js" data-apikey="<%= BUGSNAG_KEY %>"></script>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -1,5 +1,18 @@
|
|||||||
|
// @flow
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import AWS from 'aws-sdk';
|
||||||
|
import invariant from 'invariant';
|
||||||
|
import fetch from 'isomorphic-fetch';
|
||||||
|
import bugsnag from 'bugsnag';
|
||||||
|
|
||||||
|
AWS.config.update({
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
|
||||||
|
const AWS_S3_UPLOAD_BUCKET_NAME = process.env.AWS_S3_UPLOAD_BUCKET_NAME;
|
||||||
|
|
||||||
const makePolicy = () => {
|
const makePolicy = () => {
|
||||||
const policy = {
|
const policy = {
|
||||||
@ -19,13 +32,37 @@ const makePolicy = () => {
|
|||||||
return new Buffer(JSON.stringify(policy)).toString('base64');
|
return new Buffer(JSON.stringify(policy)).toString('base64');
|
||||||
};
|
};
|
||||||
|
|
||||||
const signPolicy = policy => {
|
const signPolicy = (policy: any) => {
|
||||||
|
invariant(AWS_SECRET_ACCESS_KEY, 'AWS_SECRET_ACCESS_KEY not set');
|
||||||
const signature = crypto
|
const signature = crypto
|
||||||
.createHmac('sha1', process.env.AWS_SECRET_ACCESS_KEY)
|
.createHmac('sha1', AWS_SECRET_ACCESS_KEY)
|
||||||
.update(policy)
|
.update(policy)
|
||||||
.digest('base64');
|
.digest('base64');
|
||||||
|
|
||||||
return signature;
|
return signature;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { makePolicy, signPolicy };
|
const uploadToS3FromUrl = async (url: string, key: string) => {
|
||||||
|
const s3 = new AWS.S3();
|
||||||
|
invariant(AWS_S3_UPLOAD_BUCKET_NAME, 'AWS_S3_UPLOAD_BUCKET_NAME not set');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// $FlowIssue dunno it's fine
|
||||||
|
const res = await fetch(url);
|
||||||
|
const buffer = await res.buffer();
|
||||||
|
await s3
|
||||||
|
.putObject({
|
||||||
|
Bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME,
|
||||||
|
Key: key,
|
||||||
|
ContentType: res.headers['content-type'],
|
||||||
|
ContentLength: res.headers['content-length'],
|
||||||
|
Body: buffer,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
return `https://s3.amazonaws.com/${AWS_S3_UPLOAD_BUCKET_NAME}/${key}`;
|
||||||
|
} catch (e) {
|
||||||
|
bugsnag.notify(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { makePolicy, signPolicy, uploadToS3FromUrl };
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import truncate from 'truncate-html';
|
|
||||||
import { convertToMarkdown } from '../../frontend/utils/markdown';
|
|
||||||
|
|
||||||
truncate.defaultOptions = {
|
|
||||||
stripTags: false,
|
|
||||||
ellipsis: '...',
|
|
||||||
decodeEntities: false,
|
|
||||||
excludes: ['h1', 'pre'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const truncateMarkdown = (text, length) => {
|
|
||||||
const html = convertToMarkdown(text);
|
|
||||||
return truncate(html, length);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { truncateMarkdown };
|
|
@ -13,6 +13,7 @@ const definePlugin = new webpack.DefinePlugin({
|
|||||||
SLACK_REDIRECT_URI: JSON.stringify(process.env.SLACK_REDIRECT_URI),
|
SLACK_REDIRECT_URI: JSON.stringify(process.env.SLACK_REDIRECT_URI),
|
||||||
SLACK_KEY: JSON.stringify(process.env.SLACK_KEY),
|
SLACK_KEY: JSON.stringify(process.env.SLACK_KEY),
|
||||||
BASE_URL: JSON.stringify(process.env.URL),
|
BASE_URL: JSON.stringify(process.env.URL),
|
||||||
|
BUGSNAG_KEY: JSON.stringify(process.env.BUGSNAG_KEY),
|
||||||
DEPLOYMENT: JSON.stringify(process.env.DEPLOYMENT || 'hosted'),
|
DEPLOYMENT: JSON.stringify(process.env.DEPLOYMENT || 'hosted'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
128
yarn.lock
128
yarn.lock
@ -339,6 +339,21 @@ autoprefixer@^6.3.1:
|
|||||||
postcss "^5.2.16"
|
postcss "^5.2.16"
|
||||||
postcss-value-parser "^3.2.3"
|
postcss-value-parser "^3.2.3"
|
||||||
|
|
||||||
|
aws-sdk@^2.135.0:
|
||||||
|
version "2.135.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.135.0.tgz#81f4a47b99212f2f236bf5b11b0b3a3a02086db4"
|
||||||
|
dependencies:
|
||||||
|
buffer "4.9.1"
|
||||||
|
crypto-browserify "1.0.9"
|
||||||
|
events "^1.1.1"
|
||||||
|
jmespath "0.15.0"
|
||||||
|
querystring "0.2.0"
|
||||||
|
sax "1.2.1"
|
||||||
|
url "0.10.3"
|
||||||
|
uuid "3.1.0"
|
||||||
|
xml2js "0.4.17"
|
||||||
|
xmlbuilder "4.2.1"
|
||||||
|
|
||||||
aws-sign2@~0.6.0:
|
aws-sign2@~0.6.0:
|
||||||
version "0.6.0"
|
version "0.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
|
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
|
||||||
@ -1268,7 +1283,7 @@ buffer-xor@^1.0.2:
|
|||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
|
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
|
||||||
|
|
||||||
buffer@^4.3.0, buffer@^4.9.0:
|
buffer@4.9.1, buffer@^4.3.0, buffer@^4.9.0:
|
||||||
version "4.9.1"
|
version "4.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
|
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1454,7 +1469,7 @@ charenc@~0.0.1:
|
|||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||||
|
|
||||||
cheerio@0.22.0, cheerio@^0.22.0:
|
cheerio@^0.22.0:
|
||||||
version "0.22.0"
|
version "0.22.0"
|
||||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
|
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1959,6 +1974,10 @@ cryptiles@2.x.x:
|
|||||||
dependencies:
|
dependencies:
|
||||||
boom "2.x.x"
|
boom "2.x.x"
|
||||||
|
|
||||||
|
crypto-browserify@1.0.9:
|
||||||
|
version "1.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-1.0.9.tgz#cc5449685dfb85eb11c9828acc7cb87ab5bbfcc0"
|
||||||
|
|
||||||
crypto-browserify@^3.11.0:
|
crypto-browserify@^3.11.0:
|
||||||
version "3.11.1"
|
version "3.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
|
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
|
||||||
@ -2496,22 +2515,10 @@ elliptic@^6.0.0:
|
|||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.0"
|
minimalistic-crypto-utils "^1.0.0"
|
||||||
|
|
||||||
emoji-name-map@1.1.2:
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/emoji-name-map/-/emoji-name-map-1.1.2.tgz#662f6b5582b0eaf817be6a9ac272fbd8af10ae73"
|
|
||||||
dependencies:
|
|
||||||
emojilib "^2.0.2"
|
|
||||||
iterate-object "^1.3.1"
|
|
||||||
map-o "^2.0.1"
|
|
||||||
|
|
||||||
emoji-regex@^6.1.0, emoji-regex@^6.5.1:
|
emoji-regex@^6.1.0, emoji-regex@^6.5.1:
|
||||||
version "6.5.1"
|
version "6.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
|
||||||
|
|
||||||
emojilib@^2.0.2:
|
|
||||||
version "2.2.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.2.9.tgz#ec5722689fc148f56422c14b0dc16a901d446b75"
|
|
||||||
|
|
||||||
emojis-list@^2.0.0:
|
emojis-list@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||||
@ -2935,7 +2942,7 @@ event-stream@~3.3.0:
|
|||||||
stream-combiner "~0.0.4"
|
stream-combiner "~0.0.4"
|
||||||
through "~2.3.1"
|
through "~2.3.1"
|
||||||
|
|
||||||
events@^1.0.0:
|
events@^1.0.0, events@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
||||||
|
|
||||||
@ -3861,10 +3868,6 @@ hide-powered-by@1.0.0:
|
|||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
|
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
|
||||||
|
|
||||||
highlight.js@9.4.0:
|
|
||||||
version "9.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.4.0.tgz#2687d6cf6df0d57bc68585e836bfe3ab3edf9452"
|
|
||||||
|
|
||||||
history@3.0.0:
|
history@3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/history/-/history-3.0.0.tgz#02cff4e6f69dc62dd81161104a63f5b85ead0c85"
|
resolved "https://registry.yarnpkg.com/history/-/history-3.0.0.tgz#02cff4e6f69dc62dd81161104a63f5b85ead0c85"
|
||||||
@ -3965,7 +3968,7 @@ html-webpack-plugin@2.17.0:
|
|||||||
pretty-error "^2.0.0"
|
pretty-error "^2.0.0"
|
||||||
toposort "^0.2.12"
|
toposort "^0.2.12"
|
||||||
|
|
||||||
htmlparser2@^3.9.0, htmlparser2@^3.9.1:
|
htmlparser2@^3.9.1:
|
||||||
version "3.9.2"
|
version "3.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
|
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4611,10 +4614,6 @@ isurl@^1.0.0-alpha5:
|
|||||||
has-to-string-tag-x "^1.2.0"
|
has-to-string-tag-x "^1.2.0"
|
||||||
is-object "^1.0.1"
|
is-object "^1.0.1"
|
||||||
|
|
||||||
iterate-object@^1.3.0, iterate-object@^1.3.1:
|
|
||||||
version "1.3.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/iterate-object/-/iterate-object-1.3.2.tgz#24ec15affa5d0039e8839695a21c2cae1f45b66b"
|
|
||||||
|
|
||||||
jest-changed-files@^20.0.3:
|
jest-changed-files@^20.0.3:
|
||||||
version "20.0.3"
|
version "20.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-20.0.3.tgz#9394d5cc65c438406149bef1bf4d52b68e03e3f8"
|
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-20.0.3.tgz#9394d5cc65c438406149bef1bf4d52b68e03e3f8"
|
||||||
@ -4836,6 +4835,10 @@ jest-validate@^20.0.3:
|
|||||||
leven "^2.1.0"
|
leven "^2.1.0"
|
||||||
pretty-format "^20.0.3"
|
pretty-format "^20.0.3"
|
||||||
|
|
||||||
|
jmespath@0.15.0:
|
||||||
|
version "0.15.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
||||||
|
|
||||||
joi@^6.10.1, joi@~6.10.1:
|
joi@^6.10.1, joi@~6.10.1:
|
||||||
version "6.10.1"
|
version "6.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06"
|
resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06"
|
||||||
@ -4870,10 +4873,6 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
|
|||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||||
|
|
||||||
js-tree@1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/js-tree/-/js-tree-1.1.0.tgz#087ee3ec366a5b74eb14f486016c5e0e631f1670"
|
|
||||||
|
|
||||||
js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
|
js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
|
||||||
version "3.9.1"
|
version "3.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
|
||||||
@ -5720,12 +5719,6 @@ map-cache@^0.2.0:
|
|||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
|
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
|
||||||
|
|
||||||
map-o@^2.0.1:
|
|
||||||
version "2.0.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/map-o/-/map-o-2.0.7.tgz#7b59395ee87a5200ec2ef881938e9e257f747d61"
|
|
||||||
dependencies:
|
|
||||||
iterate-object "^1.3.0"
|
|
||||||
|
|
||||||
map-obj@^1.0.0, map-obj@^1.0.1:
|
map-obj@^1.0.0, map-obj@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
|
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
|
||||||
@ -5734,12 +5727,6 @@ map-stream@~0.1.0:
|
|||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
|
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
|
||||||
|
|
||||||
marked-sanitized@^0.1.1:
|
|
||||||
version "0.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/marked-sanitized/-/marked-sanitized-0.1.1.tgz#8a5756887217f64fe92e1a92e71d0cc10e767829"
|
|
||||||
dependencies:
|
|
||||||
sanitize-html "^1.5.2"
|
|
||||||
|
|
||||||
marked-terminal@^1.6.2:
|
marked-terminal@^1.6.2:
|
||||||
version "1.7.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904"
|
resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904"
|
||||||
@ -5750,7 +5737,7 @@ marked-terminal@^1.6.2:
|
|||||||
lodash.assign "^4.2.0"
|
lodash.assign "^4.2.0"
|
||||||
node-emoji "^1.4.1"
|
node-emoji "^1.4.1"
|
||||||
|
|
||||||
marked@0.3.6, marked@^0.3.6:
|
marked@^0.3.6:
|
||||||
version "0.3.6"
|
version "0.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
|
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
|
||||||
|
|
||||||
@ -7610,10 +7597,6 @@ regex-cache@^0.4.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-equal-shallow "^0.1.3"
|
is-equal-shallow "^0.1.3"
|
||||||
|
|
||||||
regexp-quote@0.0.0:
|
|
||||||
version "0.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/regexp-quote/-/regexp-quote-0.0.0.tgz#1e0f4650c862dcbfed54fd42b148e9bb1721fcf2"
|
|
||||||
|
|
||||||
regexpu-core@^1.0.0:
|
regexpu-core@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
|
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
|
||||||
@ -7846,15 +7829,11 @@ sane@~1.6.0:
|
|||||||
walker "~1.0.5"
|
walker "~1.0.5"
|
||||||
watch "~0.10.0"
|
watch "~0.10.0"
|
||||||
|
|
||||||
sanitize-html@^1.5.2:
|
sax@1.2.1:
|
||||||
version "1.14.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.14.1.tgz#730ffa2249bdf18333effe45b286173c9c5ad0b8"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
||||||
dependencies:
|
|
||||||
htmlparser2 "^3.9.0"
|
|
||||||
regexp-quote "0.0.0"
|
|
||||||
xtend "^4.0.0"
|
|
||||||
|
|
||||||
sax@^1.2.1, sax@~1.2.1:
|
sax@>=0.6.0, sax@^1.2.1, sax@~1.2.1:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
|
|
||||||
@ -8084,8 +8063,8 @@ slate-edit-list@^0.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/slate-edit-list/-/slate-edit-list-0.7.1.tgz#84ee960d2d5b5a20ce267ad9df894395a91b93d5"
|
resolved "https://registry.yarnpkg.com/slate-edit-list/-/slate-edit-list-0.7.1.tgz#84ee960d2d5b5a20ce267ad9df894395a91b93d5"
|
||||||
|
|
||||||
slate-markdown-serializer@tommoor/slate-markdown-serializer:
|
slate-markdown-serializer@tommoor/slate-markdown-serializer:
|
||||||
version "0.4.3"
|
version "0.5.2"
|
||||||
resolved "https://codeload.github.com/tommoor/slate-markdown-serializer/tar.gz/8e987951db999617ff6759c85e384dad175d5b92"
|
resolved "https://codeload.github.com/tommoor/slate-markdown-serializer/tar.gz/75708cf421c0a0ac0d7d541b295315cbff0839c0"
|
||||||
|
|
||||||
slate-paste-linkify@^0.2.1:
|
slate-paste-linkify@^0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
@ -8711,12 +8690,6 @@ trim-right@^1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
|
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
|
||||||
|
|
||||||
"truncate-html@https://github.com/jorilallo/truncate-html/tarball/master":
|
|
||||||
version "0.1.1"
|
|
||||||
resolved "https://github.com/jorilallo/truncate-html/tarball/master#5856f297610d202045d997965fc8c33be453c2e9"
|
|
||||||
dependencies:
|
|
||||||
cheerio "0.22.0"
|
|
||||||
|
|
||||||
tryit@^1.0.1:
|
tryit@^1.0.1:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
|
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
|
||||||
@ -8903,16 +8876,16 @@ url-to-options@^1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
|
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
|
||||||
|
|
||||||
url@^0.11.0:
|
url@0.10.3, url@~0.10.1:
|
||||||
version "0.11.0"
|
version "0.10.3"
|
||||||
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode "1.3.2"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
querystring "0.2.0"
|
||||||
|
|
||||||
url@~0.10.1:
|
url@^0.11.0:
|
||||||
version "0.10.3"
|
version "0.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
|
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode "1.3.2"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
querystring "0.2.0"
|
||||||
@ -8953,14 +8926,14 @@ uuid@2.0.2:
|
|||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.2.tgz#48bd5698f0677e3c7901a1c46ef15b1643794726"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.2.tgz#48bd5698f0677e3c7901a1c46ef15b1643794726"
|
||||||
|
|
||||||
|
uuid@3.1.0, uuid@^3.0.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
||||||
|
|
||||||
uuid@^2.0.1, uuid@^2.0.3:
|
uuid@^2.0.1, uuid@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
|
||||||
|
|
||||||
uuid@^3.0.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
|
||||||
|
|
||||||
v8flags@^2.0.2:
|
v8flags@^2.0.2:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"
|
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"
|
||||||
@ -9256,6 +9229,19 @@ xml-name-validator@^2.0.1:
|
|||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
|
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
|
||||||
|
|
||||||
|
xml2js@0.4.17:
|
||||||
|
version "0.4.17"
|
||||||
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868"
|
||||||
|
dependencies:
|
||||||
|
sax ">=0.6.0"
|
||||||
|
xmlbuilder "^4.1.0"
|
||||||
|
|
||||||
|
xmlbuilder@4.2.1, xmlbuilder@^4.1.0:
|
||||||
|
version "4.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5"
|
||||||
|
dependencies:
|
||||||
|
lodash "^4.0.0"
|
||||||
|
|
||||||
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
|
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||||
|
Reference in New Issue
Block a user