diff --git a/.eslintrc b/.eslintrc index a1eb5ce4..239991f4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,10 +6,7 @@ "plugin:import/warnings", "plugin:flowtype/recommended" ], - "plugins": [ - "prettier", - "flowtype" - ], + "plugins": ["prettier", "flowtype"], "rules": { "eqeqeq": 2, "no-unused-vars": 2, @@ -22,7 +19,7 @@ "import/no-unresolved": [ "error", { - "ignore": [ "slate-drop-or-paste-images" ] + "ignore": ["slate-drop-or-paste-images"] } ], // Flow @@ -33,14 +30,8 @@ "annotationStyle": "line" } ], - "flowtype/space-after-type-colon": [ - 2, - "always" - ], - "flowtype/space-before-type-colon": [ - 2, - "never" - ], + "flowtype/space-after-type-colon": [2, "always"], + "flowtype/space-before-type-colon": [2, "never"], // Enforce that code is formatted with prettier. "prettier/prettier": [ "error", @@ -65,7 +56,8 @@ "SLACK_REDIRECT_URI": true, "DEPLOYMENT": true, "BASE_URL": true, + "BUGSNAG_KEY": true, "afterAll": true, "Bugsnag": true } -} \ No newline at end of file +} diff --git a/README.md b/README.md index b4b11cf2..84757702 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ 1. Install dependencies with `yarn` 1. Register a Slack app at https://api.slack.com/apps 1. Copy the file `.env.sample` to `.env` and fill out the keys - 1. Run DB migrations `yarn run sequelize -- db:migrate` + 1. Run DB migrations `yarn sequelize -- db:migrate` 1. Start the development server `yarn start` @@ -16,12 +16,12 @@ Sequelize is used to create and run migrations, for example: ``` -yarn run sequelize migration:create -yarn run sequelize db:migrate +yarn sequelize migration:create +yarn sequelize db:migrate ``` Or to run migrations on test database: ``` -yarn run sequelize db:migrate -- --env test +yarn sequelize db:migrate --env test ``` diff --git a/app.json b/app.json index 557124d4..3f446c67 100644 --- a/app.json +++ b/app.json @@ -45,6 +45,9 @@ }, "URL": { "required": true + }, + "BUGSNAG_KEY": { + "required": true } }, "formation": {}, diff --git a/flow-typed/globals.js b/flow-typed/globals.js index c0951f95..37ccbbce 100644 --- a/flow-typed/globals.js +++ b/flow-typed/globals.js @@ -3,5 +3,6 @@ declare var __DEV__: string; declare var SLACK_REDIRECT_URI: string; declare var SLACK_KEY: string; declare var BASE_URL: string; +declare var BUGSNAG_KEY: ?string; declare var DEPLOYMENT: string; declare var Bugsnag: any; diff --git a/frontend/components/DropdownMenu/DropdownMenu.js b/frontend/components/DropdownMenu/DropdownMenu.js index d97f2dc3..949ca308 100644 --- a/frontend/components/DropdownMenu/DropdownMenu.js +++ b/frontend/components/DropdownMenu/DropdownMenu.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { Component } from 'react'; import invariant from 'invariant'; import { observable } from 'mobx'; import { observer } from 'mobx-react'; @@ -10,14 +10,14 @@ import { color } from 'styles/constants'; import { fadeAndScaleIn } from 'styles/animations'; type Props = { - label: React.Element, - onShow?: () => void, + label: React.Element<*>, + onOpen?: () => void, onClose?: () => void, - children?: React.Element, + children?: React.Element<*>, style?: Object, }; -@observer class DropdownMenu extends React.Component { +@observer class DropdownMenu extends Component { props: Props; actionRef: Object; @observable open: boolean = false; @@ -37,7 +37,7 @@ type Props = { this.open = true; this.top = targetRect.bottom - bodyRect.top; this.right = bodyRect.width - targetRect.left - targetRect.width; - if (this.props.onShow) this.props.onShow(); + if (this.props.onOpen) this.props.onOpen(); } }; diff --git a/frontend/components/DropdownMenu/DropdownMenuItem.js b/frontend/components/DropdownMenu/DropdownMenuItem.js index 3ceb8e10..18d824ce 100644 --- a/frontend/components/DropdownMenu/DropdownMenuItem.js +++ b/frontend/components/DropdownMenu/DropdownMenuItem.js @@ -7,7 +7,7 @@ const DropdownMenuItem = ({ onClick, children, }: { - onClick?: () => void, + onClick?: SyntheticEvent => void, children?: React.Element, }) => { return ( @@ -24,11 +24,15 @@ const MenuItem = styled.div` color: ${color.slateDark}; display: flex; - justify-content: space-between; + justify-content: left; align-items: center; cursor: pointer; font-size: 15px; + svg { + margin-right: 8px; + } + a { text-decoration: none; width: 100%; diff --git a/frontend/components/Editor/Editor.js b/frontend/components/Editor/Editor.js index 4a016763..46f55213 100644 --- a/frontend/components/Editor/Editor.js +++ b/frontend/components/Editor/Editor.js @@ -9,6 +9,7 @@ import getDataTransferFiles from 'utils/getDataTransferFiles'; import Flex from 'components/Flex'; import ClickablePadding from './components/ClickablePadding'; import Toolbar from './components/Toolbar'; +import BlockInsert from './components/BlockInsert'; import Placeholder from './components/Placeholder'; import Contents from './components/Contents'; import Markdown from './serializer'; @@ -24,7 +25,7 @@ type Props = { onCancel: Function, onImageUploadStart: Function, onImageUploadStop: Function, - emoji: string, + emoji?: string, readOnly: boolean, }; @@ -172,6 +173,8 @@ type KeyData = { }; render = () => { + const { readOnly, emoji, onSave } = this.props; + return ( -
- +
+ {!readOnly && + } + {!readOnly && + } (this.editor = ref)} placeholder="Start with a title…" bodyPlaceholder="…the rest is your canvas" schema={this.schema} plugins={this.plugins} - emoji={this.props.emoji} + emoji={emoji} state={this.editorState} onKeyDown={this.onKeyDown} onChange={this.onChange} onDocumentChange={this.onDocumentChange} - onSave={this.props.onSave} - readOnly={this.props.readOnly} + onSave={onSave} + readOnly={readOnly} /> @@ -281,6 +291,10 @@ const StyledEditor = styled(Editor)` position: relative; } + a:hover { + text-decoration: ${({ readOnly }) => (readOnly ? 'underline' : 'none')}; + } + li p { display: inline; margin: 0; @@ -322,6 +336,10 @@ const StyledEditor = styled(Editor)` td { padding: 5px 20px 5px 0; } + + b, strong { + font-weight: 600; + } `; export default MarkdownEditor; diff --git a/frontend/components/Editor/components/BlockInsert.js b/frontend/components/Editor/components/BlockInsert.js new file mode 100644 index 00000000..43df6281 --- /dev/null +++ b/frontend/components/Editor/components/BlockInsert.js @@ -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 ( + + + (this.file = ref)} + onChange={this.onChooseImage} + accept="image/*" + /> + } + 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} + /> + + + ); + } +} + +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; + `} +`; diff --git a/frontend/components/Editor/components/HorizontalRule.js b/frontend/components/Editor/components/HorizontalRule.js new file mode 100644 index 00000000..286a5acb --- /dev/null +++ b/frontend/components/Editor/components/HorizontalRule.js @@ -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 ; +} + +const StyledHr = styled.hr` + border-bottom: 1px solid ${props => (props.active ? color.slate : color.slateLight)}; +`; + +export default HorizontalRule; diff --git a/frontend/components/Editor/components/Toolbar/components/FormattingToolbar.js b/frontend/components/Editor/components/Toolbar/components/FormattingToolbar.js index 2b1938bf..c56ef925 100644 --- a/frontend/components/Editor/components/Toolbar/components/FormattingToolbar.js +++ b/frontend/components/Editor/components/Toolbar/components/FormattingToolbar.js @@ -9,7 +9,6 @@ import Heading2Icon from 'components/Icon/Heading2Icon'; import ItalicIcon from 'components/Icon/ItalicIcon'; import LinkIcon from 'components/Icon/LinkIcon'; import StrikethroughIcon from 'components/Icon/StrikethroughIcon'; -import BulletedListIcon from 'components/Icon/BulletedListIcon'; export default class FormattingToolbar extends Component { props: { @@ -95,7 +94,6 @@ export default class FormattingToolbar extends Component { {this.renderMarkButton('deleted', StrikethroughIcon)} {this.renderBlockButton('heading1', Heading1Icon)} {this.renderBlockButton('heading2', Heading2Icon)} - {this.renderBlockButton('bulleted-list', BulletedListIcon)} {this.renderMarkButton('code', CodeIcon)} diff --git a/frontend/components/Editor/insertImage.js b/frontend/components/Editor/insertImage.js index 43e56069..7f59ab5c 100644 --- a/frontend/components/Editor/insertImage.js +++ b/frontend/components/Editor/insertImage.js @@ -15,7 +15,6 @@ export default async function insertImageFile( try { // load the file as a data URL const id = uuid.v4(); - const alt = file.name; const reader = new FileReader(); reader.addEventListener('load', () => { const src = reader.result; @@ -25,7 +24,7 @@ export default async function insertImageFile( .insertBlock({ type: 'image', isVoid: true, - data: { src, alt, id, loading: true }, + data: { src, id, loading: true }, }) .apply(); editor.onChange(state); @@ -46,7 +45,7 @@ export default async function insertImageFile( ); return finalTransform.setNodeByKey(placeholder.key, { - data: { src, alt, loading: false }, + data: { src, loading: false }, }); } catch (err) { throw err; diff --git a/frontend/components/Editor/plugins.js b/frontend/components/Editor/plugins.js index c487df2a..30688ea1 100644 --- a/frontend/components/Editor/plugins.js +++ b/frontend/components/Editor/plugins.js @@ -1,11 +1,11 @@ // @flow import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images'; import PasteLinkify from 'slate-paste-linkify'; -import EditList from 'slate-edit-list'; import CollapseOnEscape from 'slate-collapse-on-escape'; import TrailingBlock from 'slate-trailing-block'; import EditCode from 'slate-edit-code'; import Prism from 'slate-prism'; +import EditList from './plugins/EditList'; import KeyboardShortcuts from './plugins/KeyboardShortcuts'; import MarkdownShortcuts from './plugins/MarkdownShortcuts'; import insertImage from './insertImage'; @@ -35,10 +35,7 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { ); }, }), - EditList({ - types: ['ordered-list', 'bulleted-list', 'todo-list'], - typeItem: 'list-item', - }), + EditList, EditCode({ onlyIn: onlyInCode, containerType: 'code', diff --git a/frontend/components/Editor/plugins/EditList.js b/frontend/components/Editor/plugins/EditList.js new file mode 100644 index 00000000..7bef46f4 --- /dev/null +++ b/frontend/components/Editor/plugins/EditList.js @@ -0,0 +1,7 @@ +// @flow +import EditList from 'slate-edit-list'; + +export default EditList({ + types: ['ordered-list', 'bulleted-list', 'todo-list'], + typeItem: 'list-item', +}); diff --git a/frontend/components/Editor/plugins/MarkdownShortcuts.js b/frontend/components/Editor/plugins/MarkdownShortcuts.js index 0dd92415..de530b00 100644 --- a/frontend/components/Editor/plugins/MarkdownShortcuts.js +++ b/frontend/components/Editor/plugins/MarkdownShortcuts.js @@ -112,19 +112,17 @@ export default function MarkdownShortcuts() { if (chars === '--') { ev.preventDefault(); - const transform = state + return state .transform() .extendToStartOf(startBlock) .delete() .setBlock({ type: 'horizontal-rule', isVoid: true, - }); - state = transform + }) .collapseToStartOfNextBlock() .insertBlock('paragraph') .apply(); - return state; } }, diff --git a/frontend/components/Editor/schema.js b/frontend/components/Editor/schema.js index e915b636..2010e4c2 100644 --- a/frontend/components/Editor/schema.js +++ b/frontend/components/Editor/schema.js @@ -1,6 +1,7 @@ // @flow import React from 'react'; import Code from './components/Code'; +import HorizontalRule from './components/HorizontalRule'; import InlineCode from './components/InlineCode'; import Image from './components/Image'; import Link from './components/Link'; @@ -33,7 +34,7 @@ const createSchema = () => { 'block-quote': (props: Props) => (
{props.children}
), - 'horizontal-rule': (props: Props) =>
, + 'horizontal-rule': HorizontalRule, 'bulleted-list': (props: Props) =>
    {props.children}
, 'ordered-list': (props: Props) =>
    {props.children}
, 'todo-list': (props: Props) => {props.children}, diff --git a/frontend/components/Empty/Empty.js b/frontend/components/Empty/Empty.js new file mode 100644 index 00000000..822d990a --- /dev/null +++ b/frontend/components/Empty/Empty.js @@ -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 {children}; +}; + +const Container = styled.div` + display: flex; + color: ${color.slate}; + text-align: center; +`; + +export default Empty; diff --git a/frontend/components/Empty/index.js b/frontend/components/Empty/index.js new file mode 100644 index 00000000..5c3c3a07 --- /dev/null +++ b/frontend/components/Empty/index.js @@ -0,0 +1,3 @@ +// @flow +import Empty from './Empty'; +export default Empty; diff --git a/frontend/components/HtmlContent/HtmlContent.js b/frontend/components/HtmlContent/HtmlContent.js deleted file mode 100644 index 8f670b9a..00000000 --- a/frontend/components/HtmlContent/HtmlContent.js +++ /dev/null @@ -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; diff --git a/frontend/components/HtmlContent/index.js b/frontend/components/HtmlContent/index.js deleted file mode 100644 index 2ecb47b9..00000000 --- a/frontend/components/HtmlContent/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import HtmlContent from './HtmlContent'; -export default HtmlContent; diff --git a/frontend/components/Layout/components/SidebarCollections.js b/frontend/components/Layout/components/SidebarCollections.js index 8bd83eb5..a1cf5aa2 100644 --- a/frontend/components/Layout/components/SidebarCollections.js +++ b/frontend/components/Layout/components/SidebarCollections.js @@ -95,7 +95,7 @@ type Props = { (this.menuOpen = true)} + onOpen={() => (this.menuOpen = true)} onClose={() => (this.menuOpen = false)} onImport={this.handleImport} open={this.menuOpen} diff --git a/frontend/components/Layout/components/SidebarLink.js b/frontend/components/Layout/components/SidebarLink.js index 08b7f0fc..b10bf94b 100644 --- a/frontend/components/Layout/components/SidebarLink.js +++ b/frontend/components/Layout/components/SidebarLink.js @@ -16,7 +16,7 @@ const activeStyle = { const StyleableDiv = props =>
; const styleComponent = component => styled(component)` - display: block; + display: flex; width: 100%; overflow: hidden; text-overflow: ellipsis; @@ -42,7 +42,7 @@ function SidebarLink(props: Object) { {props.hasChildren && } - {props.children} + {props.children} ); @@ -62,4 +62,8 @@ const StyledChevron = styled(ChevronIcon)` } `; +const Content = styled.div` + width: 100%; +`; + export default SidebarLink; diff --git a/frontend/index.js b/frontend/index.js index e6784ea7..12e49fe7 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -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(); } diff --git a/frontend/menus/BlockMenu.js b/frontend/menus/BlockMenu.js new file mode 100644 index 00000000..5cd3e611 --- /dev/null +++ b/frontend/menus/BlockMenu.js @@ -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 ( + + + Add images + + + Start list + + + Start checklist + + + Add break + + + ); + } +} + +export default BlockMenu; diff --git a/frontend/menus/CollectionMenu.js b/frontend/menus/CollectionMenu.js index 6b57bf09..fc29530b 100644 --- a/frontend/menus/CollectionMenu.js +++ b/frontend/menus/CollectionMenu.js @@ -12,7 +12,7 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; @observer class CollectionMenu extends Component { props: { label?: React$Element, - onShow?: () => void, + onOpen?: () => void, onClose?: () => void, onImport?: () => void, history: Object, @@ -36,13 +36,13 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; }; render() { - const { collection, label, onShow, onClose, onImport } = this.props; + const { collection, label, onOpen, onClose, onImport } = this.props; const { allowDelete } = collection; return ( } - onShow={onShow} + onOpen={onOpen} onClose={onClose} > {collection && diff --git a/frontend/scenes/Flatpage/Flatpage.js b/frontend/scenes/Flatpage/Flatpage.js index b2353808..58bff50e 100644 --- a/frontend/scenes/Flatpage/Flatpage.js +++ b/frontend/scenes/Flatpage/Flatpage.js @@ -2,30 +2,31 @@ import React from 'react'; import { observer } from 'mobx-react'; import CenteredContent from 'components/CenteredContent'; -import HtmlContent from 'components/HtmlContent'; +import Editor from 'components/Editor'; import PageTitle from 'components/PageTitle'; -import { convertToMarkdown } from 'utils/markdown'; - type Props = { title: string, content: string, }; -@observer class Flatpage extends React.Component { - props: Props; +const Flatpage = observer((props: Props) => { + const { title, content } = props; - render() { - const { title, content } = this.props; - const htmlContent = convertToMarkdown(content); - - return ( - - - - - ); - } -} + return ( + + + {}} + onSave={() => {}} + onCancel={() => {}} + onImageUploadStart={() => {}} + onImageUploadStop={() => {}} + readOnly + /> + + ); +}); export default Flatpage; diff --git a/frontend/scenes/Search/Search.js b/frontend/scenes/Search/Search.js index 71e0b2c8..9050d056 100644 --- a/frontend/scenes/Search/Search.js +++ b/frontend/scenes/Search/Search.js @@ -12,6 +12,7 @@ import { searchUrl } from 'utils/routeHelpers'; import styled from 'styled-components'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; +import Empty from 'components/Empty'; import Flex from 'components/Flex'; import CenteredContent from 'components/CenteredContent'; import LoadingIndicator from 'components/LoadingIndicator'; @@ -57,7 +58,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` firstDocument: HTMLElement; props: Props; - @observable resultIds: Array = []; // Document IDs + @observable resultIds: string[] = []; // Document IDs @observable searchTerm: ?string = null; @observable isFetching = false; @@ -131,18 +132,19 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` } render() { - const { documents } = this.props; + const { documents, notFound } = this.props; const query = this.props.match.params.query; const hasResults = this.resultIds.length > 0; + const showEmpty = !this.isFetching && this.searchTerm && !hasResults; return ( {this.isFetching && } - {this.props.notFound && + {notFound &&

Not Found

-

We're unable to find the page you're accessing.

+

We’re unable to find the page you’re accessing.

} + {showEmpty && Oop, no matching documents.} { + setRef = (ref: HTMLInputElement) => { this.input = ref; }; render() { return ( - - @@ -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; diff --git a/frontend/scenes/SlackAuth/SlackAuth.js b/frontend/scenes/SlackAuth/SlackAuth.js index 21aa6bea..2846b75a 100644 --- a/frontend/scenes/SlackAuth/SlackAuth.js +++ b/frontend/scenes/SlackAuth/SlackAuth.js @@ -2,6 +2,7 @@ import React from 'react'; import { Redirect } from 'react-router'; import queryString from 'query-string'; +import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; import { client } from 'utils/ApiClient'; @@ -12,17 +13,15 @@ type Props = { location: Object, }; -type State = { - redirectTo: string, -}; - @observer class SlackAuth extends React.Component { props: Props; - state: State; - state = {}; + @observable redirectTo: string; - // $FlowIssue Flow doesn't like async lifecycle components https://github.com/facebook/flow/issues/1803 - async componentDidMount(): void { + componentDidMount() { + this.redirect(); + } + + async redirect() { const { error, code, state } = queryString.parse( this.props.location.search ); @@ -30,18 +29,18 @@ type State = { if (error) { if (error === 'access_denied') { // User selected "Deny" access on Slack OAuth - this.setState({ redirectTo: '/dashboard' }); + this.redirectTo = '/dashboard'; } else { - this.setState({ redirectTo: '/auth/error' }); + this.redirectTo = '/auth/error'; } } else { if (this.props.location.pathname === '/auth/slack/commands') { // User adding webhook integrations try { await client.post('/auth.slackCommands', { code }); - this.setState({ redirectTo: '/dashboard' }); + this.redirectTo = '/dashboard'; } catch (e) { - this.setState({ redirectTo: '/auth/error' }); + this.redirectTo = '/auth/error'; } } else { // Regular Slack authentication @@ -50,18 +49,15 @@ type State = { const { success } = await this.props.auth.authWithSlack(code, state); success - ? this.setState({ redirectTo: redirectTo || '/dashboard' }) - : this.setState({ redirectTo: '/auth/error' }); + ? (this.redirectTo = redirectTo || '/dashboard') + : (this.redirectTo = '/auth/error'); } } } render() { - return ( -
- {this.state.redirectTo && } -
- ); + if (this.redirectTo) return ; + return null; } } diff --git a/frontend/scenes/Starred/Starred.js b/frontend/scenes/Starred/Starred.js index 6704045c..c2abfbb0 100644 --- a/frontend/scenes/Starred/Starred.js +++ b/frontend/scenes/Starred/Starred.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { observer, inject } from 'mobx-react'; import CenteredContent from 'components/CenteredContent'; import { ListPlaceholder } from 'components/LoadingPlaceholder'; +import Empty from 'components/Empty'; import PageTitle from 'components/PageTitle'; import DocumentList from 'components/DocumentList'; import DocumentsStore from 'stores/DocumentsStore'; @@ -17,14 +18,17 @@ import DocumentsStore from 'stores/DocumentsStore'; } render() { - const { isLoaded, isFetching } = this.props.documents; + const { isLoaded, isFetching, starred } = this.props.documents; + const showLoading = !isLoaded && isFetching; + const showEmpty = isLoaded && !starred.length; return (

Starred

- {!isLoaded && isFetching && } - + {showLoading && } + {showEmpty && No starred documents yet.} +
); } diff --git a/frontend/styles/animations.js b/frontend/styles/animations.js index 257528b5..e6a9d5c2 100644 --- a/frontend/styles/animations.js +++ b/frontend/styles/animations.js @@ -1,6 +1,11 @@ // @flow import { keyframes } from 'styled-components'; +export const fadeIn = keyframes` + from { opacity: 0; } + to { opacity: 1; } +`; + export const fadeAndScaleIn = keyframes` from { opacity: 0; diff --git a/frontend/utils/emoji-mapping.json b/frontend/utils/emoji-mapping.json deleted file mode 100644 index b965bfac..00000000 --- a/frontend/utils/emoji-mapping.json +++ /dev/null @@ -1 +0,0 @@ -{"100":"💯","1234":"🔢","grinning":"😀","grimacing":"😬","grin":"😁","joy":"😂","smiley":"😃","smile":"😄","sweat_smile":"😅","laughing":"😆","innocent":"😇","wink":"😉","blush":"😊","slightly_smiling":"🙂","upside_down":"🙃","relaxed":"☺️","yum":"😋","relieved":"😌","heart_eyes":"😍","kissing_heart":"😘","kissing":"😗","kissing_smiling_eyes":"😙","kissing_closed_eyes":"😚","stuck_out_tongue_winking_eye":"😜","stuck_out_tongue_closed_eyes":"😝","stuck_out_tongue":"😛","money_mouth":"🤑","nerd":"🤓","sunglasses":"😎","hugging":"🤗","smirk":"😏","no_mouth":"😶","neutral_face":"😐","expressionless":"😑","unamused":"😒","rolling_eyes":"🙄","thinking":"🤔","flushed":"😳","disappointed":"😞","worried":"😟","angry":"😠","rage":"😡","pensive":"😔","confused":"😕","slightly_sad":"🙁","white_frowning":"☹","persevere":"😣","confounded":"😖","tired_face":"😫","weary":"😩","triumph":"😤","open_mouth":"😮","scream":"😱","fearful":"😨","cold_sweat":"😰","hushed":"😯","frowning":"😦","anguished":"😧","cry":"😢","disappointed_relieved":"😥","sleepy":"😪","sweat":"😓","sob":"😭","dizzy_face":"😵","astonished":"😲","zipper_mouth":"🤐","mask":"😷","thermometer_face":"🤒","bandage_face":"🤕","sleeping":"😴","zzz":"💤","hankey":"💩","smiling_imp":"😈","imp":"👿","japanese_ogre":"👹","japanese_goblin":"👺","skull":"💀","ghost":"👻","alien":"👽","robot":"🤖","smiley_cat":"😺","smile_cat":"😸","joy_cat":"😹","heart_eyes_cat":"😻","smirk_cat":"😼","kissing_cat":"😽","scream_cat":"🙀","crying_cat_face":"😿","pouting_cat":"😾","raised_hands":"🙌","clap":"👏","wave":"👋","+1":"👍","-1":"👎","facepunch":"👊","fist":"✊","v":"✌️","ok_hand":"👌","hand":"✋","open_hands":"👐","muscle":"💪","pray":"🙏","point_up":"☝️","point_up_2":"👆","point_down":"👇","point_left":"👈","point_right":"👉","middle_finger":"🖕","splayed_fingers":"🖐","sign_of_horns":"🤘","vulcan_salute":"🖖","writing_hand":"✍","nail_care":"💅","lips":"👄","tongue":"👅","ear":"👂","nose":"👃","eye":"👁","eyes":"👀","bust_in_silhouette":"👤","busts_in_silhouette":"👥","speaking_head":"🗣","baby":"👶","boy":"👦","girl":"👧","man":"👨","woman":"👩","person_with_blond_hair":"👱","older_man":"👴","older_woman":"👵","man_with_gua_pi_mao":"👲","man_with_turban":"👳","cop":"👮","construction_worker":"👷","guardsman":"💂","sleuth":"🕵","santa":"🎅","angel":"👼","princess":"👸","bride_with_veil":"👰","runner":"🏃","walking":"🚶","dancer":"💃","dancers":"👯","couple":"👫","two_men_holding_hands":"👬","two_women_holding_hands":"👭","bow":"🙇","information_desk_person":"💁","no_good":"🙅","ok_woman":"🙆","raising_hand":"🙋","person_with_pouting_face":"🙎","person_frowning":"🙍","haircut":"💇","massage":"💆","couple_with_heart":"💑","female_couple_with_heart":"👩‍❤️‍👩","male_couple_with_heart":"👨‍❤️‍👨","couplekiss":"💏","female_couplekiss":"👩‍❤️‍💋‍👩","male_couplekiss":"👨‍❤️‍💋‍👨","family":"👪","family_man_woman_girl":"👨‍👩‍👧","family_man_woman_girl_boy":"👨‍👩‍👧‍👦","family_man_woman_boys":"👨‍👩‍👦‍👦","family_man_woman_girls":"👨‍👩‍👧‍👧","family_women_boy":"👩‍👩‍👦","family_women_girl":"👩‍👩‍👧","family_women_girl_boy":"👩‍👩‍👧‍👦","family_women_boys":"👩‍👩‍👦‍👦","family_women_girls":"👩‍👩‍👧‍👧","family_men_boy":"👨‍👨‍👦","family_men_girl":"👨‍👨‍👧","family_men_girl_boy":"👨‍👨‍👧‍👦","family_men_boys":"👨‍👨‍👦‍👦","family_men_girls":"👨‍👨‍👧‍👧","womans_clothes":"👚","shirt":"👕","jeans":"👖","necktie":"👔","dress":"👗","bikini":"👙","kimono":"👘","lipstick":"💄","kiss":"💋","footprints":"👣","high_heel":"👠","sandal":"👡","boot":"👢","mans_shoe":"👞","athletic_shoe":"👟","womans_hat":"👒","tophat":"🎩","helmet":"⛑","mortar_board":"🎓","crown":"👑","school_satchel":"🎒","pouch":"👝","purse":"👛","handbag":"👜","briefcase":"💼","eyeglasses":"👓","dark_sunglasses":"🕶","ring":"💍","closed_umbrella":"🌂","dog":"🐶","cat":"🐱","mouse":"🐭","hamster":"🐹","rabbit":"🐰","bear":"🐻","panda_face":"🐼","koala":"🐨","tiger":"🐯","lion_face":"🦁","cow":"🐮","pig":"🐷","pig_nose":"🐽","frog":"🐸","octopus":"🐙","monkey_face":"🐵","see_no_evil":"🙈","hear_no_evil":"🙉","speak_no_evil":"🙊","monkey":"🐒","chicken":"🐔","penguin":"🐧","bird":"🐦","baby_chick":"🐤","hatching_chick":"🐣","hatched_chick":"🐥","wolf":"🐺","boar":"🐗","horse":"🐴","unicorn_face":"🦄","bee":"🐝","bug":"🐛","snail":"🐌","beetle":"🐞","ant":"🐜","spider":"🕷","scorpion":"🦂","crab":"🦀","snake":"🐍","turtle":"🐢","tropical_fish":"🐠","fish":"🐟","blowfish":"🐡","dolphin":"🐬","whale":"🐳","whale2":"🐋","crocodile":"🐊","leopard":"🐆","tiger2":"🐅","water_buffalo":"🐃","ox":"🐂","cow2":"🐄","dromedary_camel":"🐪","camel":"🐫","elephant":"🐘","goat":"🐐","ram":"🐏","sheep":"🐑","racehorse":"🐎","pig2":"🐖","rat":"🐀","mouse2":"🐁","rooster":"🐓","turkey":"🦃","dove_of_peace":"🕊","dog2":"🐕","poodle":"🐩","cat2":"🐈","rabbit2":"🐇","chipmunk":"🐿","feet":"🐾","dragon":"🐉","dragon_face":"🐲","cactus":"🌵","christmas_tree":"🎄","evergreen_tree":"🌲","deciduous_tree":"🌳","palm_tree":"🌴","seedling":"🌱","herb":"🌿","shamrock":"☘","four_leaf_clover":"🍀","bamboo":"🎍","tanabata_tree":"🎋","leaves":"🍃","fallen_leaf":"🍂","maple_leaf":"🍁","ear_of_rice":"🌾","hibiscus":"🌺","sunflower":"🌻","rose":"🌹","tulip":"🌷","blossom":"🌼","cherry_blossom":"🌸","bouquet":"💐","mushroom":"🍄","chestnut":"🌰","jack_o_lantern":"🎃","shell":"🐚","spider_web":"🕸","earth_americas":"🌎","earth_africa":"🌍","earth_asia":"🌏","full_moon":"🌕","waning_gibbous_moon":"🌖","last_quarter_moon":"🌗","waning_crescent_moon":"🌘","new_moon":"🌑","waxing_crescent_moon":"🌒","first_quarter_moon":"🌓","moon":"🌔","new_moon_with_face":"🌚","full_moon_with_face":"🌝","first_quarter_moon_with_face":"🌛","last_quarter_moon_with_face":"🌜","sun_with_face":"🌞","crescent_moon":"🌙","star":"⭐","star2":"🌟","dizzy":"💫","sparkles":"✨","comet":"☄","sunny":"☀️","white_sun_with_small_cloud":"🌤","partly_sunny":"⛅","white_sun_behind_cloud":"🌥","white_sun_behind_cloud_with_rain":"🌦","cloud":"☁️","cloud_with_rain":"🌧","thunder_cloud_and_rain":"⛈","cloud_with_lightning":"🌩","zap":"⚡","fire":"🔥","boom":"💥","snowflake":"❄️","cloud_with_snow":"🌨","snowman":"⛄","snowman_with_snow":"☃","wind_blowing_face":"🌬","dash":"💨","cloud_with_tornado":"🌪","fog":"🌫","umbrella_without_rain":"☂","umbrella":"☔","droplet":"💧","sweat_drops":"💦","ocean":"🌊","green_apple":"🍏","apple":"🍎","pear":"🍐","tangerine":"🍊","lemon":"🍋","banana":"🍌","watermelon":"🍉","grapes":"🍇","strawberry":"🍓","melon":"🍈","cherries":"🍒","peach":"🍑","pineapple":"🍍","tomato":"🍅","eggplant":"🍆","hot_pepper":"🌶","corn":"🌽","sweet_potato":"🍠","honey_pot":"🍯","bread":"🍞","cheese_wedge":"🧀","poultry_leg":"🍗","meat_on_bone":"🍖","fried_shrimp":"🍤","egg":"🍳","hamburger":"🍔","fries":"🍟","hot_dog":"🌭","pizza":"🍕","spaghetti":"🍝","taco":"🌮","burrito":"🌯","ramen":"🍜","stew":"🍲","fish_cake":"🍥","sushi":"🍣","bento":"🍱","curry":"🍛","rice_ball":"🍙","rice":"🍚","rice_cracker":"🍘","oden":"🍢","dango":"🍡","shaved_ice":"🍧","ice_cream":"🍨","icecream":"🍦","cake":"🍰","birthday":"🎂","custard":"🍮","candy":"🍬","lollipop":"🍭","chocolate_bar":"🍫","popcorn":"🍿","doughnut":"🍩","cookie":"🍪","beer":"🍺","beers":"🍻","wine_glass":"🍷","cocktail":"🍸","tropical_drink":"🍹","bottle":"🍾","sake":"🍶","tea":"🍵","coffee":"☕","baby_bottle":"🍼","fork_and_knife":"🍴","fork_and_knife_with_plate":"🍽","soccer":"⚽","basketball":"🏀","football":"🏈","baseball":"⚾","tennis":"🎾","volleyball":"🏐","rugby_football":"🏉","8ball":"🎱","golf":"⛳","golfer":"🏌","table_tennis":"🏓","badminton":"🏸","ice_hockey":"🏒","field_hockey":"🏑","cricket":"🏏","ski":"🎿","skier":"⛷","snowboarder":"🏂","ice_skate":"⛸","bow_and_arrow":"🏹","fishing_pole_and_fish":"🎣","rowboat":"🚣","swimmer":"🏊","surfer":"🏄","bath":"🛀","person_with_ball":"⛹","weight_lifter":"🏋","bicyclist":"🚴","mountain_bicyclist":"🚵","horse_racing":"🏇","man_levitating":"🕴","trophy":"🏆","running_shirt_with_sash":"🎽","sports_medal":"🏅","military_medal":"🎖","reminder_ribbon":"🎗","rosette":"🏵","ticket":"🎫","admission_ticket":"🎟","performing_arts":"🎭","art":"🎨","circus_tent":"🎪","microphone":"🎤","headphones":"🎧","musical_score":"🎼","musical_keyboard":"🎹","saxophone":"🎷","trumpet":"🎺","guitar":"🎸","violin":"🎻","clapper":"🎬","video_game":"🎮","space_invader":"👾","dart":"🎯","game_die":"🎲","slot_machine":"🎰","bowling":"🎳","car":"🚗","taxi":"🚕","blue_car":"🚙","bus":"🚌","trolleybus":"🚎","racing_car":"🏎","police_car":"🚓","ambulance":"🚑","fire_engine":"🚒","minibus":"🚐","truck":"🚚","articulated_lorry":"🚛","tractor":"🚜","racing_motorcycle":"🏍","bike":"🚲","rotating_light":"🚨","oncoming_police_car":"🚔","oncoming_bus":"🚍","oncoming_automobile":"🚘","oncoming_taxi":"🚖","aerial_tramway":"🚡","mountain_cableway":"🚠","suspension_railway":"🚟","railway_car":"🚃","train":"🚋","monorail":"🚝","bullettrain_side":"🚄","bullettrain_front":"🚅","light_rail":"🚈","mountain_railway":"🚞","steam_locomotive":"🚂","train2":"🚆","metro":"🚇","tram":"🚊","station":"🚉","helicopter":"🚁","small_airplane":"🛩","airplane":"✈️","airplane_departure":"🛫","airplane_arrival":"🛬","boat":"⛵","motor_boat":"🛥","speedboat":"🚤","ferry":"⛴","passenger_ship":"🛳","rocket":"🚀","artificial_satellite":"🛰","seat":"💺","anchor":"⚓","construction":"🚧","fuelpump":"⛽","busstop":"🚏","vertical_traffic_light":"🚦","traffic_light":"🚥","checkered_flag":"🏁","ship":"🚢","ferris_wheel":"🎡","roller_coaster":"🎢","carousel_horse":"🎠","building_construction":"🏗","foggy":"🌁","tokyo_tower":"🗼","factory":"🏭","fountain":"⛲","rice_scene":"🎑","mountain":"⛰","snow_capped_mountain":"🏔","mount_fuji":"🗻","volcano":"🌋","japan":"🗾","camping":"🏕","tent":"⛺","national_park":"🏞","motorway":"🛣","railway_track":"🛤","sunrise":"🌅","sunrise_over_mountains":"🌄","desert":"🏜","beach_with_umbrella":"🏖","desert_island":"🏝","city_sunrise":"🌇","city_sunset":"🌆","cityscape":"🏙","night_with_stars":"🌃","bridge_at_night":"🌉","milky_way":"🌌","stars":"🌠","sparkler":"🎇","fireworks":"🎆","rainbow":"🌈","buildings":"🏘","european_castle":"🏰","japanese_castle":"🏯","stadium":"🏟","statue_of_liberty":"🗽","house":"🏠","house_with_garden":"🏡","derelict_house":"🏚","office":"🏢","department_store":"🏬","post_office":"🏣","european_post_office":"🏤","hospital":"🏥","bank":"🏦","hotel":"🏨","convenience_store":"🏪","school":"🏫","love_hotel":"🏩","wedding":"💒","museum":"🏛","church":"⛪","mosque":"🕌","synagogue":"🕍","kaaba":"🕋","shinto_shrine":"⛩","watch":"⌚","iphone":"📱","calling":"📲","computer":"💻","keyboard":"⌨","desktop_computer":"🖥","printer":"🖨","three_button_mouse":"🖱","trackball":"🖲","joystick":"🕹","compression":"🗜","minidisc":"💽","floppy_disk":"💾","cd":"💿","dvd":"📀","vhs":"📼","camera":"📷","camera_with_flash":"📸","video_camera":"📹","movie_camera":"🎥","film_projector":"📽","film_frames":"🎞","telephone_receiver":"📞","phone":"☎️","pager":"📟","fax":"📠","tv":"📺","radio":"📻","studio_microphone":"🎙","level_slider":"🎚","control_knobs":"🎛","stopwatch":"⏱","timer_clock":"⏲","alarm_clock":"⏰","mantelpiece_clock":"🕰","hourglass_flowing_sand":"⏳","hourglass":"⌛","satellite":"📡","battery":"🔋","electric_plug":"🔌","bulb":"💡","flashlight":"🔦","candle":"🕯","wastebasket":"🗑","oil_drum":"🛢","money_with_wings":"💸","dollar":"💵","yen":"💴","euro":"💶","pound":"💷","moneybag":"💰","credit_card":"💳","gem":"💎","scales":"⚖","wrench":"🔧","hammer":"🔨","hammer_and_pick":"⚒","hammer_and_wrench":"🛠","pick":"⛏","nut_and_bolt":"🔩","gear":"⚙","chains":"⛓","gun":"🔫","bomb":"💣","hocho":"🔪","dagger_knife":"🗡","crossed_swords":"⚔","shield":"🛡","smoking":"🚬","skull_and_crossbones":"☠","coffin":"⚰","funeral_urn":"⚱","amphora":"🏺","crystal_ball":"🔮","prayer_beads":"📿","barber":"💈","alembic":"⚗","telescope":"🔭","microscope":"🔬","hole":"🕳","pill":"💊","syringe":"💉","thermometer":"🌡","label":"🏷","bookmark":"🔖","toilet":"🚽","shower":"🚿","bathtub":"🛁","key":"🔑","old_key":"🗝","couch_and_lamp":"🛋","sleeping_accommodation":"🛌","bed":"🛏","door":"🚪","bellhop_bell":"🛎","frame_with_picture":"🖼","world_map":"🗺","umbrella_on_ground":"⛱","moyai":"🗿","shopping_bags":"🛍","balloon":"🎈","flags":"🎏","ribbon":"🎀","gift":"🎁","confetti_ball":"🎊","tada":"🎉","dolls":"🎎","wind_chime":"🎐","crossed_flags":"🎌","izakaya_lantern":"🏮","envelope":"✉️","envelope_with_arrow":"📩","incoming_envelope":"📨","email":"📧","love_letter":"💌","postbox":"📮","mailbox_closed":"📪","mailbox":"📫","mailbox_with_mail":"📬","mailbox_with_no_mail":"📭","package":"📦","postal_horn":"📯","inbox_tray":"📥","outbox_tray":"📤","scroll":"📜","page_with_curl":"📃","bookmark_tabs":"📑","bar_chart":"📊","chart_with_upwards_trend":"📈","chart_with_downwards_trend":"📉","page_facing_up":"📄","date":"📅","calendar":"📆","spiral_calendar_pad":"🗓","card_index":"📇","card_file_box":"🗃","ballot_box_with_ballot":"🗳","file_cabinet":"🗄","clipboard":"📋","spiral_note_pad":"🗒","file_folder":"📁","open_file_folder":"📂","card_index_dividers":"🗂","rolled_up_newspaper":"🗞","newspaper":"📰","notebook":"📓","closed_book":"📕","green_book":"📗","blue_book":"📘","orange_book":"📙","notebook_with_decorative_cover":"📔","ledger":"📒","books":"📚","book":"📖","link":"🔗","paperclip":"📎","linked_paperclips":"🖇","scissors":"✂️","triangular_ruler":"📐","straight_ruler":"📏","pushpin":"📌","round_pushpin":"📍","triangular_flag_on_post":"🚩","waving_white_flag":"🏳","waving_black_flag":"🏴","closed_lock_with_key":"🔐","lock":"🔒","unlock":"🔓","lock_with_ink_pen":"🔏","lower_left_ballpoint_pen":"🖊","lower_left_fountain_pen":"🖋","black_nib":"✒️","memo":"📝","pencil2":"✏️","lower_left_crayon":"🖍","lower_left_paintbrush":"🖌","mag":"🔍","mag_right":"🔎","heart":"❤️","yellow_heart":"💛","green_heart":"💚","blue_heart":"💙","purple_heart":"💜","broken_heart":"💔","heavy_heart_exclamation_mark_ornament":"❣","two_hearts":"💕","revolving_hearts":"💞","heartbeat":"💓","heartpulse":"💗","sparkling_heart":"💖","cupid":"💘","gift_heart":"💝","heart_decoration":"💟","peace_symbol":"☮","latin_cross":"✝","star_and_crescent":"☪","om_symbol":"🕉","wheel_of_dharma":"☸","star_of_david":"✡","six_pointed_star":"🔯","menorah_with_nine_branches":"🕎","yin_yang":"☯","orthodox_cross":"☦","place_of_worship":"🛐","ophiuchus":"⛎","aries":"♈","taurus":"♉","gemini":"♊","cancer":"♋","leo":"♌","virgo":"♍","libra":"♎","scorpius":"♏","sagittarius":"♐","capricorn":"♑","aquarius":"♒","pisces":"♓","id":"🆔","atom_symbol":"⚛","u7a7a":"🈳","u5272":"🈹","radioactive_sign":"☢","biohazard_sign":"☣","mobile_phone_off":"📴","vibration_mode":"📳","u6709":"🈶","u7121":"🈚","u7533":"🈸","u55b6":"🈺","u6708":"🈷️","eight_pointed_black_star":"✴️","vs":"🆚","accept":"🉑","white_flower":"💮","ideograph_advantage":"🉐","secret":"㊙️","congratulations":"㊗️","u5408":"🈴","u6e80":"🈵","u7981":"🈲","a":"🅰️","b":"🅱️","ab":"🆎","cl":"🆑","o2":"🅾️","sos":"🆘","no_entry":"⛔","name_badge":"📛","no_entry_sign":"🚫","x":"❌","o":"⭕","anger":"💢","hotsprings":"♨️","no_pedestrians":"🚷","do_not_litter":"🚯","no_bicycles":"🚳","non-potable_water":"🚱","underage":"🔞","no_mobile_phones":"📵","exclamation":"❗","grey_exclamation":"❕","question":"❓","grey_question":"❔","bangbang":"‼️","interrobang":"⁉️","low_brightness":"🔅","high_brightness":"🔆","trident":"🔱","fleur_de_lis":"⚜","part_alternation_mark":"〽️","warning":"⚠️","children_crossing":"🚸","beginner":"🔰","recycle":"♻️","u6307":"🈯","chart":"💹","sparkle":"❇️","eight_spoked_asterisk":"✳️","negative_squared_cross_mark":"❎","white_check_mark":"✅","diamond_shape_with_a_dot_inside":"💠","cyclone":"🌀","loop":"➿","globe_with_meridians":"🌐","m":"Ⓜ️","atm":"🏧","sa":"🈂️","passport_control":"🛂","customs":"🛃","baggage_claim":"🛄","left_luggage":"🛅","wheelchair":"♿","no_smoking":"🚭","wc":"🚾","parking":"🅿️","potable_water":"🚰","mens":"🚹","womens":"🚺","baby_symbol":"🚼","restroom":"🚻","put_litter_in_its_place":"🚮","cinema":"🎦","signal_strength":"📶","koko":"🈁","ng":"🆖","ok":"🆗","up":"🆙","cool":"🆒","new":"🆕","free":"🆓","zero":"0️⃣","one":"1️⃣","two":"2️⃣","three":"3️⃣","four":"4️⃣","five":"5️⃣","six":"6️⃣","seven":"7️⃣","eight":"8️⃣","nine":"9️⃣","keycap_ten":"🔟","keycap_star":"*⃣","arrow_forward":"▶️","double_vertical_bar":"⏸","black_right_pointing_triangle_with_double_vertical_bar":"⏭","black_square_for_stop":"⏹","black_circle_for_record":"⏺","black_right_pointing_double_triangle_with_vertical_bar":"⏯","black_left_pointing_double_triangle_with_vertical_bar":"⏮","fast_forward":"⏩","rewind":"⏪","twisted_rightwards_arrows":"🔀","repeat":"🔁","repeat_one":"🔂","arrow_backward":"◀️","arrow_up_small":"🔼","arrow_down_small":"🔽","arrow_double_up":"⏫","arrow_double_down":"⏬","arrow_right":"➡️","arrow_left":"⬅️","arrow_up":"⬆️","arrow_down":"⬇️","arrow_upper_right":"↗️","arrow_lower_right":"↘️","arrow_lower_left":"↙️","arrow_upper_left":"↖️","arrow_up_down":"↕️","left_right_arrow":"↔️","arrows_counterclockwise":"🔄","arrow_right_hook":"↪️","leftwards_arrow_with_hook":"↩️","arrow_heading_up":"⤴️","arrow_heading_down":"⤵️","hash":"#️⃣","information_source":"ℹ️","abc":"🔤","abcd":"🔡","capital_abcd":"🔠","symbols":"🔣","musical_note":"🎵","notes":"🎶","wavy_dash":"〰️","curly_loop":"➰","heavy_check_mark":"✔️","arrows_clockwise":"🔃","heavy_plus_sign":"➕","heavy_minus_sign":"➖","heavy_division_sign":"➗","heavy_multiplication_x":"✖️","heavy_dollar_sign":"💲","currency_exchange":"💱","copyright":"©️","registered":"®️","tm":"™️","end":"🔚","back":"🔙","on":"🔛","top":"🔝","soon":"🔜","ballot_box_with_check":"☑️","radio_button":"🔘","white_circle":"⚪","black_circle":"⚫","red_circle":"🔴","large_blue_circle":"🔵","small_orange_diamond":"🔸","small_blue_diamond":"🔹","large_orange_diamond":"🔶","large_blue_diamond":"🔷","small_red_triangle":"🔺","black_small_square":"▪️","white_small_square":"▫️","black_large_square":"⬛","white_large_square":"⬜","small_red_triangle_down":"🔻","black_medium_square":"◼️","white_medium_square":"◻️","black_medium_small_square":"◾","white_medium_small_square":"◽","black_square_button":"🔲","white_square_button":"🔳","speaker":"🔈","sound":"🔉","loud_sound":"🔊","mute":"🔇","mega":"📣","loudspeaker":"📢","bell":"🔔","no_bell":"🔕","black_joker":"🃏","mahjong":"🀄","spades":"♠️","clubs":"♣️","hearts":"♥️","diamonds":"♦️","flower_playing_cards":"🎴","thought_balloon":"💭","right_anger_bubble":"🗯","speech_balloon":"💬","left_speech_bubble":"🗨","clock1":"🕐","clock2":"🕑","clock3":"🕒","clock4":"🕓","clock5":"🕔","clock6":"🕕","clock7":"🕖","clock8":"🕗","clock9":"🕘","clock10":"🕙","clock11":"🕚","clock12":"🕛","clock130":"🕜","clock230":"🕝","clock330":"🕞","clock430":"🕟","clock530":"🕠","clock630":"🕡","clock730":"🕢","clock830":"🕣","clock930":"🕤","clock1030":"🕥","clock1130":"🕦","clock1230":"🕧","af":"🇦🇫","ax":"🇦🇽","al":"🇦🇱","dz":"🇩🇿","as":"🇦🇸","ad":"🇦🇩","ao":"🇦🇴","ai":"🇦🇮","aq":"🇦🇶","ag":"🇦🇬","ar":"🇦🇷","am":"🇦🇲","aw":"🇦🇼","au":"🇦🇺","at":"🇦🇹","az":"🇦🇿","bs":"🇧🇸","bh":"🇧🇭","bd":"🇧🇩","bb":"🇧🇧","by":"🇧🇾","be":"🇧🇪","bz":"🇧🇿","bj":"🇧🇯","bm":"🇧🇲","bt":"🇧🇹","bo":"🇧🇴","bq":"🇧🇶","ba":"🇧🇦","bw":"🇧🇼","br":"🇧🇷","io":"🇮🇴","vg":"🇻🇬","bn":"🇧🇳","bg":"🇧🇬","bf":"🇧🇫","bi":"🇧🇮","cv":"🇨🇻","kh":"🇰🇭","cm":"🇨🇲","ca":"🇨🇦","ic":"🇮🇨","ky":"🇰🇾","cf":"🇨🇫","td":"🇹🇩","chile":"🇨🇱","cn":"🇨🇳","cx":"🇨🇽","cc":"🇨🇨","co":"🇨🇴","km":"🇰🇲","cg":"🇨🇬","drc":"🇨🇩","ck":"🇨🇰","cr":"🇨🇷","hr":"🇭🇷","cu":"🇨🇺","cw":"🇨🇼","cy":"🇨🇾","cz":"🇨🇿","dk":"🇩🇰","dj":"🇩🇯","dm":"🇩🇲","do":"🇩🇴","ec":"🇪🇨","eg":"🇪🇬","sv":"🇸🇻","gq":"🇬🇶","er":"🇪🇷","ee":"🇪🇪","et":"🇪🇹","eu":"🇪🇺","fk":"🇫🇰","fo":"🇫🇴","fj":"🇫🇯","fi":"🇫🇮","fr":"🇫🇷","gf":"🇬🇫","pf":"🇵🇫","tf":"🇹🇫","ga":"🇬🇦","gm":"🇬🇲","ge":"🇬🇪","de":"🇩🇪","gh":"🇬🇭","gi":"🇬🇮","gr":"🇬🇷","gl":"🇬🇱","gd":"🇬🇩","gp":"🇬🇵","gu":"🇬🇺","gt":"🇬🇹","gg":"🇬🇬","gn":"🇬🇳","gw":"🇬🇼","gy":"🇬🇾","ht":"🇭🇹","hn":"🇭🇳","hk":"🇭🇰","hu":"🇭🇺","is":"🇮🇸","in":"🇮🇳","indonesia":"🇮🇩","ir":"🇮🇷","iq":"🇮🇶","ie":"🇮🇪","im":"🇮🇲","il":"🇮🇱","it":"🇮🇹","ci":"🇨🇮","jm":"🇯🇲","jp":"🇯🇵","je":"🇯🇪","jo":"🇯🇴","kz":"🇰🇿","ke":"🇰🇪","ki":"🇰🇮","xk":"🇽🇰","kw":"🇰🇼","kg":"🇰🇬","la":"🇱🇦","lv":"🇱🇻","lb":"🇱🇧","ls":"🇱🇸","lr":"🇱🇷","ly":"🇱🇾","li":"🇱🇮","lt":"🇱🇹","lu":"🇱🇺","mo":"🇲🇴","mk":"🇲🇰","mg":"🇲🇬","mw":"🇲🇼","my":"🇲🇾","mv":"🇲🇻","ml":"🇲🇱","mt":"🇲🇹","mh":"🇲🇭","mq":"🇲🇶","mr":"🇲🇷","mu":"🇲🇺","yt":"🇾🇹","mx":"🇲🇽","fm":"🇫🇲","md":"🇲🇩","mc":"🇲🇨","mn":"🇲🇳","me":"🇲🇪","ms":"🇲🇸","ma":"🇲🇦","mz":"🇲🇿","mm":"🇲🇲","na":"🇳🇦","nr":"🇳🇷","np":"🇳🇵","nl":"🇳🇱","nc":"🇳🇨","nz":"🇳🇿","ni":"🇳🇮","ne":"🇳🇪","nigeria":"🇳🇬","nu":"🇳🇺","nf":"🇳🇫","mp":"🇲🇵","kp":"🇰🇵","no":"🇳🇴","om":"🇴🇲","pk":"🇵🇰","pw":"🇵🇼","ps":"🇵🇸","pa":"🇵🇦","pg":"🇵🇬","py":"🇵🇾","pe":"🇵🇪","ph":"🇵🇭","pn":"🇵🇳","pl":"🇵🇱","pt":"🇵🇹","pr":"🇵🇷","qa":"🇶🇦","re":"🇷🇪","ro":"🇷🇴","ru":"🇷🇺","rw":"🇷🇼","bl":"🇧🇱","sh":"🇸🇭","kn":"🇰🇳","lc":"🇱🇨","pm":"🇵🇲","vc":"🇻🇨","ws":"🇼🇸","sm":"🇸🇲","st":"🇸🇹","saudi_arabia":"🇸🇦","sn":"🇸🇳","rs":"🇷🇸","sc":"🇸🇨","sl":"🇸🇱","sg":"🇸🇬","sx":"🇸🇽","sk":"🇸🇰","si":"🇸🇮","sb":"🇸🇧","so":"🇸🇴","za":"🇿🇦","gs":"🇬🇸","kr":"🇰🇷","ss":"🇸🇸","es":"🇪🇸","lk":"🇱🇰","sd":"🇸🇩","sr":"🇸🇷","sz":"🇸🇿","se":"🇸🇪","ch":"🇨🇭","sy":"🇸🇾","tw":"🇹🇼","tj":"🇹🇯","tz":"🇹🇿","th":"🇹🇭","tl":"🇹🇱","tg":"🇹🇬","tk":"🇹🇰","to":"🇹🇴","tt":"🇹🇹","tn":"🇹🇳","tr":"🇹🇷","turkmenistan":"🇹🇲","tc":"🇹🇨","tuvalu":"🇹🇻","ug":"🇺🇬","ua":"🇺🇦","ae":"🇦🇪","gb":"🇬🇧","us":"🇺🇸","vi":"🇻🇮","uy":"🇺🇾","uz":"🇺🇿","vu":"🇻🇺","va":"🇻🇦","ve":"🇻🇪","vn":"🇻🇳","wf":"🇼🇫","eh":"🇪🇭","ye":"🇾🇪","zm":"🇿🇲","zw":"🇿🇼"} \ No newline at end of file diff --git a/frontend/utils/emojify.js b/frontend/utils/emojify.js deleted file mode 100644 index eeee3e1f..00000000 --- a/frontend/utils/emojify.js +++ /dev/null @@ -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; diff --git a/frontend/utils/markdown.js b/frontend/utils/markdown.js deleted file mode 100644 index e5511c66..00000000 --- a/frontend/utils/markdown.js +++ /dev/null @@ -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 `
${highlighted}
`; -}; -renderer.heading = (text, level) => { - const headingSlug = _.escape(slug(text)); - return ` - - ${text} - # - - `; -}; - -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 }; diff --git a/frontend/utils/toc/index.js b/frontend/utils/toc/index.js deleted file mode 100644 index 8eaa5499..00000000 --- a/frontend/utils/toc/index.js +++ /dev/null @@ -1,148 +0,0 @@ -// @flow -/* eslint-disable */ - -/** - * 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 = ''; - var stop = ''; - var re = /([\s\S]+?)/; - - // 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); -}; diff --git a/frontend/utils/toc/utils.js b/frontend/utils/toc/utils.js deleted file mode 100644 index e1cd9177..00000000 --- a/frontend/utils/toc/utils.js +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint-disable */ - -/*! - * 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, ''); -}; diff --git a/package.json b/package.json index 11e75b1e..04d7b820 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ }, "dependencies": { "@tommoor/slate-drop-or-paste-images": "0.5.1", + "aws-sdk": "^2.135.0", "babel-core": "^6.24.1", "babel-eslint": "^7.2.3", "babel-loader": "6.2.5", @@ -85,7 +86,6 @@ "css-loader": "^0.28.7", "debug": "2.2.0", "dotenv": "^4.0.0", - "emoji-name-map": "1.1.2", "emoji-regex": "^6.5.1", "eslint": "^3.19.0", "eslint-config-react-app": "^0.6.2", @@ -100,7 +100,6 @@ "fbemitter": "^2.1.1", "file-loader": "0.9.0", "flow-typed": "^2.1.2", - "highlight.js": "9.4.0", "history": "3.0.0", "html-webpack-plugin": "2.17.0", "http-errors": "1.4.0", @@ -108,7 +107,6 @@ "invariant": "^2.2.2", "isomorphic-fetch": "2.2.1", "js-search": "^1.4.2", - "js-tree": "1.1.0", "json-loader": "0.5.4", "jsonwebtoken": "7.0.1", "koa": "^2.2.0", @@ -125,8 +123,6 @@ "localforage": "^1.5.0", "lodash": "^4.17.4", "lodash.orderby": "4.4.0", - "marked": "0.3.6", - "marked-sanitized": "^0.1.1", "mobx": "^3.1.9", "mobx-react": "^4.1.8", "mobx-react-devtools": "^4.2.11", @@ -170,7 +166,6 @@ "string-hash": "^1.1.0", "style-loader": "^0.18.2", "styled-components": "^2.0.0", - "truncate-html": "https://github.com/jorilallo/truncate-html/tarball/master", "url-loader": "0.5.7", "uuid": "2.0.2", "validator": "5.2.0", diff --git a/scripts/update_emoji_mapping.js b/scripts/update_emoji_mapping.js deleted file mode 100644 index 958763e4..00000000 --- a/scripts/update_emoji_mapping.js +++ /dev/null @@ -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) -); diff --git a/server/api/auth.js b/server/api/auth.js index 42752b47..51304e5d 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -47,6 +47,10 @@ router.post('auth.slack', async ctx => { await team.createFirstCollection(user.id); } + // Update user's avatar + await user.updateAvatar(); + await user.save(); + ctx.body = { data: { user: await presentUser(ctx, user), diff --git a/server/api/documents.js b/server/api/documents.js index b21b7da8..5e098ca8 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -4,10 +4,16 @@ import httpErrors from 'http-errors'; import auth from './middlewares/authentication'; import pagination from './middlewares/pagination'; -import { presentDocument } from '../presenters'; -import { Document, Collection, Star, View } from '../models'; +import { presentDocument, presentRevision } from '../presenters'; +import { Document, Collection, Star, View, Revision } from '../models'; + +const authDocumentForUser = (ctx, document) => { + const user = ctx.state.user; + if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound(); +}; const router = new Router(); + router.post('documents.list', auth(), pagination(), async ctx => { let { sort = 'updatedAt', direction } = ctx.body; if (direction !== 'ASC') direction = 'DESC'; @@ -101,23 +107,38 @@ router.post('documents.info', auth(), async ctx => { ctx.assertPresent(id, 'id is required'); const document = await Document.findById(id); - if (!document) throw httpErrors.NotFound(); - - // Don't expose private documents outside the team - if (document.private) { - if (!ctx.state.user) throw httpErrors.NotFound(); - - const user = await ctx.state.user; - if (document.teamId !== user.teamId) { - throw httpErrors.NotFound(); - } - } + authDocumentForUser(ctx, document); ctx.body = { data: await presentDocument(ctx, document), }; }); +router.post('documents.revisions', auth(), pagination(), async ctx => { + let { id, sort = 'updatedAt', direction } = ctx.body; + if (direction !== 'ASC') direction = 'DESC'; + ctx.assertPresent(id, 'id is required'); + const document = await Document.findById(id); + + authDocumentForUser(ctx, document); + + const revisions = await Revision.findAll({ + where: { documentId: id }, + order: [[sort, direction]], + offset: ctx.state.pagination.offset, + limit: ctx.state.pagination.limit, + }); + + const data = await Promise.all( + revisions.map(revision => presentRevision(ctx, revision)) + ); + + ctx.body = { + pagination: ctx.state.pagination, + data, + }; +}); + router.post('documents.search', auth(), async ctx => { const { query } = ctx.body; ctx.assertPresent(query, 'query is required'); @@ -142,8 +163,7 @@ router.post('documents.star', auth(), async ctx => { const user = await ctx.state.user; const document = await Document.findById(id); - if (!document || document.teamId !== user.teamId) - throw httpErrors.BadRequest(); + authDocumentForUser(ctx, document); await Star.findOrCreate({ where: { documentId: document.id, userId: user.id }, @@ -156,8 +176,7 @@ router.post('documents.unstar', auth(), async ctx => { const user = await ctx.state.user; const document = await Document.findById(id); - if (!document || document.teamId !== user.teamId) - throw httpErrors.BadRequest(); + authDocumentForUser(ctx, document); await Star.destroy({ where: { documentId: document.id, userId: user.id }, @@ -228,7 +247,7 @@ router.post('documents.update', auth(), async ctx => { const document = await Document.findById(id); const collection = document.collection; - if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound(); + authDocumentForUser(ctx, document); // Update document if (title) document.title = title; @@ -254,15 +273,14 @@ router.post('documents.move', auth(), async ctx => { ctx.assertUuid(parentDocument, 'parentDocument must be an uuid'); if (index) ctx.assertPositiveInteger(index, 'index must be an integer (>=0)'); - const user = ctx.state.user; const document = await Document.findById(id); const collection = await Collection.findById(document.atlasId); + authDocumentForUser(ctx, document); + if (collection.type !== 'atlas') throw httpErrors.BadRequest("This document can't be moved"); - if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound(); - // Set parent document if (parentDocument) { const parent = await Document.findById(parentDocument); @@ -292,12 +310,10 @@ router.post('documents.delete', auth(), async ctx => { const { id } = ctx.body; ctx.assertPresent(id, 'id is required'); - const user = ctx.state.user; const document = await Document.findById(id); const collection = await Collection.findById(document.atlasId); - if (!document || document.teamId !== user.teamId) - throw httpErrors.BadRequest(); + authDocumentForUser(ctx, document); if (collection.type === 'atlas') { // Don't allow deletion of root docs diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 8f1b7c58..7d2bc37d 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -43,6 +43,24 @@ describe('#documents.list', async () => { }); }); +describe('#documents.revision', async () => { + it("should return document's revisions", async () => { + const { user, document } = await seed(); + const res = await server.post('/api/documents.revisions', { + body: { + token: user.getJwtToken(), + id: document.id, + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(1); + expect(body.data[0].id).not.toEqual(document.id); + expect(body.data[0].title).toEqual(document.title); + }); +}); + describe('#documents.search', async () => { it('should return results', async () => { const { user } = await seed(); diff --git a/server/index.js b/server/index.js index ae6317b7..10c7bb92 100644 --- a/server/index.js +++ b/server/index.js @@ -65,8 +65,8 @@ if (process.env.NODE_ENV === 'development') { app.use(logger()); } -if (process.env.NODE_ENV === 'production') { - bugsnag.register('ad7a85f99b1b9324a31e16732cdf3192'); +if (process.env.NODE_ENV === 'production' && process.env.BUGSNAG_KEY) { + bugsnag.register(process.env.BUGSNAG_KEY); app.on('error', bugsnag.koaHandler); } diff --git a/server/migrations/20171016012353-remove-collection-navigationtree.js b/server/migrations/20171016012353-remove-collection-navigationtree.js new file mode 100644 index 00000000..0ba1eb4c --- /dev/null +++ b/server/migrations/20171016012353-remove-collection-navigationtree.js @@ -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, + }); + }, +}; diff --git a/server/migrations/20171017055026-remove-document-html.js b/server/migrations/20171017055026-remove-document-html.js new file mode 100644 index 00000000..b9cdd821 --- /dev/null +++ b/server/migrations/20171017055026-remove-document-html.js @@ -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, + }); + }, +}; diff --git a/server/migrations/20171019071915-user-avatar-url.js b/server/migrations/20171019071915-user-avatar-url.js new file mode 100644 index 00000000..5be75d52 --- /dev/null +++ b/server/migrations/20171019071915-user-avatar-url.js @@ -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'); + }, +}; diff --git a/server/models/Collection.js b/server/models/Collection.js index 1ca29075..a05f0289 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -29,7 +29,6 @@ const Collection = sequelize.define( creatorId: DataTypes.UUID, /* type: atlas */ - navigationTree: DataTypes.JSONB, // legacy documentStructure: DataTypes.JSONB, }, { @@ -98,28 +97,6 @@ Collection.prototype.getUrl = function() { return `/collections/${this.id}`; }; -Collection.prototype.getDocumentsStructure = async function() { - // Lazy fill this.documentStructure - TMP for internal release - if (!this.documentStructure) { - this.documentStructure = this.navigationTree.children; - - // Remove parent references from all root documents - await this.navigationTree.children.forEach(async ({ id }) => { - const document = await Document.findById(id); - document.parentDocumentId = null; - await document.save(); - }); - - // Remove root document - const rootDocument = await Document.findById(this.navigationTree.id); - await rootDocument.destroy(); - - await this.save(); - } - - return this.documentStructure; -}; - Collection.prototype.addDocumentToStructure = async function( document, index, diff --git a/server/models/Document.js b/server/models/Document.js index 7ee9e425..95e3b146 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -2,12 +2,9 @@ import slug from 'slug'; import _ from 'lodash'; import randomstring from 'randomstring'; -import emojiRegex from 'emoji-regex'; import isUUID from 'validator/lib/isUUID'; import { DataTypes, sequelize } from '../sequelize'; -import { convertToMarkdown } from '../../frontend/utils/markdown'; -import { truncateMarkdown } from '../utils/truncate'; import parseTitle from '../../shared/parseTitle'; import Revision from './Revision'; @@ -25,8 +22,6 @@ const createRevision = doc => { return Revision.create({ title: doc.title, text: doc.text, - html: doc.html, - preview: doc.preview, userId: doc.lastModifiedById, documentId: doc.id, }); @@ -40,8 +35,6 @@ const beforeSave = async doc => { const { emoji } = parseTitle(doc.text); doc.emoji = emoji; - doc.html = convertToMarkdown(doc.text); - doc.preview = truncateMarkdown(doc.text, 160); doc.revisionCount += 1; // Collaborators @@ -74,8 +67,6 @@ const Document = sequelize.define( private: { type: DataTypes.BOOLEAN, defaultValue: true }, title: DataTypes.STRING, text: DataTypes.TEXT, - html: DataTypes.TEXT, - preview: DataTypes.TEXT, revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 }, parentDocumentId: DataTypes.UUID, createdById: { diff --git a/server/models/Revision.js b/server/models/Revision.js index d69892ea..f5dcc846 100644 --- a/server/models/Revision.js +++ b/server/models/Revision.js @@ -9,8 +9,6 @@ const Revision = sequelize.define('revision', { }, title: DataTypes.STRING, text: DataTypes.TEXT, - html: DataTypes.TEXT, - preview: DataTypes.TEXT, userId: { type: 'UUID', diff --git a/server/models/User.js b/server/models/User.js index 69158780..94b0affc 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,7 +1,9 @@ // @flow import crypto from 'crypto'; import bcrypt from 'bcrypt'; +import uuid from 'uuid'; import { DataTypes, sequelize, encryptedFields } from '../sequelize'; +import { uploadToS3FromUrl } from '../utils/s3'; import JWT from 'jsonwebtoken'; @@ -18,6 +20,7 @@ const User = sequelize.define( email: { type: DataTypes.STRING }, username: { type: DataTypes.STRING }, name: DataTypes.STRING, + avatarUrl: { type: DataTypes.STRING, allowNull: true }, password: DataTypes.VIRTUAL, passwordDigest: DataTypes.STRING, isAdmin: DataTypes.BOOLEAN, @@ -66,6 +69,12 @@ User.prototype.verifyPassword = function(password) { }); }); }; +User.prototype.updateAvatar = async function() { + this.avatarUrl = await uploadToS3FromUrl( + this.slackData.image_192, + `avatars/${this.id}/${uuid.v4()}` + ); +}; const setRandomJwtSecret = model => { model.jwtSecret = crypto.randomBytes(64).toString('hex'); diff --git a/server/presenters/collection.js b/server/presenters/collection.js index a6fdbc8e..aeae3dce 100644 --- a/server/presenters/collection.js +++ b/server/presenters/collection.js @@ -19,7 +19,7 @@ async function present(ctx: Object, collection: Collection) { }; if (collection.type === 'atlas') { - data.documents = await collection.getDocumentsStructure(); + data.documents = collection.documentStructure; } if (collection.documents) { diff --git a/server/presenters/document.js b/server/presenters/document.js index 5927a83d..bf483cd1 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -21,8 +21,6 @@ async function present(ctx: Object, document: Document, options: ?Options) { private: document.private, title: document.title, text: document.text, - html: document.html, - preview: document.preview, emoji: document.emoji, createdAt: document.createdAt, createdBy: presentUser(ctx, document.createdBy), diff --git a/server/presenters/index.js b/server/presenters/index.js index 22e34f25..8eab4228 100644 --- a/server/presenters/index.js +++ b/server/presenters/index.js @@ -2,6 +2,7 @@ import presentUser from './user'; import presentView from './view'; import presentDocument from './document'; +import presentRevision from './revision'; import presentCollection from './collection'; import presentApiKey from './apiKey'; import presentTeam from './team'; @@ -10,6 +11,7 @@ export { presentUser, presentView, presentDocument, + presentRevision, presentCollection, presentApiKey, presentTeam, diff --git a/server/presenters/revision.js b/server/presenters/revision.js new file mode 100644 index 00000000..ef137767 --- /dev/null +++ b/server/presenters/revision.js @@ -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; diff --git a/server/presenters/user.js b/server/presenters/user.js index f545cd66..1d064ace 100644 --- a/server/presenters/user.js +++ b/server/presenters/user.js @@ -8,7 +8,8 @@ function present(ctx: Object, user: User) { id: user.id, username: user.username, name: user.name, - avatarUrl: user.slackData ? user.slackData.image_192 : null, + avatarUrl: user.avatarUrl || + (user.slackData ? user.slackData.image_192 : null), }; } diff --git a/server/slack.js b/server/slack.js index 62bb75eb..82d1a07f 100644 --- a/server/slack.js +++ b/server/slack.js @@ -15,7 +15,6 @@ export async function request(endpoint: string, body: Object) { } catch (e) { throw httpErrors.BadRequest(); } - console.log('DATA', data); if (!data.ok) throw httpErrors.BadRequest(data.error); return data; @@ -28,7 +27,7 @@ export async function oauthAccess( return request('oauth.access', { client_id: process.env.SLACK_KEY, client_secret: process.env.SLACK_SECRET, - redirect_uri: `${process.env.URL || ''}/auth/slack`, + redirect_uri, code, }); } diff --git a/server/static/index.html b/server/static/index.html index e79463d5..3613c5c0 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -26,6 +26,6 @@
- + - + \ No newline at end of file diff --git a/server/utils/s3.js b/server/utils/s3.js index 65351a98..14072b5a 100644 --- a/server/utils/s3.js +++ b/server/utils/s3.js @@ -1,5 +1,18 @@ +// @flow import crypto from 'crypto'; 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 policy = { @@ -19,13 +32,37 @@ const makePolicy = () => { 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 - .createHmac('sha1', process.env.AWS_SECRET_ACCESS_KEY) + .createHmac('sha1', AWS_SECRET_ACCESS_KEY) .update(policy) .digest('base64'); 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 }; diff --git a/server/utils/truncate.js b/server/utils/truncate.js deleted file mode 100644 index 85664daa..00000000 --- a/server/utils/truncate.js +++ /dev/null @@ -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 }; diff --git a/webpack.config.js b/webpack.config.js index f99b890c..5e33b37a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,6 +13,7 @@ const definePlugin = new webpack.DefinePlugin({ SLACK_REDIRECT_URI: JSON.stringify(process.env.SLACK_REDIRECT_URI), SLACK_KEY: JSON.stringify(process.env.SLACK_KEY), BASE_URL: JSON.stringify(process.env.URL), + BUGSNAG_KEY: JSON.stringify(process.env.BUGSNAG_KEY), DEPLOYMENT: JSON.stringify(process.env.DEPLOYMENT || 'hosted'), }); diff --git a/yarn.lock b/yarn.lock index 87925c56..16b429b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -339,6 +339,21 @@ autoprefixer@^6.3.1: postcss "^5.2.16" 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: version "0.6.0" 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" 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" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" dependencies: @@ -1454,7 +1469,7 @@ charenc@~0.0.1: version "0.0.2" 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" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" dependencies: @@ -1959,6 +1974,10 @@ cryptiles@2.x.x: dependencies: 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: version "3.11.1" 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-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: version "6.5.1" 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: version "2.1.0" 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" through "~2.3.1" -events@^1.0.0: +events@^1.0.0, events@^1.1.1: version "1.1.1" 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" 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: version "3.0.0" 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" toposort "^0.2.12" -htmlparser2@^3.9.0, htmlparser2@^3.9.1: +htmlparser2@^3.9.1: version "3.9.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" dependencies: @@ -4611,10 +4614,6 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" 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: version "20.0.3" 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" 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: version "6.10.1" 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" 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: version "3.9.1" 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" 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: version "1.0.1" 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" 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: version "1.7.0" 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" node-emoji "^1.4.1" -marked@0.3.6, marked@^0.3.6: +marked@^0.3.6: version "0.3.6" resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7" @@ -7610,10 +7597,6 @@ regex-cache@^0.4.2: dependencies: 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: version "1.0.0" 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" watch "~0.10.0" -sanitize-html@^1.5.2: - version "1.14.1" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.14.1.tgz#730ffa2249bdf18333effe45b286173c9c5ad0b8" - dependencies: - htmlparser2 "^3.9.0" - regexp-quote "0.0.0" - xtend "^4.0.0" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" -sax@^1.2.1, sax@~1.2.1: +sax@>=0.6.0, sax@^1.2.1, sax@~1.2.1: version "1.2.4" 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" slate-markdown-serializer@tommoor/slate-markdown-serializer: - version "0.4.3" - resolved "https://codeload.github.com/tommoor/slate-markdown-serializer/tar.gz/8e987951db999617ff6759c85e384dad175d5b92" + version "0.5.2" + resolved "https://codeload.github.com/tommoor/slate-markdown-serializer/tar.gz/75708cf421c0a0ac0d7d541b295315cbff0839c0" slate-paste-linkify@^0.2.1: version "0.2.1" @@ -8711,12 +8690,6 @@ trim-right@^1.0.1: version "1.0.1" 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: version "1.0.3" 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" resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" +url@0.10.3, url@~0.10.1: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" dependencies: punycode "1.3.2" querystring "0.2.0" -url@~0.10.1: - version "0.10.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" dependencies: punycode "1.3.2" querystring "0.2.0" @@ -8953,14 +8926,14 @@ uuid@2.0.2: version "2.0.2" 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: version "2.0.3" 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: version "2.1.1" 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" 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: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"