diff --git a/.eslintrc b/.eslintrc index ed273945..65facbe7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,7 +6,10 @@ "plugin:import/warnings", "plugin:flowtype/recommended" ], - "plugins": ["prettier", "flowtype"], + "plugins": [ + "prettier", + "flowtype" + ], "rules": { "eqeqeq": 2, "no-unused-vars": 2, @@ -18,14 +21,19 @@ "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" + ], "prettier/prettier": [ "error", { "printWidth": 80, - "trailingComma": "es5", - "singleQuote": true + "trailingComma": "es5" } ] }, @@ -34,11 +42,14 @@ "createClass": "createReactClass", "pragma": "React", "version": "detect", - "flowVersion": "0.86" + "flowVersion": "0.86" }, "import/resolver": { "node": { - "paths": ["app", "."] + "paths": [ + "app", + "." + ] } }, "flowtype": { @@ -57,4 +68,4 @@ "afterAll": true, "Sentry": true } -} +} \ No newline at end of file diff --git a/app/components/Actions.js b/app/components/Actions.js index 370b99a2..920f3899 100644 --- a/app/components/Actions.js +++ b/app/components/Actions.js @@ -1,7 +1,7 @@ // @flow -import styled from 'styled-components'; -import breakpoint from 'styled-components-breakpoint'; -import Flex from 'shared/components/Flex'; +import styled from "styled-components"; +import breakpoint from "styled-components-breakpoint"; +import Flex from "shared/components/Flex"; export const Action = styled(Flex)` justify-content: center; @@ -40,7 +40,7 @@ const Actions = styled(Flex)` display: none; } - ${breakpoint('tablet')` + ${breakpoint("tablet")` left: auto; padding: 24px; `}; diff --git a/app/components/Alert.js b/app/components/Alert.js index fc5f4c91..629a3c22 100644 --- a/app/components/Alert.js +++ b/app/components/Alert.js @@ -1,18 +1,18 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import Flex from 'shared/components/Flex'; -import styled from 'styled-components'; +import * as React from "react"; +import { observer } from "mobx-react"; +import Flex from "shared/components/Flex"; +import styled from "styled-components"; type Props = { children: React.Node, - type?: 'info' | 'success' | 'warning' | 'danger' | 'offline', + type?: "info" | "success" | "warning" | "danger" | "offline", }; @observer class Alert extends React.Component { defaultProps = { - type: 'info', + type: "info", }; render() { diff --git a/app/components/Analytics.js b/app/components/Analytics.js index ebcbb7aa..284c203b 100644 --- a/app/components/Analytics.js +++ b/app/components/Analytics.js @@ -1,6 +1,6 @@ // @flow /* global ga */ -import * as React from 'react'; +import * as React from "react"; type Props = { children?: React.Node, @@ -20,12 +20,12 @@ export default class Analytics extends React.Component { // $FlowIssue ga.l = +new Date(); - ga('create', process.env.GOOGLE_ANALYTICS_ID, 'auto'); - ga('set', { dimension1: 'true' }); - ga('send', 'pageview'); + ga("create", process.env.GOOGLE_ANALYTICS_ID, "auto"); + ga("set", { dimension1: "true" }); + ga("send", "pageview"); - const script = document.createElement('script'); - script.src = 'https://www.google-analytics.com/analytics.js'; + const script = document.createElement("script"); + script.src = "https://www.google-analytics.com/analytics.js"; script.async = true; if (document.body) { diff --git a/app/components/Authenticated.js b/app/components/Authenticated.js index 14f53c3e..a81a8200 100644 --- a/app/components/Authenticated.js +++ b/app/components/Authenticated.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import AuthStore from 'stores/AuthStore'; -import LoadingIndicator from 'components/LoadingIndicator'; -import { isCustomSubdomain } from 'shared/utils/domains'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import AuthStore from "stores/AuthStore"; +import LoadingIndicator from "components/LoadingIndicator"; +import { isCustomSubdomain } from "shared/utils/domains"; type Props = { auth: AuthStore, @@ -38,4 +38,4 @@ const Authenticated = observer(({ auth, children }: Props) => { return null; }); -export default inject('auth')(Authenticated); +export default inject("auth")(Authenticated); diff --git a/app/components/Avatar/Avatar.js b/app/components/Avatar/Avatar.js index e5970971..1066a34a 100644 --- a/app/components/Avatar/Avatar.js +++ b/app/components/Avatar/Avatar.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import placeholder from './placeholder.png'; +import * as React from "react"; +import styled from "styled-components"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import placeholder from "./placeholder.png"; type Props = { src: string, diff --git a/app/components/Avatar/AvatarWithPresence.js b/app/components/Avatar/AvatarWithPresence.js index 3518fbcf..1d2f6f5c 100644 --- a/app/components/Avatar/AvatarWithPresence.js +++ b/app/components/Avatar/AvatarWithPresence.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import styled from 'styled-components'; -import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; -import Avatar from 'components/Avatar'; -import Tooltip from 'components/Tooltip'; -import User from 'models/User'; -import UserProfile from 'scenes/UserProfile'; -import { EditIcon } from 'outline-icons'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import styled from "styled-components"; +import distanceInWordsToNow from "date-fns/distance_in_words_to_now"; +import Avatar from "components/Avatar"; +import Tooltip from "components/Tooltip"; +import User from "models/User"; +import UserProfile from "scenes/UserProfile"; +import { EditIcon } from "outline-icons"; type Props = { user: User, @@ -44,10 +44,10 @@ class AvatarWithPresence extends React.Component { - {user.name} {isCurrentUser && '(You)'} + {user.name} {isCurrentUser && "(You)"}
{isPresent - ? isEditing ? 'currently editing' : 'currently viewing' + ? isEditing ? "currently editing" : "currently viewing" : `viewed ${distanceInWordsToNow(new Date(lastViewedAt))} ago`} } diff --git a/app/components/Avatar/index.js b/app/components/Avatar/index.js index cd77b1bb..0106b24d 100644 --- a/app/components/Avatar/index.js +++ b/app/components/Avatar/index.js @@ -1,6 +1,6 @@ // @flow -import Avatar from './Avatar'; -import AvatarWithPresence from './AvatarWithPresence'; +import Avatar from "./Avatar"; +import AvatarWithPresence from "./AvatarWithPresence"; export { AvatarWithPresence }; export default Avatar; diff --git a/app/components/Badge.js b/app/components/Badge.js index ba310e3a..d79082e4 100644 --- a/app/components/Badge.js +++ b/app/components/Badge.js @@ -1,5 +1,5 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const Badge = styled.span` margin-left: 10px; diff --git a/app/components/Button.js b/app/components/Button.js index 07a28cd2..45a7e128 100644 --- a/app/components/Button.js +++ b/app/components/Button.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { darken, lighten } from 'polished'; -import { ExpandedIcon } from 'outline-icons'; +import * as React from "react"; +import styled from "styled-components"; +import { darken, lighten } from "polished"; +import { ExpandedIcon } from "outline-icons"; const RealButton = styled.button` display: inline-block; @@ -54,10 +54,10 @@ const RealButton = styled.button` background: ${props.theme.buttonNeutralBackground}; color: ${props.theme.buttonNeutralText}; box-shadow: ${ - props.borderOnHover ? 'none' : 'rgba(0, 0, 0, 0.07) 0px 1px 2px' + props.borderOnHover ? "none" : "rgba(0, 0, 0, 0.07) 0px 1px 2px" }; border: 1px solid ${ - props.borderOnHover ? 'transparent' : props.theme.buttonNeutralBorder + props.borderOnHover ? "transparent" : props.theme.buttonNeutralBorder }; svg { @@ -102,7 +102,7 @@ const Label = styled.span` white-space: nowrap; text-overflow: ellipsis; - ${props => props.hasIcon && 'padding-left: 4px;'}; + ${props => props.hasIcon && "padding-left: 4px;"}; `; export const Inner = styled.span` @@ -113,8 +113,8 @@ export const Inner = styled.span` justify-content: center; align-items: center; - ${props => props.hasIcon && props.hasText && 'padding-left: 4px;'}; - ${props => props.hasIcon && !props.hasText && 'padding: 0 4px;'}; + ${props => props.hasIcon && props.hasText && "padding-left: 4px;"}; + ${props => props.hasIcon && !props.hasText && "padding: 0 4px;"}; `; export type Props = { @@ -130,7 +130,7 @@ export type Props = { }; function Button({ - type = 'text', + type = "text", icon, children, value, diff --git a/app/components/CenteredContent.js b/app/components/CenteredContent.js index 2c5f9c45..aa79a6f7 100644 --- a/app/components/CenteredContent.js +++ b/app/components/CenteredContent.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import breakpoint from 'styled-components-breakpoint'; +import * as React from "react"; +import styled from "styled-components"; +import breakpoint from "styled-components-breakpoint"; type Props = { children?: React.Node, @@ -11,7 +11,7 @@ const Container = styled.div` width: 100%; padding: 60px 20px; - ${breakpoint('tablet')` + ${breakpoint("tablet")` padding: 60px; `}; `; diff --git a/app/components/Checkbox.js b/app/components/Checkbox.js index f6420cff..8acbaabc 100644 --- a/app/components/Checkbox.js +++ b/app/components/Checkbox.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import HelpText from 'components/HelpText'; -import VisuallyHidden from 'components/VisuallyHidden'; +import * as React from "react"; +import styled from "styled-components"; +import HelpText from "components/HelpText"; +import VisuallyHidden from "components/VisuallyHidden"; export type Props = { checked?: boolean, @@ -15,13 +15,13 @@ export type Props = { const LabelText = styled.span` font-weight: 500; - margin-left: ${props => (props.small ? '6px' : '10px')}; - ${props => (props.small ? `color: ${props.theme.textSecondary}` : '')}; + margin-left: ${props => (props.small ? "6px" : "10px")}; + ${props => (props.small ? `color: ${props.theme.textSecondary}` : "")}; `; const Wrapper = styled.div` padding-bottom: 8px; - ${props => (props.small ? 'font-size: 14px' : '')}; + ${props => (props.small ? "font-size: 14px" : "")}; `; const Label = styled.label` diff --git a/app/components/ClickablePadding.js b/app/components/ClickablePadding.js index 30ac2cf3..04ed7b51 100644 --- a/app/components/ClickablePadding.js +++ b/app/components/ClickablePadding.js @@ -1,9 +1,9 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const ClickablePadding = styled.div` min-height: 10em; - cursor: ${({ onClick }) => (onClick ? 'text' : 'default')}; + cursor: ${({ onClick }) => (onClick ? "text" : "default")}; ${({ grow }) => grow && `flex-grow: 100;`}; `; diff --git a/app/components/Collaborators.js b/app/components/Collaborators.js index ce1b6e64..ea673d01 100644 --- a/app/components/Collaborators.js +++ b/app/components/Collaborators.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import { sortBy, keyBy } from 'lodash'; -import { MAX_AVATAR_DISPLAY } from 'shared/constants'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import { sortBy, keyBy } from "lodash"; +import { MAX_AVATAR_DISPLAY } from "shared/constants"; -import { AvatarWithPresence } from 'components/Avatar'; -import Facepile from 'components/Facepile'; -import Document from 'models/Document'; -import ViewsStore from 'stores/ViewsStore'; -import DocumentPresenceStore from 'stores/DocumentPresenceStore'; +import { AvatarWithPresence } from "components/Avatar"; +import Facepile from "components/Facepile"; +import Document from "models/Document"; +import ViewsStore from "stores/ViewsStore"; +import DocumentPresenceStore from "stores/DocumentPresenceStore"; type Props = { views: ViewsStore, @@ -73,4 +73,4 @@ class Collaborators extends React.Component { } } -export default inject('views', 'presence')(Collaborators); +export default inject("views", "presence")(Collaborators); diff --git a/app/components/CollectionIcon.js b/app/components/CollectionIcon.js index a5b1c348..c28b4977 100644 --- a/app/components/CollectionIcon.js +++ b/app/components/CollectionIcon.js @@ -1,11 +1,11 @@ // @flow -import * as React from 'react'; -import { inject, observer } from 'mobx-react'; -import { getLuminance } from 'polished'; -import { PrivateCollectionIcon, CollectionIcon } from 'outline-icons'; -import Collection from 'models/Collection'; -import { icons } from 'components/IconPicker'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import { inject, observer } from "mobx-react"; +import { getLuminance } from "polished"; +import { PrivateCollectionIcon, CollectionIcon } from "outline-icons"; +import Collection from "models/Collection"; +import { icons } from "components/IconPicker"; +import UiStore from "stores/UiStore"; type Props = { collection: Collection, @@ -18,18 +18,18 @@ function ResolvedCollectionIcon({ collection, expanded, size, ui }: Props) { // If the chosen icon color is very dark then we invert it in dark mode // otherwise it will be impossible to see against the dark background. const color = - ui.resolvedTheme === 'dark' + ui.resolvedTheme === "dark" ? getLuminance(collection.color) > 0.12 ? collection.color - : 'currentColor' + : "currentColor" : collection.color; - if (collection.icon && collection.icon !== 'collection') { + if (collection.icon && collection.icon !== "collection") { try { const Component = icons[collection.icon].component; return ; } catch (error) { - console.warn('Failed to render custom icon ' + collection.icon); + console.warn("Failed to render custom icon " + collection.icon); } } @@ -42,4 +42,4 @@ function ResolvedCollectionIcon({ collection, expanded, size, ui }: Props) { return ; } -export default inject('ui')(observer(ResolvedCollectionIcon)); +export default inject("ui")(observer(ResolvedCollectionIcon)); diff --git a/app/components/CopyToClipboard.js b/app/components/CopyToClipboard.js index 88ba33f5..d6bb9784 100644 --- a/app/components/CopyToClipboard.js +++ b/app/components/CopyToClipboard.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import copy from 'copy-to-clipboard'; +import * as React from "react"; +import copy from "copy-to-clipboard"; type Props = { text: string, @@ -19,7 +19,7 @@ class CopyToClipboard extends React.PureComponent { if (onCopy) onCopy(); - if (elem && elem.props && typeof elem.props.onClick === 'function') { + if (elem && elem.props && typeof elem.props.onClick === "function") { elem.props.onClick(ev); } }; diff --git a/app/components/DocumentHistory/DocumentHistory.js b/app/components/DocumentHistory/DocumentHistory.js index 0ab51f2d..47f2cfc7 100644 --- a/app/components/DocumentHistory/DocumentHistory.js +++ b/app/components/DocumentHistory/DocumentHistory.js @@ -1,20 +1,20 @@ // @flow -import * as React from 'react'; -import { observable, action } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import type { RouterHistory } from 'react-router-dom'; -import styled from 'styled-components'; -import { Waypoint } from 'react-waypoint'; -import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; +import * as React from "react"; +import { observable, action } from "mobx"; +import { observer, inject } from "mobx-react"; +import type { RouterHistory } from "react-router-dom"; +import styled from "styled-components"; +import { Waypoint } from "react-waypoint"; +import ArrowKeyNavigation from "boundless-arrow-key-navigation"; -import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore'; -import DocumentsStore from 'stores/DocumentsStore'; -import RevisionsStore from 'stores/RevisionsStore'; +import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore"; +import DocumentsStore from "stores/DocumentsStore"; +import RevisionsStore from "stores/RevisionsStore"; -import Flex from 'shared/components/Flex'; -import { ListPlaceholder } from 'components/LoadingPlaceholder'; -import Revision from './components/Revision'; -import { documentHistoryUrl } from 'utils/routeHelpers'; +import Flex from "shared/components/Flex"; +import { ListPlaceholder } from "components/LoadingPlaceholder"; +import Revision from "./components/Revision"; +import { documentHistoryUrl } from "utils/routeHelpers"; type Props = { match: Object, @@ -146,4 +146,4 @@ const Sidebar = styled(Flex)` z-index: 1; `; -export default inject('documents', 'revisions')(DocumentHistory); +export default inject("documents", "revisions")(DocumentHistory); diff --git a/app/components/DocumentHistory/components/Revision.js b/app/components/DocumentHistory/components/Revision.js index ee277c58..30ae6c5a 100644 --- a/app/components/DocumentHistory/components/Revision.js +++ b/app/components/DocumentHistory/components/Revision.js @@ -1,18 +1,18 @@ // @flow -import * as React from 'react'; -import { NavLink } from 'react-router-dom'; -import styled, { withTheme } from 'styled-components'; -import format from 'date-fns/format'; -import { MoreIcon } from 'outline-icons'; +import * as React from "react"; +import { NavLink } from "react-router-dom"; +import styled, { withTheme } from "styled-components"; +import format from "date-fns/format"; +import { MoreIcon } from "outline-icons"; -import Flex from 'shared/components/Flex'; -import Time from 'shared/components/Time'; -import Avatar from 'components/Avatar'; -import RevisionMenu from 'menus/RevisionMenu'; -import Document from 'models/Document'; -import Revision from 'models/Revision'; +import Flex from "shared/components/Flex"; +import Time from "shared/components/Time"; +import Avatar from "components/Avatar"; +import RevisionMenu from "menus/RevisionMenu"; +import Document from "models/Document"; +import Revision from "models/Revision"; -import { documentHistoryUrl } from 'utils/routeHelpers'; +import { documentHistoryUrl } from "utils/routeHelpers"; type Props = { theme: Object, @@ -32,12 +32,12 @@ class RevisionListItem extends React.Component { activeStyle={{ background: theme.primary, color: theme.white }} > - {' '} + {" "} {revision.createdBy.name} {showMenu && ( diff --git a/app/components/DocumentHistory/index.js b/app/components/DocumentHistory/index.js index e533e31a..7d566709 100644 --- a/app/components/DocumentHistory/index.js +++ b/app/components/DocumentHistory/index.js @@ -1,3 +1,3 @@ // @flow -import DocumentHistory from './DocumentHistory'; +import DocumentHistory from "./DocumentHistory"; export default DocumentHistory; diff --git a/app/components/DocumentList.js b/app/components/DocumentList.js index 98f9f41d..d12e730b 100644 --- a/app/components/DocumentList.js +++ b/app/components/DocumentList.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import Document from 'models/Document'; -import DocumentPreview from 'components/DocumentPreview'; -import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; +import * as React from "react"; +import Document from "models/Document"; +import DocumentPreview from "components/DocumentPreview"; +import ArrowKeyNavigation from "boundless-arrow-key-navigation"; type Props = { documents: Document[], diff --git a/app/components/DocumentPreview/DocumentPreview.js b/app/components/DocumentPreview/DocumentPreview.js index 740d26af..48321d48 100644 --- a/app/components/DocumentPreview/DocumentPreview.js +++ b/app/components/DocumentPreview/DocumentPreview.js @@ -1,16 +1,16 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { Link } from 'react-router-dom'; -import { StarredIcon } from 'outline-icons'; -import styled, { withTheme } from 'styled-components'; -import Flex from 'shared/components/Flex'; -import Badge from 'components/Badge'; -import Tooltip from 'components/Tooltip'; -import Highlight from 'components/Highlight'; -import PublishingInfo from 'components/PublishingInfo'; -import DocumentMenu from 'menus/DocumentMenu'; -import Document from 'models/Document'; +import * as React from "react"; +import { observer } from "mobx-react"; +import { Link } from "react-router-dom"; +import { StarredIcon } from "outline-icons"; +import styled, { withTheme } from "styled-components"; +import Flex from "shared/components/Flex"; +import Badge from "components/Badge"; +import Tooltip from "components/Tooltip"; +import Highlight from "components/Highlight"; +import PublishingInfo from "components/PublishingInfo"; +import DocumentMenu from "menus/DocumentMenu"; +import Document from "models/Document"; type Props = { document: Document, @@ -26,7 +26,7 @@ const StyledStar = withTheme(styled(({ solid, theme, ...props }) => ( ))` flex-shrink: 0; - opacity: ${props => (props.solid ? '1 !important' : 0)}; + opacity: ${props => (props.solid ? "1 !important" : 0)}; transition: all 100ms ease-in-out; &:hover { @@ -82,8 +82,8 @@ const Heading = styled.h3` margin-bottom: 0.25em; overflow: hidden; white-space: nowrap; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, - Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; `; const Actions = styled(Flex)` @@ -124,7 +124,7 @@ class DocumentPreview extends React.Component { replaceResultMarks = (tag: string) => { // don't use SEARCH_RESULT_REGEX here as it causes // an infinite loop to trigger a regex inside it's own callback - return tag.replace(/]*>(.*?)<\/b>/gi, '$1'); + return tag.replace(/]*>(.*?)<\/b>/gi, "$1"); }; render() { @@ -152,7 +152,7 @@ class DocumentPreview extends React.Component { {...rest} > - + <Title text={document.title || "Untitled"} highlight={highlight} /> {!document.isDraft && !document.isArchived && ( <Actions> diff --git a/app/components/DocumentPreview/index.js b/app/components/DocumentPreview/index.js index a5a2b1af..5b0f0d9d 100644 --- a/app/components/DocumentPreview/index.js +++ b/app/components/DocumentPreview/index.js @@ -1,3 +1,3 @@ // @flow -import DocumentPreview from './DocumentPreview'; +import DocumentPreview from "./DocumentPreview"; export default DocumentPreview; diff --git a/app/components/DropToImport.js b/app/components/DropToImport.js index 6dc8fd01..7276e87b 100644 --- a/app/components/DropToImport.js +++ b/app/components/DropToImport.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import { createGlobalStyle } from 'styled-components'; -import invariant from 'invariant'; -import importFile from 'utils/importFile'; -import Dropzone from 'react-dropzone'; -import DocumentsStore from 'stores/DocumentsStore'; -import LoadingIndicator from 'components/LoadingIndicator'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import { createGlobalStyle } from "styled-components"; +import invariant from "invariant"; +import importFile from "utils/importFile"; +import Dropzone from "react-dropzone"; +import DocumentsStore from "stores/DocumentsStore"; +import LoadingIndicator from "components/LoadingIndicator"; const EMPTY_OBJECT = {}; let importingLock = false; @@ -56,7 +56,7 @@ class DropToImport extends React.Component<Props> { if (documentId && !collectionId) { const document = await this.props.documents.fetch(documentId); - invariant(document, 'Document not available'); + invariant(document, "Document not available"); collectionId = document.collectionId; } @@ -110,4 +110,4 @@ class DropToImport extends React.Component<Props> { } } -export default inject('documents')(withRouter(DropToImport)); +export default inject("documents")(withRouter(DropToImport)); diff --git a/app/components/DropdownMenu/DropdownMenu.js b/app/components/DropdownMenu/DropdownMenu.js index d4bab28b..8b8a8f6d 100644 --- a/app/components/DropdownMenu/DropdownMenu.js +++ b/app/components/DropdownMenu/DropdownMenu.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import invariant from 'invariant'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import { PortalWithState } from 'react-portal'; -import { MoreIcon } from 'outline-icons'; -import styled from 'styled-components'; -import Flex from 'shared/components/Flex'; -import { fadeAndScaleIn } from 'shared/styles/animations'; -import NudeButton from 'components/NudeButton'; +import * as React from "react"; +import invariant from "invariant"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import { PortalWithState } from "react-portal"; +import { MoreIcon } from "outline-icons"; +import styled from "styled-components"; +import Flex from "shared/components/Flex"; +import { fadeAndScaleIn } from "shared/styles/animations"; +import NudeButton from "components/NudeButton"; let previousClosePortal; let counter = 0; @@ -25,7 +25,7 @@ type Props = { className?: string, hover?: boolean, style?: Object, - position?: 'left' | 'right' | 'center', + position?: "left" | "right" | "center", }; @observer @@ -37,7 +37,7 @@ class DropdownMenu extends React.Component<Props> { @observable bottom: ?number; @observable right: ?number; @observable left: ?number; - @observable position: 'left' | 'right' | 'center'; + @observable position: "left" | "right" | "center"; @observable fixed: ?boolean; @observable bodyRect: ClientRect; @observable labelRect: ClientRect; @@ -51,21 +51,21 @@ class DropdownMenu extends React.Component<Props> { return (ev: SyntheticMouseEvent<HTMLElement>) => { ev.preventDefault(); const currentTarget = ev.currentTarget; - invariant(document.body, 'why you not here'); + invariant(document.body, "why you not here"); if (currentTarget instanceof HTMLDivElement) { this.bodyRect = document.body.getBoundingClientRect(); this.labelRect = currentTarget.getBoundingClientRect(); this.top = this.labelRect.bottom - this.bodyRect.top; this.bottom = undefined; - this.position = this.props.position || 'left'; + this.position = this.props.position || "left"; if (currentTarget.parentElement) { const triggerParentStyle = getComputedStyle( currentTarget.parentElement ); - if (triggerParentStyle.position === 'static') { + if (triggerParentStyle.position === "static") { this.fixed = true; this.top = this.labelRect.bottom; } @@ -84,10 +84,10 @@ class DropdownMenu extends React.Component<Props> { }; initPosition() { - if (this.position === 'left') { + if (this.position === "left") { this.right = this.bodyRect.width - this.labelRect.left - this.labelRect.width; - } else if (this.position === 'center') { + } else if (this.position === "center") { this.left = this.labelRect.left + this.labelRect.width / 2; } else { this.left = this.labelRect.left; @@ -95,7 +95,7 @@ class DropdownMenu extends React.Component<Props> { } onOpen = () => { - if (typeof this.props.onOpen === 'function') { + if (typeof this.props.onOpen === "function") { this.props.onOpen(); } this.fitOnTheScreen(); @@ -114,18 +114,18 @@ class DropdownMenu extends React.Component<Props> { this.bottom = undefined; } - if (this.position === 'left' || this.position === 'right') { + if (this.position === "left" || this.position === "right") { const totalWidth = - Math.sign(this.position === 'left' ? -1 : 1) * el.offsetLeft + + Math.sign(this.position === "left" ? -1 : 1) * el.offsetLeft + el.scrollWidth; const isVisible = totalWidth < window.innerWidth; if (!isVisible) { - if (this.position === 'right') { - this.position = 'left'; + if (this.position === "right") { + this.position = "left"; this.left = undefined; - } else if (this.position === 'left') { - this.position = 'right'; + } else if (this.position === "left") { + this.position = "right"; this.right = undefined; } } @@ -177,7 +177,7 @@ class DropdownMenu extends React.Component<Props> { <NudeButton id={`${this.id}button`} aria-haspopup="true" - aria-expanded={isOpen ? 'true' : 'false'} + aria-expanded={isOpen ? "true" : "false"} aria-controls={this.id} > <MoreIcon /> @@ -201,7 +201,7 @@ class DropdownMenu extends React.Component<Props> { hover ? this.closeAfterTimeout(closePortal) : undefined } onClick={ - typeof children === 'function' + typeof children === "function" ? undefined : ev => { ev.stopPropagation(); @@ -213,7 +213,7 @@ class DropdownMenu extends React.Component<Props> { aria-labelledby={`${this.id}button`} role="menu" > - {typeof children === 'function' + {typeof children === "function" ? children({ closePortal }) : children} </Menu> @@ -228,35 +228,35 @@ class DropdownMenu extends React.Component<Props> { } const Label = styled(Flex).attrs({ - justify: 'center', - align: 'center', + justify: "center", + align: "center", })` z-index: 1000; cursor: pointer; `; const Position = styled.div` - position: ${({ fixed }) => (fixed ? 'fixed' : 'absolute')}; + position: ${({ fixed }) => (fixed ? "fixed" : "absolute")}; display: flex; - ${({ left }) => (left !== undefined ? `left: ${left}px` : '')}; - ${({ right }) => (right !== undefined ? `right: ${right}px` : '')}; - ${({ top }) => (top !== undefined ? `top: ${top}px` : '')}; - ${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : '')}; + ${({ left }) => (left !== undefined ? `left: ${left}px` : "")}; + ${({ right }) => (right !== undefined ? `right: ${right}px` : "")}; + ${({ top }) => (top !== undefined ? `top: ${top}px` : "")}; + ${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : "")}; max-height: 75%; z-index: 1000; transform: ${props => - props.position === 'center' ? 'translateX(-50%)' : 'initial'}; + props.position === "center" ? "translateX(-50%)" : "initial"}; pointer-events: none; `; const Menu = styled.div` animation: ${fadeAndScaleIn} 200ms ease; - transform-origin: ${props => (props.left !== undefined ? '25%' : '75%')} 0; + transform-origin: ${props => (props.left !== undefined ? "25%" : "75%")} 0; background: ${props => props.theme.menuBackground}; ${props => props.theme.menuBorder ? `border: 1px solid ${props.theme.menuBorder}` - : ''}; + : ""}; border-radius: 2px; padding: 0.5em 0; min-width: 180px; diff --git a/app/components/DropdownMenu/DropdownMenuItem.js b/app/components/DropdownMenu/DropdownMenuItem.js index e3ef4220..0526e34a 100644 --- a/app/components/DropdownMenu/DropdownMenuItem.js +++ b/app/components/DropdownMenu/DropdownMenuItem.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import { CheckmarkIcon } from 'outline-icons'; -import styled from 'styled-components'; +import * as React from "react"; +import { CheckmarkIcon } from "outline-icons"; +import styled from "styled-components"; type Props = { onClick?: (SyntheticEvent<>) => void | Promise<void>, @@ -28,7 +28,7 @@ const DropdownMenuItem = ({ {selected !== undefined && ( <React.Fragment> <CheckmarkIcon - color={selected === false ? 'transparent' : undefined} + color={selected === false ? "transparent" : undefined} />  </React.Fragment> )} @@ -57,12 +57,12 @@ const MenuItem = styled.a` } svg { - opacity: ${props => (props.disabled ? '.5' : 1)}; + opacity: ${props => (props.disabled ? ".5" : 1)}; } ${props => props.disabled - ? 'pointer-events: none;' + ? "pointer-events: none;" : ` &:hover { diff --git a/app/components/DropdownMenu/index.js b/app/components/DropdownMenu/index.js index f6b44681..b1fa231a 100644 --- a/app/components/DropdownMenu/index.js +++ b/app/components/DropdownMenu/index.js @@ -1,3 +1,3 @@ // @flow -export { default as DropdownMenu } from './DropdownMenu'; -export { default as DropdownMenuItem } from './DropdownMenuItem'; +export { default as DropdownMenu } from "./DropdownMenu"; +export { default as DropdownMenuItem } from "./DropdownMenuItem"; diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index 61591f9a..9e3e80c6 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -1,16 +1,16 @@ // @flow -import * as React from 'react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import { lighten } from 'polished'; -import styled, { withTheme } from 'styled-components'; -import RichMarkdownEditor from 'rich-markdown-editor'; -import { uploadFile } from 'utils/uploadFile'; -import isInternalUrl from 'utils/isInternalUrl'; -import Tooltip from 'components/Tooltip'; -import UiStore from 'stores/UiStore'; -import embeds from '../../embeds'; +import * as React from "react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import { lighten } from "polished"; +import styled, { withTheme } from "styled-components"; +import RichMarkdownEditor from "rich-markdown-editor"; +import { uploadFile } from "utils/uploadFile"; +import isInternalUrl from "utils/isInternalUrl"; +import Tooltip from "components/Tooltip"; +import UiStore from "stores/UiStore"; +import embeds from "../../embeds"; const EMPTY_ARRAY = []; @@ -36,7 +36,7 @@ class Editor extends React.Component<Props> { onClickLink = (href: string) => { // on page hash - if (href[0] === '#') { + if (href[0] === "#") { window.location.href = href; return; } @@ -46,7 +46,7 @@ class Editor extends React.Component<Props> { let navigateTo = href; // probably absolute - if (href[0] !== '/') { + if (href[0] !== "/") { try { const url = new URL(href); navigateTo = url.pathname + url.hash; @@ -57,7 +57,7 @@ class Editor extends React.Component<Props> { this.props.history.push(navigateTo); } else { - window.open(href, '_blank'); + window.open(href, "_blank"); } }; diff --git a/app/components/Editor/index.js b/app/components/Editor/index.js index 223d4abd..cef858be 100644 --- a/app/components/Editor/index.js +++ b/app/components/Editor/index.js @@ -1,3 +1,3 @@ // @flow -import Editor from './Editor'; +import Editor from "./Editor"; export default Editor; diff --git a/app/components/Empty.js b/app/components/Empty.js index 1c990992..4f78753a 100644 --- a/app/components/Empty.js +++ b/app/components/Empty.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; +import * as React from "react"; +import styled from "styled-components"; type Props = { children: React.Node, diff --git a/app/components/ErrorBoundary.js b/app/components/ErrorBoundary.js index c28c5369..eb1b6062 100644 --- a/app/components/ErrorBoundary.js +++ b/app/components/ErrorBoundary.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { observer } from 'mobx-react'; -import { observable } from 'mobx'; -import HelpText from 'components/HelpText'; -import Button from 'components/Button'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import { githubIssuesUrl } from '../../shared/utils/routeHelpers'; +import * as React from "react"; +import styled from "styled-components"; +import { observer } from "mobx-react"; +import { observable } from "mobx"; +import HelpText from "components/HelpText"; +import Button from "components/Button"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import { githubIssuesUrl } from "../../shared/utils/routeHelpers"; type Props = { children: React.Node, @@ -49,12 +49,12 @@ class ErrorBoundary extends React.Component<Props> { <h1>Something Unexpected Happened</h1> <HelpText> Sorry, an unrecoverable error occurred{isReported && - ' – our engineers have been notified'}. Please try reloading the + " – our engineers have been notified"}. Please try reloading the page, it may have been a temporary glitch. </HelpText> {this.showDetails && <Pre>{this.error.toString()}</Pre>} <p> - <Button onClick={this.handleReload}>Reload</Button>{' '} + <Button onClick={this.handleReload}>Reload</Button>{" "} {this.showDetails ? ( <Button onClick={this.handleReportBug} neutral> Report a Bug… diff --git a/app/components/Facepile.js b/app/components/Facepile.js index 3511bd2d..9bd9970f 100644 --- a/app/components/Facepile.js +++ b/app/components/Facepile.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import styled, { withTheme } from 'styled-components'; -import Flex from 'shared/components/Flex'; -import Avatar from 'components/Avatar'; -import User from 'models/User'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import styled, { withTheme } from "styled-components"; +import Flex from "shared/components/Flex"; +import Avatar from "components/Avatar"; +import User from "models/User"; type Props = { users: User[], @@ -73,4 +73,4 @@ const Avatars = styled(Flex)` cursor: pointer; `; -export default inject('views', 'presence')(withTheme(Facepile)); +export default inject("views", "presence")(withTheme(Facepile)); diff --git a/app/components/Fade.js b/app/components/Fade.js index d9b98b3d..bf3dc996 100644 --- a/app/components/Fade.js +++ b/app/components/Fade.js @@ -1,9 +1,9 @@ // @flow -import styled from 'styled-components'; -import { fadeIn } from 'shared/styles/animations'; +import styled from "styled-components"; +import { fadeIn } from "shared/styles/animations"; const Fade = styled.span` - animation: ${fadeIn} ${props => props.timing || '250ms'} ease-in-out; + animation: ${fadeIn} ${props => props.timing || "250ms"} ease-in-out; `; export default Fade; diff --git a/app/components/GroupListItem.js b/app/components/GroupListItem.js index 3d35c6ca..be616c47 100644 --- a/app/components/GroupListItem.js +++ b/app/components/GroupListItem.js @@ -1,17 +1,17 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { MAX_AVATAR_DISPLAY } from 'shared/constants'; -import Modal from 'components/Modal'; -import Flex from 'shared/components/Flex'; -import Facepile from 'components/Facepile'; -import GroupMembers from 'scenes/GroupMembers'; -import ListItem from 'components/List/Item'; -import Group from 'models/Group'; -import CollectionGroupMembership from 'models/CollectionGroupMembership'; -import GroupMembershipsStore from 'stores/GroupMembershipsStore'; +import * as React from "react"; +import styled from "styled-components"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import { MAX_AVATAR_DISPLAY } from "shared/constants"; +import Modal from "components/Modal"; +import Flex from "shared/components/Flex"; +import Facepile from "components/Facepile"; +import GroupMembers from "scenes/GroupMembers"; +import ListItem from "components/List/Item"; +import Group from "models/Group"; +import CollectionGroupMembership from "models/CollectionGroupMembership"; +import GroupMembershipsStore from "stores/GroupMembershipsStore"; type Props = { group: Group, @@ -53,7 +53,7 @@ class GroupListItem extends React.Component<Props> { } subtitle={ <React.Fragment> - {memberCount} member{memberCount === 1 ? '' : 's'} + {memberCount} member{memberCount === 1 ? "" : "s"} </React.Fragment> } actions={ @@ -91,4 +91,4 @@ const Title = styled.span` } `; -export default inject('groupMemberships')(GroupListItem); +export default inject("groupMemberships")(GroupListItem); diff --git a/app/components/Heading.js b/app/components/Heading.js index db4fd9db..fa47b2ec 100644 --- a/app/components/Heading.js +++ b/app/components/Heading.js @@ -1,5 +1,5 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const Heading = styled.h1` display: flex; diff --git a/app/components/HelpText.js b/app/components/HelpText.js index 1ae8e4cd..af222361 100644 --- a/app/components/HelpText.js +++ b/app/components/HelpText.js @@ -1,10 +1,10 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const HelpText = styled.p` margin-top: 0; color: ${props => props.theme.textSecondary}; - font-size: ${props => (props.small ? '13px' : 'inherit')}; + font-size: ${props => (props.small ? "13px" : "inherit")}; `; export default HelpText; diff --git a/app/components/Highlight.js b/app/components/Highlight.js index 41cc25f3..83d370b5 100644 --- a/app/components/Highlight.js +++ b/app/components/Highlight.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import replace from 'string-replace-to-array'; -import styled from 'styled-components'; +import * as React from "react"; +import replace from "string-replace-to-array"; +import styled from "styled-components"; type Props = { highlight: ?string | RegExp, @@ -22,8 +22,8 @@ function Highlight({ regex = highlight; } else { regex = new RegExp( - (highlight || '').replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'), - caseSensitive ? 'g' : 'gi' + (highlight || "").replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"), + caseSensitive ? "g" : "gi" ); } return ( diff --git a/app/components/IconPicker.js b/app/components/IconPicker.js index 5d60b17d..49c41cfb 100644 --- a/app/components/IconPicker.js +++ b/app/components/IconPicker.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import { TwitterPicker } from 'react-color'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import { TwitterPicker } from "react-color"; import { CollectionIcon, CoinsIcon, @@ -22,99 +22,99 @@ import { QuestionMarkIcon, SunIcon, VehicleIcon, -} from 'outline-icons'; -import styled from 'styled-components'; -import { LabelText } from 'components/Input'; -import { DropdownMenu } from 'components/DropdownMenu'; -import NudeButton from 'components/NudeButton'; -import Flex from 'shared/components/Flex'; +} from "outline-icons"; +import styled from "styled-components"; +import { LabelText } from "components/Input"; +import { DropdownMenu } from "components/DropdownMenu"; +import NudeButton from "components/NudeButton"; +import Flex from "shared/components/Flex"; export const icons = { collection: { component: CollectionIcon, - keywords: 'collection', + keywords: "collection", }, coins: { component: CoinsIcon, - keywords: 'coins money finance sales income revenue cash', + keywords: "coins money finance sales income revenue cash", }, academicCap: { component: AcademicCapIcon, - keywords: 'learn teach lesson guide tutorial onboarding training', + keywords: "learn teach lesson guide tutorial onboarding training", }, beaker: { component: BeakerIcon, - keywords: 'lab research experiment test', + keywords: "lab research experiment test", }, buildingBlocks: { component: BuildingBlocksIcon, - keywords: 'app blocks product prototype', + keywords: "app blocks product prototype", }, cloud: { component: CloudIcon, - keywords: 'cloud service aws infrastructure', + keywords: "cloud service aws infrastructure", }, code: { component: CodeIcon, - keywords: 'developer api code development engineering programming', + keywords: "developer api code development engineering programming", }, eye: { component: EyeIcon, - keywords: 'eye view', + keywords: "eye view", }, leaf: { component: LeafIcon, - keywords: 'leaf plant outdoors nature ecosystem climate', + keywords: "leaf plant outdoors nature ecosystem climate", }, lightbulb: { component: LightBulbIcon, - keywords: 'lightbulb idea', + keywords: "lightbulb idea", }, moon: { component: MoonIcon, - keywords: 'night moon dark', + keywords: "night moon dark", }, notepad: { component: NotepadIcon, - keywords: 'journal notepad write notes', + keywords: "journal notepad write notes", }, padlock: { component: PadlockIcon, - keywords: 'padlock private security authentication authorization auth', + keywords: "padlock private security authentication authorization auth", }, palette: { component: PaletteIcon, - keywords: 'design palette art brand', + keywords: "design palette art brand", }, pencil: { component: EditIcon, - keywords: 'copy writing post blog', + keywords: "copy writing post blog", }, question: { component: QuestionMarkIcon, - keywords: 'question help support faq', + keywords: "question help support faq", }, sun: { component: SunIcon, - keywords: 'day sun weather', + keywords: "day sun weather", }, vehicle: { component: VehicleIcon, - keywords: 'truck car travel transport', + keywords: "truck car travel transport", }, }; const colors = [ - '#4E5C6E', - '#0366d6', - '#7F6BFF', - '#E76F51', - '#FC2D2D', - '#FFBE0B', - '#2A9D8F', - '#00D084', - '#EE84F0', - '#2F362F', + "#4E5C6E", + "#0366d6", + "#7F6BFF", + "#E76F51", + "#FC2D2D", + "#FFBE0B", + "#2A9D8F", + "#00D084", + "#EE84F0", + "#2F362F", ]; type Props = { @@ -134,11 +134,11 @@ class IconPicker extends React.Component<Props> { node: ?HTMLElement; componentDidMount() { - window.addEventListener('click', this.handleClickOutside); + window.addEventListener("click", this.handleClickOutside); } componentWillUnmount() { - window.removeEventListener('click', this.handleClickOutside); + window.removeEventListener("click", this.handleClickOutside); } handleClose = () => { @@ -163,7 +163,7 @@ class IconPicker extends React.Component<Props> { }; render() { - const Component = icons[this.props.icon || 'collection'].component; + const Component = icons[this.props.icon || "collection"].component; return ( <Wrapper ref={ref => (this.node = ref)}> @@ -230,7 +230,7 @@ const ColorPicker = styled(TwitterPicker)` background: transparent !important; `; -const Wrapper = styled('div')` +const Wrapper = styled("div")` display: inline-block; position: relative; `; diff --git a/app/components/Input.js b/app/components/Input.js index b2c7414f..ad59d6ab 100644 --- a/app/components/Input.js +++ b/app/components/Input.js @@ -1,15 +1,15 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable } from 'mobx'; -import styled from 'styled-components'; -import VisuallyHidden from 'components/VisuallyHidden'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable } from "mobx"; +import styled from "styled-components"; +import VisuallyHidden from "components/VisuallyHidden"; +import Flex from "shared/components/Flex"; const RealTextarea = styled.textarea` border: 0; flex: 1; - padding: 8px 12px 8px ${props => (props.hasIcon ? '8px' : '12px')}; + padding: 8px 12px 8px ${props => (props.hasIcon ? "8px" : "12px")}; outline: none; background: none; color: ${props => props.theme.text}; @@ -23,7 +23,7 @@ const RealTextarea = styled.textarea` const RealInput = styled.input` border: 0; flex: 1; - padding: 8px 12px 8px ${props => (props.hasIcon ? '8px' : '12px')}; + padding: 8px 12px 8px ${props => (props.hasIcon ? "8px" : "12px")}; outline: none; background: none; color: ${props => props.theme.text}; @@ -40,10 +40,10 @@ const RealInput = styled.input` `; const Wrapper = styled.div` - flex: ${props => (props.flex ? '1' : '0')}; - max-width: ${props => (props.short ? '350px' : '100%')}; - min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : '0')}; - max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'initial')}; + flex: ${props => (props.flex ? "1" : "0")}; + max-width: ${props => (props.short ? "350px" : "100%")}; + min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")}; + max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "initial")}; `; const IconWrapper = styled.span` @@ -56,13 +56,13 @@ const IconWrapper = styled.span` export const Outline = styled(Flex)` display: flex; flex: 1; - margin: ${props => (props.margin !== undefined ? props.margin : '0 0 16px')}; + margin: ${props => (props.margin !== undefined ? props.margin : "0 0 16px")}; color: inherit; border-width: 1px; border-style: solid; border-color: ${props => props.hasError - ? 'red' + ? "red" : props.focused ? props.theme.inputBorderFocused : props.theme.inputBorder}; @@ -118,7 +118,7 @@ class Input extends React.Component<Props> { render() { const { - type = 'text', + type = "text", icon, label, margin, @@ -131,7 +131,7 @@ class Input extends React.Component<Props> { ...rest } = this.props; - const InputComponent = type === 'textarea' ? RealTextarea : RealInput; + const InputComponent = type === "textarea" ? RealTextarea : RealInput; const wrappedLabel = <LabelText>{label}</LabelText>; return ( @@ -149,7 +149,7 @@ class Input extends React.Component<Props> { ref={ref => (this.input = ref)} onBlur={this.handleBlur} onFocus={this.handleFocus} - type={type === 'textarea' ? undefined : type} + type={type === "textarea" ? undefined : type} hasIcon={!!icon} {...rest} /> diff --git a/app/components/InputRich.js b/app/components/InputRich.js index 087c54f9..ee8c93fe 100644 --- a/app/components/InputRich.js +++ b/app/components/InputRich.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import styled, { withTheme } from 'styled-components'; -import { LabelText, Outline } from 'components/Input'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import styled, { withTheme } from "styled-components"; +import { LabelText, Outline } from "components/Input"; type Props = { label: string, @@ -31,7 +31,7 @@ class InputRich extends React.Component<Props> { loadEditor = async () => { try { - const EditorImport = await import('./Editor'); + const EditorImport = await import("./Editor"); this.editorComponent = EditorImport.default; } catch (err) { console.error(err); @@ -64,7 +64,7 @@ class InputRich extends React.Component<Props> { {...rest} /> ) : ( - 'Loading…' + "Loading…" )} </StyledOutline> </React.Fragment> @@ -74,8 +74,8 @@ class InputRich extends React.Component<Props> { const StyledOutline = styled(Outline)` padding: 8px 12px; - min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : '0')}; - max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'auto')}; + min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")}; + max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "auto")}; overflow-y: auto; > * { diff --git a/app/components/InputSearch.js b/app/components/InputSearch.js index f318db3d..be7053ee 100644 --- a/app/components/InputSearch.js +++ b/app/components/InputSearch.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import keydown from 'react-keydown'; -import { observer } from 'mobx-react'; -import { observable } from 'mobx'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import styled, { withTheme } from 'styled-components'; -import { SearchIcon } from 'outline-icons'; -import { searchUrl } from 'utils/routeHelpers'; -import Input from './Input'; +import * as React from "react"; +import keydown from "react-keydown"; +import { observer } from "mobx-react"; +import { observable } from "mobx"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import styled, { withTheme } from "styled-components"; +import { SearchIcon } from "outline-icons"; +import { searchUrl } from "utils/routeHelpers"; +import Input from "./Input"; type Props = { history: RouterHistory, @@ -21,7 +21,7 @@ class InputSearch extends React.Component<Props> { input: ?Input; @observable focused: boolean = false; - @keydown('meta+f') + @keydown("meta+f") focus(ev) { ev.preventDefault(); @@ -46,7 +46,7 @@ class InputSearch extends React.Component<Props> { }; render() { - const { theme, placeholder = 'Search…' } = this.props; + const { theme, placeholder = "Search…" } = this.props; return ( <InputMaxWidth diff --git a/app/components/InputSelect.js b/app/components/InputSelect.js index d60689b7..726e24b5 100644 --- a/app/components/InputSelect.js +++ b/app/components/InputSelect.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable } from 'mobx'; -import styled from 'styled-components'; -import VisuallyHidden from 'components/VisuallyHidden'; -import { Outline, LabelText } from './Input'; +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable } from "mobx"; +import styled from "styled-components"; +import VisuallyHidden from "components/VisuallyHidden"; +import { Outline, LabelText } from "./Input"; const Select = styled.select` border: 0; diff --git a/app/components/Key.js b/app/components/Key.js index 092a8c89..98757f9f 100644 --- a/app/components/Key.js +++ b/app/components/Key.js @@ -1,10 +1,10 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const Key = styled.kbd` display: inline-block; padding: 4px 6px; - font: 11px 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, + font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 10px; color: ${props => props.theme.almostBlack}; diff --git a/app/components/Labeled.js b/app/components/Labeled.js index 1ae75454..4fda039b 100644 --- a/app/components/Labeled.js +++ b/app/components/Labeled.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import Flex from 'shared/components/Flex'; -import styled from 'styled-components'; +import * as React from "react"; +import { observer } from "mobx-react"; +import Flex from "shared/components/Flex"; +import styled from "styled-components"; type Props = { label: React.Node | string, diff --git a/app/components/Layout.js b/app/components/Layout.js index d48ce0f4..8349be3d 100644 --- a/app/components/Layout.js +++ b/app/components/Layout.js @@ -1,32 +1,32 @@ // @flow -import * as React from 'react'; -import { Switch, Route, Redirect } from 'react-router-dom'; -import { Helmet } from 'react-helmet'; -import styled, { withTheme } from 'styled-components'; -import breakpoint from 'styled-components-breakpoint'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import keydown from 'react-keydown'; -import Analytics from 'components/Analytics'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { Switch, Route, Redirect } from "react-router-dom"; +import { Helmet } from "react-helmet"; +import styled, { withTheme } from "styled-components"; +import breakpoint from "styled-components-breakpoint"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import keydown from "react-keydown"; +import Analytics from "components/Analytics"; +import Flex from "shared/components/Flex"; import { homeUrl, searchUrl, matchDocumentSlug as slug, -} from 'utils/routeHelpers'; +} from "utils/routeHelpers"; -import { LoadingIndicatorBar } from 'components/LoadingIndicator'; -import { GlobalStyles } from 'components/DropToImport'; -import Sidebar from 'components/Sidebar'; -import SettingsSidebar from 'components/Sidebar/Settings'; -import Modals from 'components/Modals'; -import DocumentHistory from 'components/DocumentHistory'; -import Modal from 'components/Modal'; -import KeyboardShortcuts from 'scenes/KeyboardShortcuts'; -import ErrorSuspended from 'scenes/ErrorSuspended'; -import AuthStore from 'stores/AuthStore'; -import UiStore from 'stores/UiStore'; -import DocumentsStore from 'stores/DocumentsStore'; +import { LoadingIndicatorBar } from "components/LoadingIndicator"; +import { GlobalStyles } from "components/DropToImport"; +import Sidebar from "components/Sidebar"; +import SettingsSidebar from "components/Sidebar/Settings"; +import Modals from "components/Modals"; +import DocumentHistory from "components/DocumentHistory"; +import Modal from "components/Modal"; +import KeyboardShortcuts from "scenes/KeyboardShortcuts"; +import ErrorSuspended from "scenes/ErrorSuspended"; +import AuthStore from "stores/AuthStore"; +import UiStore from "stores/UiStore"; +import DocumentsStore from "stores/DocumentsStore"; type Props = { documents: DocumentsStore, @@ -62,7 +62,7 @@ class Layout extends React.Component<Props> { window.document.body.style.background = this.props.theme.background; } - @keydown('shift+/') + @keydown("shift+/") handleOpenKeyboardShortcuts() { if (this.props.ui.editMode) return; this.keyboardShortcutsOpen = true; @@ -72,7 +72,7 @@ class Layout extends React.Component<Props> { this.keyboardShortcutsOpen = false; }; - @keydown(['t', '/', 'meta+k']) + @keydown(["t", "/", "meta+k"]) goToSearch(ev) { if (this.props.ui.editMode) return; ev.preventDefault(); @@ -80,7 +80,7 @@ class Layout extends React.Component<Props> { this.redirectTo = searchUrl(); } - @keydown('d') + @keydown("d") goToDashboard() { if (this.props.ui.editMode) return; this.redirectTo = homeUrl(); @@ -157,9 +157,9 @@ const Content = styled(Flex)` margin: 0; } - ${breakpoint('tablet')` + ${breakpoint("tablet")` margin-left: ${props => (props.editMode ? 0 : props.theme.sidebarWidth)}; `}; `; -export default inject('auth', 'ui', 'documents')(withTheme(Layout)); +export default inject("auth", "ui", "documents")(withTheme(Layout)); diff --git a/app/components/List/Item.js b/app/components/List/Item.js index 1394ff00..e4104447 100644 --- a/app/components/List/Item.js +++ b/app/components/List/Item.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import styled from "styled-components"; +import Flex from "shared/components/Flex"; type Props = { image?: React.Node, @@ -16,7 +16,7 @@ const ListItem = ({ image, title, subtitle, actions }: Props) => { return ( <Wrapper compact={compact}> {image && <Image>{image}</Image>} - <Content align={compact ? 'center' : undefined} column={!compact}> + <Content align={compact ? "center" : undefined} column={!compact}> <Heading>{title}</Heading> {subtitle && <Subtitle>{subtitle}</Subtitle>} </Content> @@ -27,7 +27,7 @@ const ListItem = ({ image, title, subtitle, actions }: Props) => { const Wrapper = styled.li` display: flex; - padding: ${props => (props.compact ? '8px' : '12px')} 0; + padding: ${props => (props.compact ? "8px" : "12px")} 0; margin: 0; border-bottom: 1px solid ${props => props.theme.divider}; diff --git a/app/components/List/List.js b/app/components/List/List.js index 892580d6..33fb89e0 100644 --- a/app/components/List/List.js +++ b/app/components/List/List.js @@ -1,5 +1,5 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const List = styled.ol` margin: 0; diff --git a/app/components/List/Placeholder.js b/app/components/List/Placeholder.js index 51c6f3de..de1fbcb0 100644 --- a/app/components/List/Placeholder.js +++ b/app/components/List/Placeholder.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import { times } from 'lodash'; -import styled from 'styled-components'; -import Mask from 'components/Mask'; -import Fade from 'components/Fade'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { times } from "lodash"; +import styled from "styled-components"; +import Mask from "components/Mask"; +import Fade from "components/Fade"; +import Flex from "shared/components/Flex"; type Props = { count?: number, diff --git a/app/components/List/index.js b/app/components/List/index.js index 01a19157..d31f1b04 100644 --- a/app/components/List/index.js +++ b/app/components/List/index.js @@ -1,3 +1,3 @@ // @flow -import List from './List'; +import List from "./List"; export default List; diff --git a/app/components/LoadingIndicator/LoadingIndicator.js b/app/components/LoadingIndicator/LoadingIndicator.js index d191452c..5f08a746 100644 --- a/app/components/LoadingIndicator/LoadingIndicator.js +++ b/app/components/LoadingIndicator/LoadingIndicator.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import { inject, observer } from 'mobx-react'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import { inject, observer } from "mobx-react"; +import UiStore from "stores/UiStore"; type Props = { ui: UiStore, @@ -22,4 +22,4 @@ class LoadingIndicator extends React.Component<Props> { } } -export default inject('ui')(LoadingIndicator); +export default inject("ui")(LoadingIndicator); diff --git a/app/components/LoadingIndicator/LoadingIndicatorBar.js b/app/components/LoadingIndicator/LoadingIndicatorBar.js index b6d42a34..e6b4b392 100644 --- a/app/components/LoadingIndicator/LoadingIndicatorBar.js +++ b/app/components/LoadingIndicator/LoadingIndicatorBar.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import styled, { keyframes } from 'styled-components'; +import * as React from "react"; +import styled, { keyframes } from "styled-components"; const LoadingIndicatorBar = () => { return ( diff --git a/app/components/LoadingIndicator/index.js b/app/components/LoadingIndicator/index.js index fe3e594b..f90b3ecb 100644 --- a/app/components/LoadingIndicator/index.js +++ b/app/components/LoadingIndicator/index.js @@ -1,5 +1,5 @@ // @flow -import LoadingIndicator from './LoadingIndicator'; -import LoadingIndicatorBar from './LoadingIndicatorBar'; +import LoadingIndicator from "./LoadingIndicator"; +import LoadingIndicatorBar from "./LoadingIndicatorBar"; export default LoadingIndicator; export { LoadingIndicatorBar }; diff --git a/app/components/LoadingPlaceholder/ListPlaceholder.js b/app/components/LoadingPlaceholder/ListPlaceholder.js index aba0e53d..586cdfd1 100644 --- a/app/components/LoadingPlaceholder/ListPlaceholder.js +++ b/app/components/LoadingPlaceholder/ListPlaceholder.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import { times } from 'lodash'; -import styled from 'styled-components'; -import Mask from 'components/Mask'; -import Fade from 'components/Fade'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { times } from "lodash"; +import styled from "styled-components"; +import Mask from "components/Mask"; +import Fade from "components/Fade"; +import Flex from "shared/components/Flex"; type Props = { count?: number, diff --git a/app/components/LoadingPlaceholder/LoadingPlaceholder.js b/app/components/LoadingPlaceholder/LoadingPlaceholder.js index b20d66e7..75e0fc85 100644 --- a/app/components/LoadingPlaceholder/LoadingPlaceholder.js +++ b/app/components/LoadingPlaceholder/LoadingPlaceholder.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Mask from 'components/Mask'; -import Fade from 'components/Fade'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import styled from "styled-components"; +import Mask from "components/Mask"; +import Fade from "components/Fade"; +import Flex from "shared/components/Flex"; export default function LoadingPlaceholder(props: Object) { return ( diff --git a/app/components/LoadingPlaceholder/index.js b/app/components/LoadingPlaceholder/index.js index 62e7311e..54559704 100644 --- a/app/components/LoadingPlaceholder/index.js +++ b/app/components/LoadingPlaceholder/index.js @@ -1,6 +1,6 @@ // @flow -import LoadingPlaceholder from './LoadingPlaceholder'; -import ListPlaceholder from './ListPlaceholder'; +import LoadingPlaceholder from "./LoadingPlaceholder"; +import ListPlaceholder from "./ListPlaceholder"; export default LoadingPlaceholder; export { ListPlaceholder }; diff --git a/app/components/Mask.js b/app/components/Mask.js index 86d5c3a6..cc36ee9c 100644 --- a/app/components/Mask.js +++ b/app/components/Mask.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { pulsate } from 'shared/styles/animations'; -import { randomInteger } from 'shared/random'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import styled from "styled-components"; +import { pulsate } from "shared/styles/animations"; +import { randomInteger } from "shared/random"; +import Flex from "shared/components/Flex"; type Props = { header?: boolean, diff --git a/app/components/Modal.js b/app/components/Modal.js index 57b6aff9..3828393a 100644 --- a/app/components/Modal.js +++ b/app/components/Modal.js @@ -1,16 +1,16 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import styled, { createGlobalStyle } from 'styled-components'; -import breakpoint from 'styled-components-breakpoint'; -import ReactModal from 'react-modal'; -import { transparentize } from 'polished'; -import { CloseIcon, BackIcon } from 'outline-icons'; -import NudeButton from 'components/NudeButton'; -import { fadeAndScaleIn } from 'shared/styles/animations'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { observer } from "mobx-react"; +import styled, { createGlobalStyle } from "styled-components"; +import breakpoint from "styled-components-breakpoint"; +import ReactModal from "react-modal"; +import { transparentize } from "polished"; +import { CloseIcon, BackIcon } from "outline-icons"; +import NudeButton from "components/NudeButton"; +import { fadeAndScaleIn } from "shared/styles/animations"; +import Flex from "shared/components/Flex"; -ReactModal.setAppElement('#root'); +ReactModal.setAppElement("#root"); type Props = { children?: React.Node, @@ -26,7 +26,7 @@ const GlobalStyles = createGlobalStyle` z-index: 100; } - ${breakpoint('tablet')` + ${breakpoint("tablet")` .ReactModalPortal + .ReactModalPortal { .ReactModal__Overlay { margin-left: 12px; @@ -57,7 +57,7 @@ const GlobalStyles = createGlobalStyle` const Modal = ({ children, isOpen, - title = 'Untitled', + title = "Untitled", onRequestClose, ...rest }: Props) => { @@ -114,7 +114,7 @@ const StyledModal = styled(ReactModal)` padding: 8vh 2rem 2rem; outline: none; - ${breakpoint('tablet')` + ${breakpoint("tablet")` padding-top: 13vh; `}; `; @@ -141,7 +141,7 @@ const Close = styled(NudeButton)` opacity: 1; } - ${breakpoint('tablet')` + ${breakpoint("tablet")` display: none; `}; `; @@ -161,7 +161,7 @@ const Back = styled(NudeButton)` opacity: 1; } - ${breakpoint('tablet')` + ${breakpoint("tablet")` display: flex; `}; `; diff --git a/app/components/Modals.js b/app/components/Modals.js index 64c360e1..67be2c4e 100644 --- a/app/components/Modals.js +++ b/app/components/Modals.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import BaseModal from 'components/Modal'; -import UiStore from 'stores/UiStore'; -import CollectionNew from 'scenes/CollectionNew'; -import CollectionEdit from 'scenes/CollectionEdit'; -import CollectionDelete from 'scenes/CollectionDelete'; -import CollectionExport from 'scenes/CollectionExport'; -import DocumentDelete from 'scenes/DocumentDelete'; -import DocumentShare from 'scenes/DocumentShare'; +import * as React from "react"; +import { observer } from "mobx-react"; +import BaseModal from "components/Modal"; +import UiStore from "stores/UiStore"; +import CollectionNew from "scenes/CollectionNew"; +import CollectionEdit from "scenes/CollectionEdit"; +import CollectionDelete from "scenes/CollectionDelete"; +import CollectionExport from "scenes/CollectionExport"; +import DocumentDelete from "scenes/DocumentDelete"; +import DocumentShare from "scenes/DocumentShare"; type Props = { ui: UiStore, diff --git a/app/components/NudeButton.js b/app/components/NudeButton.js index 7a847f9d..5461c923 100644 --- a/app/components/NudeButton.js +++ b/app/components/NudeButton.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { lighten } from 'polished'; +import * as React from "react"; +import styled from "styled-components"; +import { lighten } from "polished"; const Button = styled.button` width: 24px; diff --git a/app/components/PageTitle.js b/app/components/PageTitle.js index f4e79228..481646b4 100644 --- a/app/components/PageTitle.js +++ b/app/components/PageTitle.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import { Helmet } from 'react-helmet'; +import * as React from "react"; +import { Helmet } from "react-helmet"; type Props = { title: string, @@ -13,7 +13,7 @@ const PageTitle = ({ title, favicon }: Props) => ( <link rel="shortcut icon" type="image/png" - href={favicon || '/favicon-32.png'} + href={favicon || "/favicon-32.png"} sizes="32x32" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> diff --git a/app/components/PaginatedDocumentList.js b/app/components/PaginatedDocumentList.js index e856d66a..7d45a7be 100644 --- a/app/components/PaginatedDocumentList.js +++ b/app/components/PaginatedDocumentList.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import Document from 'models/Document'; -import DocumentPreview from 'components/DocumentPreview'; -import PaginatedList from 'components/PaginatedList'; +import * as React from "react"; +import { observer } from "mobx-react"; +import Document from "models/Document"; +import DocumentPreview from "components/DocumentPreview"; +import PaginatedList from "components/PaginatedList"; type Props = { documents: Document[], diff --git a/app/components/PaginatedList.js b/app/components/PaginatedList.js index 6611c926..7440ef73 100644 --- a/app/components/PaginatedList.js +++ b/app/components/PaginatedList.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import { observable, action } from 'mobx'; -import { observer } from 'mobx-react'; -import { Waypoint } from 'react-waypoint'; -import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; +import * as React from "react"; +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { Waypoint } from "react-waypoint"; +import ArrowKeyNavigation from "boundless-arrow-key-navigation"; -import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore'; -import { ListPlaceholder } from 'components/LoadingPlaceholder'; +import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore"; +import { ListPlaceholder } from "components/LoadingPlaceholder"; type Props = { fetch?: (options: ?Object) => Promise<void>, diff --git a/app/components/PathToDocument.js b/app/components/PathToDocument.js index 942b4b4e..ad1c3c62 100644 --- a/app/components/PathToDocument.js +++ b/app/components/PathToDocument.js @@ -1,21 +1,21 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import styled from 'styled-components'; -import { GoToIcon } from 'outline-icons'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { observer } from "mobx-react"; +import styled from "styled-components"; +import { GoToIcon } from "outline-icons"; +import Flex from "shared/components/Flex"; -import Document from 'models/Document'; -import Collection from 'models/Collection'; -import type { DocumentPath } from 'stores/CollectionsStore'; -import CollectionIcon from 'components/CollectionIcon'; +import Document from "models/Document"; +import Collection from "models/Collection"; +import type { DocumentPath } from "stores/CollectionsStore"; +import CollectionIcon from "components/CollectionIcon"; type Props = { result: DocumentPath, document?: ?Document, collection: ?Collection, onSuccess?: () => void, - ref?: (?React.ElementRef<'div'>) => void, + ref?: (?React.ElementRef<"div">) => void, }; @observer @@ -25,7 +25,7 @@ class PathToDocument extends React.Component<Props> { const { document, result, onSuccess } = this.props; if (!document) return; - if (result.type === 'document') { + if (result.type === "document") { await document.move(result.collectionId, result.id); } else { await document.move(result.collectionId, null); @@ -48,7 +48,7 @@ class PathToDocument extends React.Component<Props> { .reduce((prev, curr) => [prev, <StyledGoToIcon />, curr])} {document && ( <Flex> - {' '} + {" "} <StyledGoToIcon /> <Title>{document.title} )} @@ -77,7 +77,7 @@ const ResultWrapper = styled.div` cursor: default; `; -const ResultWrapperLink = styled(ResultWrapper.withComponent('a'))` +const ResultWrapperLink = styled(ResultWrapper.withComponent("a"))` margin: 0 -8px; padding: 8px 4px; border-radius: 8px; diff --git a/app/components/Popover.js b/app/components/Popover.js index 641e19e8..c6c904e9 100644 --- a/app/components/Popover.js +++ b/app/components/Popover.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import BoundlessPopover from 'boundless-popover'; -import styled, { keyframes } from 'styled-components'; +import * as React from "react"; +import BoundlessPopover from "boundless-popover"; +import styled, { keyframes } from "styled-components"; const fadeIn = keyframes` from { diff --git a/app/components/PublishingInfo.js b/app/components/PublishingInfo.js index bb17571d..ce9ef0ca 100644 --- a/app/components/PublishingInfo.js +++ b/app/components/PublishingInfo.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import { inject, observer } from 'mobx-react'; -import styled from 'styled-components'; -import Document from 'models/Document'; -import Flex from 'shared/components/Flex'; -import Time from 'shared/components/Time'; -import Breadcrumb from 'shared/components/Breadcrumb'; -import CollectionsStore from 'stores/CollectionsStore'; -import AuthStore from 'stores/AuthStore'; +import * as React from "react"; +import { inject, observer } from "mobx-react"; +import styled from "styled-components"; +import Document from "models/Document"; +import Flex from "shared/components/Flex"; +import Time from "shared/components/Time"; +import Breadcrumb from "shared/components/Breadcrumb"; +import CollectionsStore from "stores/CollectionsStore"; +import AuthStore from "stores/AuthStore"; const Container = styled(Flex)` color: ${props => props.theme.textTertiary}; @@ -19,7 +19,7 @@ const Container = styled(Flex)` const Modified = styled.span` color: ${props => props.highlight ? props.theme.text : props.theme.textTertiary}; - font-weight: ${props => (props.highlight ? '600' : '400')}; + font-weight: ${props => (props.highlight ? "600" : "400")}; `; type Props = { @@ -102,7 +102,7 @@ function PublishingInfo({ return ( - {updatedByMe ? 'You' : updatedBy.name}  + {updatedByMe ? "You" : updatedBy.name}  {content} {showCollection && collection && ( @@ -118,4 +118,4 @@ function PublishingInfo({ ); } -export default inject('collections', 'auth')(observer(PublishingInfo)); +export default inject("collections", "auth")(observer(PublishingInfo)); diff --git a/app/components/ScrollToTop.js b/app/components/ScrollToTop.js index 9b9cea20..115159e0 100644 --- a/app/components/ScrollToTop.js +++ b/app/components/ScrollToTop.js @@ -1,8 +1,8 @@ // @flow // based on: https://reacttraining.com/react-router/web/guides/scroll-restoration -import * as React from 'react'; -import { withRouter } from 'react-router-dom'; -import type { Location } from 'react-router-dom'; +import * as React from "react"; +import { withRouter } from "react-router-dom"; +import type { Location } from "react-router-dom"; type Props = { location: Location, diff --git a/app/components/Scrollable.js b/app/components/Scrollable.js index 7a145948..ffabcfe6 100644 --- a/app/components/Scrollable.js +++ b/app/components/Scrollable.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import styled from 'styled-components'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import styled from "styled-components"; type Props = { shadow?: boolean, @@ -32,7 +32,7 @@ const Wrapper = styled.div` overscroll-behavior: none; -webkit-overflow-scrolling: touch; box-shadow: ${props => - props.shadow ? '0 1px inset rgba(0,0,0,.1)' : 'none'}; + props.shadow ? "0 1px inset rgba(0,0,0,.1)" : "none"}; transition: all 250ms ease-in-out; `; diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js index 8c79c8b6..88be199a 100644 --- a/app/components/Sidebar/Main.js +++ b/app/components/Sidebar/Main.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import styled from 'styled-components'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import styled from "styled-components"; import { ArchiveIcon, HomeIcon, @@ -10,25 +10,25 @@ import { StarredIcon, TrashIcon, PlusIcon, -} from 'outline-icons'; +} from "outline-icons"; -import Flex from 'shared/components/Flex'; -import Modal from 'components/Modal'; -import Invite from 'scenes/Invite'; -import AccountMenu from 'menus/AccountMenu'; -import Sidebar from './Sidebar'; -import Scrollable from 'components/Scrollable'; -import Section from './components/Section'; -import Collections from './components/Collections'; -import SidebarLink from './components/SidebarLink'; -import HeaderBlock from './components/HeaderBlock'; -import Bubble from './components/Bubble'; +import Flex from "shared/components/Flex"; +import Modal from "components/Modal"; +import Invite from "scenes/Invite"; +import AccountMenu from "menus/AccountMenu"; +import Sidebar from "./Sidebar"; +import Scrollable from "components/Scrollable"; +import Section from "./components/Section"; +import Collections from "./components/Collections"; +import SidebarLink from "./components/SidebarLink"; +import HeaderBlock from "./components/HeaderBlock"; +import Bubble from "./components/Bubble"; -import AuthStore from 'stores/AuthStore'; -import DocumentsStore from 'stores/DocumentsStore'; -import PoliciesStore from 'stores/PoliciesStore'; -import UiStore from 'stores/UiStore'; -import { observable } from 'mobx'; +import AuthStore from "stores/AuthStore"; +import DocumentsStore from "stores/DocumentsStore"; +import PoliciesStore from "stores/PoliciesStore"; +import UiStore from "stores/UiStore"; +import { observable } from "mobx"; type Props = { auth: AuthStore, @@ -47,7 +47,7 @@ class MainSidebar extends React.Component { handleCreateCollection = (ev: SyntheticEvent<>) => { ev.preventDefault(); - this.props.ui.setActiveModal('collection-new'); + this.props.ui.setActiveModal("collection-new"); }; handleInviteModalOpen = (ev: SyntheticEvent<>) => { @@ -90,7 +90,7 @@ class MainSidebar extends React.Component { /> } @@ -172,4 +172,4 @@ const Drafts = styled(Flex)` height: 24px; `; -export default inject('documents', 'policies', 'auth', 'ui')(MainSidebar); +export default inject("documents", "policies", "auth", "ui")(MainSidebar); diff --git a/app/components/Sidebar/Settings.js b/app/components/Sidebar/Settings.js index c4c0731a..3e064039 100644 --- a/app/components/Sidebar/Settings.js +++ b/app/components/Sidebar/Settings.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import type { RouterHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import type { RouterHistory } from "react-router-dom"; +import styled from "styled-components"; import { DocumentIcon, EmailIcon, @@ -15,20 +15,20 @@ import { TeamIcon, BulletedListIcon, ExpandedIcon, -} from 'outline-icons'; -import ZapierIcon from './icons/Zapier'; -import SlackIcon from './icons/Slack'; +} from "outline-icons"; +import ZapierIcon from "./icons/Zapier"; +import SlackIcon from "./icons/Slack"; -import Flex from 'shared/components/Flex'; -import Sidebar from './Sidebar'; -import Scrollable from 'components/Scrollable'; -import Section from './components/Section'; -import Header from './components/Header'; -import SidebarLink from './components/SidebarLink'; -import HeaderBlock from './components/HeaderBlock'; -import Version from './components/Version'; -import PoliciesStore from 'stores/PoliciesStore'; -import AuthStore from 'stores/AuthStore'; +import Flex from "shared/components/Flex"; +import Sidebar from "./Sidebar"; +import Scrollable from "components/Scrollable"; +import Section from "./components/Section"; +import Header from "./components/Header"; +import SidebarLink from "./components/SidebarLink"; +import HeaderBlock from "./components/HeaderBlock"; +import Version from "./components/Version"; +import PoliciesStore from "stores/PoliciesStore"; +import AuthStore from "stores/AuthStore"; type Props = { history: RouterHistory, @@ -39,7 +39,7 @@ type Props = { @observer class SettingsSidebar extends React.Component { returnToDashboard = () => { - this.props.history.push('/'); + this.props.history.push("/"); }; render() { @@ -146,7 +146,7 @@ class SettingsSidebar extends React.Component { )} {can.update && - process.env.DEPLOYMENT !== 'hosted' && ( + process.env.DEPLOYMENT !== "hosted" && (
Installation
@@ -168,4 +168,4 @@ const ReturnToApp = styled(Flex)` height: 16px; `; -export default inject('auth', 'policies')(SettingsSidebar); +export default inject("auth", "policies")(SettingsSidebar); diff --git a/app/components/Sidebar/Sidebar.js b/app/components/Sidebar/Sidebar.js index d13693eb..17394ac0 100644 --- a/app/components/Sidebar/Sidebar.js +++ b/app/components/Sidebar/Sidebar.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { withRouter } from 'react-router-dom'; -import type { Location } from 'react-router-dom'; -import styled from 'styled-components'; -import breakpoint from 'styled-components-breakpoint'; -import { observer, inject } from 'mobx-react'; -import { CloseIcon, MenuIcon } from 'outline-icons'; -import Fade from 'components/Fade'; -import Flex from 'shared/components/Flex'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import { withRouter } from "react-router-dom"; +import type { Location } from "react-router-dom"; +import styled from "styled-components"; +import breakpoint from "styled-components-breakpoint"; +import { observer, inject } from "mobx-react"; +import { CloseIcon, MenuIcon } from "outline-icons"; +import Fade from "components/Fade"; +import Flex from "shared/components/Flex"; +import UiStore from "stores/UiStore"; let firstRender = true; @@ -69,7 +69,7 @@ const Container = styled(Flex)` width: 100%; background: ${props => props.theme.sidebarBackground}; transition: left 100ms ease-out, ${props => props.theme.backgroundTransition}; - margin-left: ${props => (props.mobileSidebarVisible ? 0 : '-100%')}; + margin-left: ${props => (props.mobileSidebarVisible ? 0 : "-100%")}; z-index: 1000; @media print { @@ -79,7 +79,7 @@ const Container = styled(Flex)` &:before, &:after { - content: ''; + content: ""; background: ${props => props.theme.sidebarBackground}; position: absolute; top: -50vh; @@ -93,7 +93,7 @@ const Container = styled(Flex)` bottom: -50vh; } - ${breakpoint('tablet')` + ${breakpoint("tablet")` left: ${props => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)}; width: ${props => props.theme.sidebarWidth}; margin: 0; @@ -106,14 +106,14 @@ const Toggle = styled.a` align-items: center; position: fixed; top: 0; - left: ${props => (props.mobileSidebarVisible ? 'auto' : 0)}; - right: ${props => (props.mobileSidebarVisible ? 0 : 'auto')}; + left: ${props => (props.mobileSidebarVisible ? "auto" : 0)}; + right: ${props => (props.mobileSidebarVisible ? 0 : "auto")}; z-index: 1; margin: 12px; - ${breakpoint('tablet')` + ${breakpoint("tablet")` display: none; `}; `; -export default withRouter(inject('ui')(Sidebar)); +export default withRouter(inject("ui")(Sidebar)); diff --git a/app/components/Sidebar/components/Bubble.js b/app/components/Sidebar/components/Bubble.js index 8f10490d..1767bde2 100644 --- a/app/components/Sidebar/components/Bubble.js +++ b/app/components/Sidebar/components/Bubble.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { bounceIn } from 'shared/styles/animations'; +import * as React from "react"; +import styled from "styled-components"; +import { bounceIn } from "shared/styles/animations"; type Props = { count: number, @@ -17,7 +17,7 @@ const Count = styled.div` color: ${props => props.theme.white}; background: ${props => props.theme.slateDark}; display: inline-block; - font-feature-settings: 'tnum'; + font-feature-settings: "tnum"; font-weight: 600; font-size: 9px; white-space: nowrap; diff --git a/app/components/Sidebar/components/CollectionLink.js b/app/components/Sidebar/components/CollectionLink.js index 107c8683..3c311ed3 100644 --- a/app/components/Sidebar/components/CollectionLink.js +++ b/app/components/Sidebar/components/CollectionLink.js @@ -1,17 +1,17 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable } from 'mobx'; -import Collection from 'models/Collection'; -import Document from 'models/Document'; -import CollectionMenu from 'menus/CollectionMenu'; -import UiStore from 'stores/UiStore'; -import DocumentsStore from 'stores/DocumentsStore'; -import SidebarLink from './SidebarLink'; -import DocumentLink from './DocumentLink'; -import CollectionIcon from 'components/CollectionIcon'; -import DropToImport from 'components/DropToImport'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable } from "mobx"; +import Collection from "models/Collection"; +import Document from "models/Document"; +import CollectionMenu from "menus/CollectionMenu"; +import UiStore from "stores/UiStore"; +import DocumentsStore from "stores/DocumentsStore"; +import SidebarLink from "./SidebarLink"; +import DocumentLink from "./DocumentLink"; +import CollectionIcon from "components/CollectionIcon"; +import DropToImport from "components/DropToImport"; +import Flex from "shared/components/Flex"; type Props = { collection: Collection, diff --git a/app/components/Sidebar/components/Collections.js b/app/components/Sidebar/components/Collections.js index 10c9834e..771e23c5 100644 --- a/app/components/Sidebar/components/Collections.js +++ b/app/components/Sidebar/components/Collections.js @@ -1,22 +1,22 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import keydown from 'react-keydown'; -import Flex from 'shared/components/Flex'; -import { PlusIcon } from 'outline-icons'; -import { newDocumentUrl } from 'utils/routeHelpers'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import keydown from "react-keydown"; +import Flex from "shared/components/Flex"; +import { PlusIcon } from "outline-icons"; +import { newDocumentUrl } from "utils/routeHelpers"; -import Header from './Header'; -import SidebarLink from './SidebarLink'; -import CollectionLink from './CollectionLink'; -import CollectionsLoading from './CollectionsLoading'; -import Fade from 'components/Fade'; +import Header from "./Header"; +import SidebarLink from "./SidebarLink"; +import CollectionLink from "./CollectionLink"; +import CollectionsLoading from "./CollectionsLoading"; +import Fade from "components/Fade"; -import CollectionsStore from 'stores/CollectionsStore'; -import PoliciesStore from 'stores/PoliciesStore'; -import UiStore from 'stores/UiStore'; -import DocumentsStore from 'stores/DocumentsStore'; +import CollectionsStore from "stores/CollectionsStore"; +import PoliciesStore from "stores/PoliciesStore"; +import UiStore from "stores/UiStore"; +import DocumentsStore from "stores/DocumentsStore"; type Props = { history: RouterHistory, @@ -39,7 +39,7 @@ class Collections extends React.Component { } } - @keydown('n') + @keydown("n") goToNewDocument() { if (this.props.ui.editMode) return; @@ -94,6 +94,6 @@ class Collections extends React.Component { } } -export default inject('collections', 'ui', 'documents', 'policies')( +export default inject("collections", "ui", "documents", "policies")( withRouter(Collections) ); diff --git a/app/components/Sidebar/components/CollectionsLoading.js b/app/components/Sidebar/components/CollectionsLoading.js index f3e48894..d2a9c995 100644 --- a/app/components/Sidebar/components/CollectionsLoading.js +++ b/app/components/Sidebar/components/CollectionsLoading.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Mask from 'components/Mask'; +import * as React from "react"; +import styled from "styled-components"; +import Mask from "components/Mask"; function CollectionsLoading() { return ( diff --git a/app/components/Sidebar/components/DocumentLink.js b/app/components/Sidebar/components/DocumentLink.js index 8d4ef39b..4cc216fc 100644 --- a/app/components/Sidebar/components/DocumentLink.js +++ b/app/components/Sidebar/components/DocumentLink.js @@ -1,17 +1,17 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable } from 'mobx'; -import styled from 'styled-components'; -import Document from 'models/Document'; -import DocumentMenu from 'menus/DocumentMenu'; -import SidebarLink from './SidebarLink'; -import DropToImport from 'components/DropToImport'; -import Fade from 'components/Fade'; -import Collection from 'models/Collection'; -import DocumentsStore from 'stores/DocumentsStore'; -import Flex from 'shared/components/Flex'; -import { type NavigationNode } from 'types'; +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable } from "mobx"; +import styled from "styled-components"; +import Document from "models/Document"; +import DocumentMenu from "menus/DocumentMenu"; +import SidebarLink from "./SidebarLink"; +import DropToImport from "components/DropToImport"; +import Fade from "components/Fade"; +import Collection from "models/Collection"; +import DocumentsStore from "stores/DocumentsStore"; +import Flex from "shared/components/Flex"; +import { type NavigationNode } from "types"; type Props = { node: NavigationNode, @@ -96,7 +96,7 @@ class DocumentLink extends React.Component { state: { title: node.title }, }} expanded={showChildren ? true : undefined} - label={node.title || 'Untitled'} + label={node.title || "Untitled"} depth={depth} exact={false} menuOpen={this.menuOpen} diff --git a/app/components/Sidebar/components/Header.js b/app/components/Sidebar/components/Header.js index 2b1ac0c4..ef95382f 100644 --- a/app/components/Sidebar/components/Header.js +++ b/app/components/Sidebar/components/Header.js @@ -1,6 +1,6 @@ // @flow -import Flex from 'shared/components/Flex'; -import styled from 'styled-components'; +import Flex from "shared/components/Flex"; +import styled from "styled-components"; const Header = styled(Flex)` font-size: 11px; diff --git a/app/components/Sidebar/components/HeaderBlock.js b/app/components/Sidebar/components/HeaderBlock.js index d7a6b61f..01f3807e 100644 --- a/app/components/Sidebar/components/HeaderBlock.js +++ b/app/components/Sidebar/components/HeaderBlock.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import styled, { withTheme } from 'styled-components'; -import { ExpandedIcon } from 'outline-icons'; -import Flex from 'shared/components/Flex'; -import TeamLogo from 'shared/components/TeamLogo'; +import * as React from "react"; +import styled, { withTheme } from "styled-components"; +import { ExpandedIcon } from "outline-icons"; +import Flex from "shared/components/Flex"; +import TeamLogo from "shared/components/TeamLogo"; type Props = { teamName: string, @@ -26,7 +26,7 @@ function HeaderBlock({ - {teamName}{' '} + {teamName}{" "} {showDisclosure && } {subheading} diff --git a/app/components/Sidebar/components/Section.js b/app/components/Sidebar/components/Section.js index df8d97ce..9ece925a 100644 --- a/app/components/Sidebar/components/Section.js +++ b/app/components/Sidebar/components/Section.js @@ -1,6 +1,6 @@ // @flow -import styled from 'styled-components'; -import Flex from 'shared/components/Flex'; +import styled from "styled-components"; +import Flex from "shared/components/Flex"; const Section = styled(Flex)` position: relative; diff --git a/app/components/Sidebar/components/SidebarLink.js b/app/components/Sidebar/components/SidebarLink.js index 80dd6ade..dd969513 100644 --- a/app/components/Sidebar/components/SidebarLink.js +++ b/app/components/Sidebar/components/SidebarLink.js @@ -1,11 +1,11 @@ // @flow -import * as React from 'react'; -import { observable, action } from 'mobx'; -import { observer } from 'mobx-react'; -import { withRouter, NavLink } from 'react-router-dom'; -import { CollapsedIcon } from 'outline-icons'; -import styled, { withTheme } from 'styled-components'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { withRouter, NavLink } from "react-router-dom"; +import { CollapsedIcon } from "outline-icons"; +import styled, { withTheme } from "styled-components"; +import Flex from "shared/components/Flex"; type Props = { to?: string | Object, @@ -82,7 +82,7 @@ class SidebarLink extends React.Component { onClick={onClick} exact={exact !== false} to={to} - as={to ? undefined : href ? 'a' : 'div'} + as={to ? undefined : href ? "a" : "div"} href={href} > {icon && {icon}} @@ -108,7 +108,7 @@ const IconWrapper = styled.span` `; const Action = styled.span` - display: ${props => (props.menuOpen ? 'inline' : 'none')}; + display: ${props => (props.menuOpen ? "inline" : "none")}; position: absolute; top: 4px; right: 4px; @@ -168,7 +168,7 @@ const Disclosure = styled(CollapsedIcon)` position: absolute; left: -24px; - ${({ expanded }) => !expanded && 'transform: rotate(-90deg);'}; + ${({ expanded }) => !expanded && "transform: rotate(-90deg);"}; `; export default withRouter(withTheme(SidebarLink)); diff --git a/app/components/Sidebar/components/Version.js b/app/components/Sidebar/components/Version.js index 140a23d1..48f41bd7 100644 --- a/app/components/Sidebar/components/Version.js +++ b/app/components/Sidebar/components/Version.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Badge from 'components/Badge'; -import SidebarLink from './SidebarLink'; -import { version } from '../../../../package.json'; +import * as React from "react"; +import styled from "styled-components"; +import Badge from "components/Badge"; +import SidebarLink from "./SidebarLink"; +import { version } from "../../../../package.json"; export default function Version() { // $FlowFixMe @@ -14,7 +14,7 @@ export default function Version() { async function loadReleases() { let out = 0; const res = await fetch( - 'https://api.github.com/repos/outline/outline/releases' + "https://api.github.com/repos/outline/outline/releases" ); const releases = await res.json(); for (const release of releases) { @@ -37,9 +37,9 @@ export default function Version() {
{releasesBehind === 0 - ? 'Up to date' + ? "Up to date" : `${releasesBehind} version${ - releasesBehind === 1 ? '' : 's' + releasesBehind === 1 ? "" : "s" } behind`} diff --git a/app/components/Sidebar/icons/Slack.js b/app/components/Sidebar/icons/Slack.js index 8c6a07f0..47f6729a 100644 --- a/app/components/Sidebar/icons/Slack.js +++ b/app/components/Sidebar/icons/Slack.js @@ -1,11 +1,11 @@ // @flow -import * as React from 'react'; +import * as React from "react"; type Props = { color?: string, }; -export default function SlackIcon({ color = '#4E5C6E' }: Props) { +export default function SlackIcon({ color = "#4E5C6E" }: Props) { return ( { if (!process.env.WEBSOCKETS_ENABLED) return; this.socket = io(window.location.origin, { - path: '/realtime', + path: "/realtime", }); this.socket.authenticated = false; @@ -54,38 +54,38 @@ class SocketProvider extends React.Component { } = this.props; if (!auth.token) return; - this.socket.on('connect', () => { + this.socket.on("connect", () => { // immediately send current users token to the websocket backend where it // is verified, if all goes well an 'authenticated' message will be // received in response - this.socket.emit('authentication', { + this.socket.emit("authentication", { token: auth.token, }); }); - this.socket.on('disconnect', () => { + this.socket.on("disconnect", () => { // when the socket is disconnected we need to clear all presence state as // it's no longer reliable. presence.clear(); }); - this.socket.on('authenticated', () => { + this.socket.on("authenticated", () => { this.socket.authenticated = true; }); - this.socket.on('unauthorized', err => { + this.socket.on("unauthorized", err => { this.socket.authenticated = false; ui.showToast(err.message); throw err; }); - this.socket.on('entities', async event => { + this.socket.on("entities", async event => { if (event.documentIds) { for (const documentDescriptor of event.documentIds) { const documentId = documentDescriptor.id; let document = documents.get(documentId) || {}; - if (event.event === 'documents.delete') { + if (event.event === "documents.delete") { const document = documents.get(documentId); if (document) { document.deletedAt = documentDescriptor.updatedAt; @@ -136,7 +136,7 @@ class SocketProvider extends React.Component { const collectionId = collectionDescriptor.id; const collection = collections.get(collectionId) || {}; - if (event.event === 'collections.delete') { + if (event.event === "collections.delete") { documents.removeCollectionDocuments(collectionId); continue; } @@ -184,17 +184,17 @@ class SocketProvider extends React.Component { } }); - this.socket.on('documents.star', event => { + this.socket.on("documents.star", event => { documents.starredIds.set(event.documentId, true); }); - this.socket.on('documents.unstar', event => { + this.socket.on("documents.unstar", event => { documents.starredIds.set(event.documentId, false); }); // received when a user is given access to a collection // if the user is us then we go ahead and load the collection from API. - this.socket.on('collections.add_user', event => { + this.socket.on("collections.add_user", event => { if (auth.user && event.userId === auth.user.id) { collections.fetch(event.collectionId, { force: true }); } @@ -208,7 +208,7 @@ class SocketProvider extends React.Component { // received when a user is removed from having access to a collection // to keep state in sync we must update our UI if the user is us, // or otherwise just remove any membership state we have for that user. - this.socket.on('collections.remove_user', event => { + this.socket.on("collections.remove_user", event => { if (auth.user && event.userId === auth.user.id) { collections.remove(event.collectionId); memberships.removeCollectionMemberships(event.collectionId); @@ -220,32 +220,32 @@ class SocketProvider extends React.Component { // received a message from the API server that we should request // to join a specific room. Forward that to the ws server. - this.socket.on('join', event => { - this.socket.emit('join', event); + this.socket.on("join", event => { + this.socket.emit("join", event); }); // received a message from the API server that we should request // to leave a specific room. Forward that to the ws server. - this.socket.on('leave', event => { - this.socket.emit('leave', event); + this.socket.on("leave", event => { + this.socket.emit("leave", event); }); // received whenever we join a document room, the payload includes // userIds that are present/viewing and those that are editing. - this.socket.on('document.presence', event => { + this.socket.on("document.presence", event => { presence.init(event.documentId, event.userIds, event.editingIds); }); // received whenever a new user joins a document room, aka they // navigate to / start viewing a document - this.socket.on('user.join', event => { + this.socket.on("user.join", event => { presence.touch(event.documentId, event.userId, event.isEditing); views.touch(event.documentId, event.userId); }); // received whenever a new user leaves a document room, aka they // navigate away / stop viewing a document - this.socket.on('user.leave', event => { + this.socket.on("user.leave", event => { presence.leave(event.documentId, event.userId); views.touch(event.documentId, event.userId); }); @@ -253,7 +253,7 @@ class SocketProvider extends React.Component { // received when another client in a document room wants to change // or update it's presence. Currently the only property is whether // the client is in editing state or not. - this.socket.on('user.presence', event => { + this.socket.on("user.presence", event => { presence.touch(event.documentId, event.userId, event.isEditing); }); } @@ -275,13 +275,13 @@ class SocketProvider extends React.Component { } export default inject( - 'auth', - 'ui', - 'documents', - 'collections', - 'groups', - 'memberships', - 'presence', - 'policies', - 'views' + "auth", + "ui", + "documents", + "collections", + "groups", + "memberships", + "presence", + "policies", + "views" )(SocketProvider); diff --git a/app/components/Subheading.js b/app/components/Subheading.js index e1f8240b..a4cf41fa 100644 --- a/app/components/Subheading.js +++ b/app/components/Subheading.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; +import * as React from "react"; +import styled from "styled-components"; type Props = { children: React.Node, @@ -13,7 +13,7 @@ const H3 = styled.h3` line-height: 1; `; -const Underline = styled('span')` +const Underline = styled("span")` position: relative; top: 1px; diff --git a/app/components/Switch.js b/app/components/Switch.js index 3992ee91..43a6cf42 100644 --- a/app/components/Switch.js +++ b/app/components/Switch.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { LabelText } from 'components/Input'; +import * as React from "react"; +import styled from "styled-components"; +import { LabelText } from "components/Input"; type Props = { width?: number, @@ -57,7 +57,7 @@ const Slider = styled.span` &:before { position: absolute; - content: ''; + content: ""; height: ${props => props.height - 8}px; width: ${props => props.height - 8}px; left: 4px; diff --git a/app/components/Tab.js b/app/components/Tab.js index 48b1ac8e..a4eea9cb 100644 --- a/app/components/Tab.js +++ b/app/components/Tab.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import styled, { withTheme } from 'styled-components'; -import { NavLink } from 'react-router-dom'; -import { lighten } from 'polished'; +import * as React from "react"; +import styled, { withTheme } from "styled-components"; +import { NavLink } from "react-router-dom"; +import { lighten } from "polished"; type Props = { theme: Object, @@ -35,7 +35,7 @@ const StyledNavLink = styled(NavLink)` function Tab(props: Props) { const activeStyle = { - paddingBottom: '5px', + paddingBottom: "5px", borderBottom: `3px solid ${props.theme.textSecondary}`, color: props.theme.textSecondary, }; diff --git a/app/components/Tabs.js b/app/components/Tabs.js index a92da1dd..e61c3fed 100644 --- a/app/components/Tabs.js +++ b/app/components/Tabs.js @@ -1,5 +1,5 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const Tabs = styled.nav` border-bottom: 1px solid ${props => props.theme.divider}; diff --git a/app/components/Theme.js b/app/components/Theme.js index bcf9aefe..9ed2c1b1 100644 --- a/app/components/Theme.js +++ b/app/components/Theme.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import { inject, observer } from 'mobx-react'; -import { ThemeProvider } from 'styled-components'; -import { dark, light } from 'shared/styles/theme'; -import GlobalStyles from 'shared/styles/globals'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import { inject, observer } from "mobx-react"; +import { ThemeProvider } from "styled-components"; +import { dark, light } from "shared/styles/theme"; +import GlobalStyles from "shared/styles/globals"; +import UiStore from "stores/UiStore"; type Props = { ui: UiStore, @@ -13,7 +13,7 @@ type Props = { function Theme({ children, ui }: Props) { return ( - + {children} @@ -22,4 +22,4 @@ function Theme({ children, ui }: Props) { ); } -export default inject('ui')(observer(Theme)); +export default inject("ui")(observer(Theme)); diff --git a/app/components/Toasts/Toasts.js b/app/components/Toasts/Toasts.js index 65c18559..ffa60e42 100644 --- a/app/components/Toasts/Toasts.js +++ b/app/components/Toasts/Toasts.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import styled from 'styled-components'; -import Toast from './components/Toast'; -import UiStore from '../../stores/UiStore'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import styled from "styled-components"; +import Toast from "./components/Toast"; +import UiStore from "../../stores/UiStore"; type Props = { ui: UiStore, @@ -37,4 +37,4 @@ const List = styled.ol` z-index: 1000; `; -export default inject('ui')(Toasts); +export default inject("ui")(Toasts); diff --git a/app/components/Toasts/components/Toast.js b/app/components/Toasts/components/Toast.js index 4be464b3..d65d27d9 100644 --- a/app/components/Toasts/components/Toast.js +++ b/app/components/Toasts/components/Toast.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { darken } from 'polished'; -import { fadeAndScaleIn } from 'shared/styles/animations'; -import type { Toast as TToast } from '../../../types'; +import * as React from "react"; +import styled from "styled-components"; +import { darken } from "polished"; +import { fadeAndScaleIn } from "shared/styles/animations"; +import type { Toast as TToast } from "../../../types"; type Props = { onRequestClose: () => void, @@ -33,7 +33,7 @@ class Toast extends React.Component { const { toast, onRequestClose } = this.props; const { action } = toast; const message = - typeof toast.message === 'string' + typeof toast.message === "string" ? toast.message : toast.message.toString(); @@ -41,11 +41,11 @@ class Toast extends React.Component {
  • {message} {action && ( - + {action.text} )} diff --git a/app/components/Toasts/index.js b/app/components/Toasts/index.js index f0b2732f..13373bf8 100644 --- a/app/components/Toasts/index.js +++ b/app/components/Toasts/index.js @@ -1,3 +1,3 @@ // @flow -import Toasts from './Toasts'; +import Toasts from "./Toasts"; export default Toasts; diff --git a/app/components/Tooltip.js b/app/components/Tooltip.js index ddc1b56c..dbafffa8 100644 --- a/app/components/Tooltip.js +++ b/app/components/Tooltip.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Tippy from '@tippy.js/react'; +import * as React from "react"; +import styled from "styled-components"; +import Tippy from "@tippy.js/react"; type Props = { tooltip: React.Node, shortcut?: React.Node, - placement?: 'top' | 'bottom' | 'left' | 'right', + placement?: "top" | "bottom" | "left" | "right", children: React.Node, delay?: number, className?: string, @@ -47,7 +47,7 @@ const Shortcut = styled.kbd` display: inline-block; padding: 2px 4px; - font: 10px 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, + font: 10px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 10px; color: ${props => props.theme.tooltipBackground}; diff --git a/app/components/VisuallyHidden.js b/app/components/VisuallyHidden.js index 554349c3..8b0256ae 100644 --- a/app/components/VisuallyHidden.js +++ b/app/components/VisuallyHidden.js @@ -1,7 +1,7 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; -const VisuallyHidden = styled('span')` +const VisuallyHidden = styled("span")` position: absolute !important; height: 1px; width: 1px; diff --git a/app/embeds/Abstract.js b/app/embeds/Abstract.js index 504c06c5..f655c070 100644 --- a/app/embeds/Abstract.js +++ b/app/embeds/Abstract.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; type Props = {| attrs: {| @@ -11,8 +11,8 @@ type Props = {| export default class Abstract extends React.Component { static ENABLED = [ - new RegExp('https?://share.(?:go)?abstract.com/(.*)$'), - new RegExp('https?://app.(?:go)?abstract.com/(?:share|embed)/(.*)$'), + new RegExp("https?://share.(?:go)?abstract.com/(.*)$"), + new RegExp("https?://app.(?:go)?abstract.com/(?:share|embed)/(.*)$"), ]; render() { diff --git a/app/embeds/Abstract.test.js b/app/embeds/Abstract.test.js index b07ad277..aaf58371 100644 --- a/app/embeds/Abstract.test.js +++ b/app/embeds/Abstract.test.js @@ -1,58 +1,58 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Abstract from './Abstract'; +import Abstract from "./Abstract"; -describe('Abstract', () => { +describe("Abstract", () => { const match = Abstract.ENABLED[0]; const match2 = Abstract.ENABLED[1]; - test('to be enabled on share subdomain link', () => { + test("to be enabled on share subdomain link", () => { expect( - 'https://share.goabstract.com/aaec8bba-f473-4f64-96e7-bff41c70ff8a'.match( + "https://share.goabstract.com/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match( match ) ).toBeTruthy(); expect( - 'https://share.abstract.com/aaec8bba-f473-4f64-96e7-bff41c70ff8a'.match( + "https://share.abstract.com/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match( match ) ).toBeTruthy(); }); - test('to be enabled on share link', () => { + test("to be enabled on share link", () => { expect( - 'https://app.goabstract.com/share/aaec8bba-f473-4f64-96e7-bff41c70ff8a'.match( + "https://app.goabstract.com/share/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match( match2 ) ).toBeTruthy(); expect( - 'https://app.abstract.com/share/aaec8bba-f473-4f64-96e7-bff41c70ff8a'.match( + "https://app.abstract.com/share/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match( match2 ) ).toBeTruthy(); }); - test('to be enabled on embed link', () => { + test("to be enabled on embed link", () => { expect( - 'https://app.goabstract.com/embed/aaec8bba-f473-4f64-96e7-bff41c70ff8a'.match( + "https://app.goabstract.com/embed/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match( match2 ) ).toBeTruthy(); expect( - 'https://app.abstract.com/embed/aaec8bba-f473-4f64-96e7-bff41c70ff8a'.match( + "https://app.abstract.com/embed/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match( match2 ) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://abstract.com'.match(match)).toBe(null); - expect('https://goabstract.com'.match(match)).toBe(null); - expect('https://app.goabstract.com'.match(match)).toBe(null); - expect('https://abstract.com/features'.match(match)).toBe(null); - expect('https://app.abstract.com/home'.match(match)).toBe(null); - expect('https://abstract.com/pricing'.match(match)).toBe(null); - expect('https://goabstract.com/pricing'.match(match)).toBe(null); - expect('https://www.goabstract.com/pricing'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://abstract.com".match(match)).toBe(null); + expect("https://goabstract.com".match(match)).toBe(null); + expect("https://app.goabstract.com".match(match)).toBe(null); + expect("https://abstract.com/features".match(match)).toBe(null); + expect("https://app.abstract.com/home".match(match)).toBe(null); + expect("https://abstract.com/pricing".match(match)).toBe(null); + expect("https://goabstract.com/pricing".match(match)).toBe(null); + expect("https://www.goabstract.com/pricing".match(match)).toBe(null); }); }); diff --git a/app/embeds/Airtable.js b/app/embeds/Airtable.js index 8ed59f80..e7775522 100644 --- a/app/embeds/Airtable.js +++ b/app/embeds/Airtable.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; -const URL_REGEX = new RegExp('https://airtable.com/(?:embed/)?(shr.*)$'); +const URL_REGEX = new RegExp("https://airtable.com/(?:embed/)?(shr.*)$"); type Props = {| attrs: {| diff --git a/app/embeds/Airtable.test.js b/app/embeds/Airtable.test.js index 6ce7ae58..1401c1f5 100644 --- a/app/embeds/Airtable.test.js +++ b/app/embeds/Airtable.test.js @@ -1,21 +1,21 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Airtable from './Airtable'; +import Airtable from "./Airtable"; -describe('Airtable', () => { +describe("Airtable", () => { const match = Airtable.ENABLED[0]; - test('to be enabled on share link', () => { - expect('https://airtable.com/shrEoQs3erLnppMie'.match(match)).toBeTruthy(); + test("to be enabled on share link", () => { + expect("https://airtable.com/shrEoQs3erLnppMie".match(match)).toBeTruthy(); }); - test('to be enabled on embed link', () => { + test("to be enabled on embed link", () => { expect( - 'https://airtable.com/embed/shrEoQs3erLnppMie'.match(match) + "https://airtable.com/embed/shrEoQs3erLnppMie".match(match) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://airtable.com'.match(match)).toBe(null); - expect('https://airtable.com/features'.match(match)).toBe(null); - expect('https://airtable.com/pricing'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://airtable.com".match(match)).toBe(null); + expect("https://airtable.com/features".match(match)).toBe(null); + expect("https://airtable.com/pricing".match(match)).toBe(null); }); }); diff --git a/app/embeds/Codepen.js b/app/embeds/Codepen.js index 57a6ea15..e809cc67 100644 --- a/app/embeds/Codepen.js +++ b/app/embeds/Codepen.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; -const URL_REGEX = new RegExp('^https://codepen.io/(.*?)/(pen|embed)/(.*)$'); +const URL_REGEX = new RegExp("^https://codepen.io/(.*?)/(pen|embed)/(.*)$"); type Props = {| attrs: {| @@ -15,7 +15,7 @@ export default class Codepen extends React.Component { static ENABLED = [URL_REGEX]; render() { - const normalizedUrl = this.props.attrs.href.replace(/\/pen\//, '/embed/'); + const normalizedUrl = this.props.attrs.href.replace(/\/pen\//, "/embed/"); return ; } diff --git a/app/embeds/Codepen.test.js b/app/embeds/Codepen.test.js index e21b037e..fbb14991 100644 --- a/app/embeds/Codepen.test.js +++ b/app/embeds/Codepen.test.js @@ -1,22 +1,22 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Codepen from './Codepen'; +import Codepen from "./Codepen"; -describe('Codepen', () => { +describe("Codepen", () => { const match = Codepen.ENABLED[0]; - test('to be enabled on pen link', () => { + test("to be enabled on pen link", () => { expect( - 'https://codepen.io/chriscoyier/pen/gfdDu'.match(match) + "https://codepen.io/chriscoyier/pen/gfdDu".match(match) ).toBeTruthy(); }); - test('to be enabled on embed link', () => { + test("to be enabled on embed link", () => { expect( - 'https://codepen.io/chriscoyier/embed/gfdDu'.match(match) + "https://codepen.io/chriscoyier/embed/gfdDu".match(match) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://codepen.io'.match(match)).toBe(null); - expect('https://codepen.io/chriscoyier'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://codepen.io".match(match)).toBe(null); + expect("https://codepen.io/chriscoyier".match(match)).toBe(null); }); }); diff --git a/app/embeds/Figma.js b/app/embeds/Figma.js index dc207e19..e8491cc1 100644 --- a/app/embeds/Figma.js +++ b/app/embeds/Figma.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = new RegExp( - 'https://([w.-]+.)?figma.com/(file|proto)/([0-9a-zA-Z]{22,128})(?:/.*)?$' + "https://([w.-]+.)?figma.com/(file|proto)/([0-9a-zA-Z]{22,128})(?:/.*)?$" ); type Props = {| diff --git a/app/embeds/Figma.test.js b/app/embeds/Figma.test.js index af721b58..988da5f9 100644 --- a/app/embeds/Figma.test.js +++ b/app/embeds/Figma.test.js @@ -1,22 +1,22 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Figma from './Figma'; +import Figma from "./Figma"; -describe('Figma', () => { +describe("Figma", () => { const match = Figma.ENABLED[0]; - test('to be enabled on file link', () => { + test("to be enabled on file link", () => { expect( - 'https://www.figma.com/file/LKQ4FJ4bTnCSjedbRpk931'.match(match) + "https://www.figma.com/file/LKQ4FJ4bTnCSjedbRpk931".match(match) ).toBeTruthy(); }); - test('to be enabled on prototype link', () => { + test("to be enabled on prototype link", () => { expect( - 'https://www.figma.com/proto/LKQ4FJ4bTnCSjedbRpk931'.match(match) + "https://www.figma.com/proto/LKQ4FJ4bTnCSjedbRpk931".match(match) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://www.figma.com'.match(match)).toBe(null); - expect('https://www.figma.com/features'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://www.figma.com".match(match)).toBe(null); + expect("https://www.figma.com/features".match(match)).toBe(null); }); }); diff --git a/app/embeds/Framer.js b/app/embeds/Framer.js index 6bfee717..0b58f7c4 100644 --- a/app/embeds/Framer.js +++ b/app/embeds/Framer.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; -const URL_REGEX = new RegExp('^https://framer.cloud/(.*)$'); +const URL_REGEX = new RegExp("^https://framer.cloud/(.*)$"); type Props = {| attrs: {| diff --git a/app/embeds/Framer.test.js b/app/embeds/Framer.test.js index 49ac832b..22c96e99 100644 --- a/app/embeds/Framer.test.js +++ b/app/embeds/Framer.test.js @@ -1,13 +1,13 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Framer from './Framer'; +import Framer from "./Framer"; -describe('Framer', () => { +describe("Framer", () => { const match = Framer.ENABLED[0]; - test('to be enabled on share link', () => { - expect('https://framer.cloud/PVwJO'.match(match)).toBeTruthy(); + test("to be enabled on share link", () => { + expect("https://framer.cloud/PVwJO".match(match)).toBeTruthy(); }); - test('to not be enabled on root', () => { - expect('https://framer.cloud'.match(match)).toBe(null); + test("to not be enabled on root", () => { + expect("https://framer.cloud".match(match)).toBe(null); }); }); diff --git a/app/embeds/Gist.js b/app/embeds/Gist.js index 23abdd14..e093d088 100644 --- a/app/embeds/Gist.js +++ b/app/embeds/Gist.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; +import * as React from "react"; const URL_REGEX = new RegExp( - '^https://gist.github.com/([a-zd](?:[a-zd]|-(?=[a-zd])){0,38})/(.*)$' + "^https://gist.github.com/([a-zd](?:[a-zd]|-(?=[a-zd])){0,38})/(.*)$" ); type Props = {| @@ -23,7 +23,7 @@ class Gist extends React.Component { get id() { const gistUrl = new URL(this.props.attrs.href); - return gistUrl.pathname.split('/')[2]; + return gistUrl.pathname.split("/")[2]; } updateIframeContent() { @@ -33,9 +33,9 @@ class Gist extends React.Component { // We need to add some temporary content to the iframe for the document // to be available, otherwise it's undefined on first load - const temp = document.getElementById('gist'); + const temp = document.getElementById("gist"); if (temp) { - temp.innerHTML = ''; + temp.innerHTML = ""; temp.appendChild(iframe); } @@ -52,7 +52,7 @@ class Gist extends React.Component { gistLink }">`; const styles = - ''; + ""; const iframeHtml = `${ styles }${gistScript}`; diff --git a/app/embeds/Gist.test.js b/app/embeds/Gist.test.js index 7de1e7b0..da0669a0 100644 --- a/app/embeds/Gist.test.js +++ b/app/embeds/Gist.test.js @@ -1,17 +1,17 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Gist from './Gist'; +import Gist from "./Gist"; -describe('Gist', () => { +describe("Gist", () => { const match = Gist.ENABLED[0]; - test('to be enabled on gist link', () => { + test("to be enabled on gist link", () => { expect( - 'https://gist.github.com/wmertens/0b4fd66ca7055fd290ecc4b9d95271a9'.match( + "https://gist.github.com/wmertens/0b4fd66ca7055fd290ecc4b9d95271a9".match( match ) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://gist.github.com/tommoor'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://gist.github.com/tommoor".match(match)).toBe(null); }); }); diff --git a/app/embeds/GoogleDocs.js b/app/embeds/GoogleDocs.js index ccf0d9bf..0cbfaa1e 100644 --- a/app/embeds/GoogleDocs.js +++ b/app/embeds/GoogleDocs.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = new RegExp( - '^https?://docs.google.com/document/d/(.*)/pub(.*)$' + "^https?://docs.google.com/document/d/(.*)/pub(.*)$" ); type Props = {| diff --git a/app/embeds/GoogleDocs.test.js b/app/embeds/GoogleDocs.test.js index f7e97e98..426407ee 100644 --- a/app/embeds/GoogleDocs.test.js +++ b/app/embeds/GoogleDocs.test.js @@ -1,29 +1,29 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import GoogleDocs from './GoogleDocs'; +import GoogleDocs from "./GoogleDocs"; -describe('GoogleDocs', () => { +describe("GoogleDocs", () => { const match = GoogleDocs.ENABLED[0]; - test('to be enabled on share link', () => { + test("to be enabled on share link", () => { expect( - 'https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pubhtml'.match( + "https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pubhtml".match( match ) ).toBeTruthy(); expect( - 'https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub'.match( + "https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub".match( match ) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { + test("to not be enabled elsewhere", () => { expect( - 'https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit'.match( + "https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match( match ) ).toBe(null); - expect('https://docs.google.com/document'.match(match)).toBe(null); - expect('https://docs.google.com'.match(match)).toBe(null); - expect('https://www.google.com'.match(match)).toBe(null); + expect("https://docs.google.com/document".match(match)).toBe(null); + expect("https://docs.google.com".match(match)).toBe(null); + expect("https://www.google.com".match(match)).toBe(null); }); }); diff --git a/app/embeds/GoogleSheets.js b/app/embeds/GoogleSheets.js index 48a848d4..78cb749c 100644 --- a/app/embeds/GoogleSheets.js +++ b/app/embeds/GoogleSheets.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = new RegExp( - '^https?://docs.google.com/spreadsheets/d/(.*)/pub(.*)$' + "^https?://docs.google.com/spreadsheets/d/(.*)/pub(.*)$" ); type Props = {| diff --git a/app/embeds/GoogleSheets.test.js b/app/embeds/GoogleSheets.test.js index 6afdf725..81a45255 100644 --- a/app/embeds/GoogleSheets.test.js +++ b/app/embeds/GoogleSheets.test.js @@ -1,24 +1,24 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import GoogleSheets from './GoogleSheets'; +import GoogleSheets from "./GoogleSheets"; -describe('GoogleSheets', () => { +describe("GoogleSheets", () => { const match = GoogleSheets.ENABLED[0]; - test('to be enabled on share link', () => { + test("to be enabled on share link", () => { expect( - 'https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub'.match( + "https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub".match( match ) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { + test("to not be enabled elsewhere", () => { expect( - 'https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit'.match( + "https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match( match ) ).toBe(null); - expect('https://docs.google.com/spreadsheets'.match(match)).toBe(null); - expect('https://docs.google.com'.match(match)).toBe(null); - expect('https://www.google.com'.match(match)).toBe(null); + expect("https://docs.google.com/spreadsheets".match(match)).toBe(null); + expect("https://docs.google.com".match(match)).toBe(null); + expect("https://www.google.com".match(match)).toBe(null); }); }); diff --git a/app/embeds/GoogleSlides.js b/app/embeds/GoogleSlides.js index ee0dd919..f735d049 100644 --- a/app/embeds/GoogleSlides.js +++ b/app/embeds/GoogleSlides.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = new RegExp( - '^https?://docs.google.com/presentation/d/(.*)/pub(.*)$' + "^https?://docs.google.com/presentation/d/(.*)/pub(.*)$" ); type Props = {| @@ -19,7 +19,7 @@ export default class GoogleSlides extends React.Component { render() { return ( diff --git a/app/embeds/GoogleSlides.test.js b/app/embeds/GoogleSlides.test.js index 38de1637..fb0daed4 100644 --- a/app/embeds/GoogleSlides.test.js +++ b/app/embeds/GoogleSlides.test.js @@ -1,29 +1,29 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import GoogleSlides from './GoogleSlides'; +import GoogleSlides from "./GoogleSlides"; -describe('GoogleSlides', () => { +describe("GoogleSlides", () => { const match = GoogleSlides.ENABLED[0]; - test('to be enabled on share link', () => { + test("to be enabled on share link", () => { expect( - 'https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub?start=false&loop=false&delayms=3000'.match( + "https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub?start=false&loop=false&delayms=3000".match( match ) ).toBeTruthy(); expect( - 'https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub'.match( + "https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub".match( match ) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { + test("to not be enabled elsewhere", () => { expect( - 'https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit'.match( + "https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match( match ) ).toBe(null); - expect('https://docs.google.com/presentation'.match(match)).toBe(null); - expect('https://docs.google.com'.match(match)).toBe(null); - expect('https://www.google.com'.match(match)).toBe(null); + expect("https://docs.google.com/presentation".match(match)).toBe(null); + expect("https://docs.google.com".match(match)).toBe(null); + expect("https://www.google.com".match(match)).toBe(null); }); }); diff --git a/app/embeds/InVision.js b/app/embeds/InVision.js index e72092cd..80ea9d18 100644 --- a/app/embeds/InVision.js +++ b/app/embeds/InVision.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import ImageZoom from 'react-medium-image-zoom'; -import Frame from './components/Frame'; +import * as React from "react"; +import ImageZoom from "react-medium-image-zoom"; +import Frame from "./components/Frame"; const IFRAME_REGEX = new RegExp( - '^https://(invis.io/.*)|(projects.invisionapp.com/share/.*)$' + "^https://(invis.io/.*)|(projects.invisionapp.com/share/.*)$" ); const IMAGE_REGEX = new RegExp( - '^https://(opal.invisionapp.com/static-signed/live-embed/.*)$' + "^https://(opal.invisionapp.com/static-signed/live-embed/.*)$" ); type Props = {| @@ -27,10 +27,10 @@ export default class InVision extends React.Component { { +describe("InVision", () => { const match = InVision.ENABLED[0]; - test('to be enabled on shortlink', () => { - expect('https://invis.io/69PG07QYQTE'.match(match)).toBeTruthy(); + test("to be enabled on shortlink", () => { + expect("https://invis.io/69PG07QYQTE".match(match)).toBeTruthy(); }); - test('to be enabled on share', () => { + test("to be enabled on share", () => { expect( - 'https://projects.invisionapp.com/share/69PG07QYQTE'.match(match) + "https://projects.invisionapp.com/share/69PG07QYQTE".match(match) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://invis.io'.match(match)).toBe(null); - expect('https://invisionapp.com'.match(match)).toBe(null); - expect('https://projects.invisionapp.com'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://invis.io".match(match)).toBe(null); + expect("https://invisionapp.com".match(match)).toBe(null); + expect("https://projects.invisionapp.com".match(match)).toBe(null); }); }); diff --git a/app/embeds/Loom.js b/app/embeds/Loom.js index e4681258..29eb5f66 100644 --- a/app/embeds/Loom.js +++ b/app/embeds/Loom.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = /^https:\/\/(www\.)?(use)?loom.com\/(embed|share)\/(.*)$/; @@ -15,7 +15,7 @@ export default class Loom extends React.Component { static ENABLED = [URL_REGEX]; render() { - const normalizedUrl = this.props.attrs.href.replace('share', 'embed'); + const normalizedUrl = this.props.attrs.href.replace("share", "embed"); return ; } diff --git a/app/embeds/Loom.test.js b/app/embeds/Loom.test.js index 6acf6c43..69f6b7a0 100644 --- a/app/embeds/Loom.test.js +++ b/app/embeds/Loom.test.js @@ -1,32 +1,32 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Loom from './Loom'; +import Loom from "./Loom"; -describe('Loom', () => { +describe("Loom", () => { const match = Loom.ENABLED[0]; - test('to be enabled on share link', () => { + test("to be enabled on share link", () => { expect( - 'https://www.loom.com/share/55327cbb265743f39c2c442c029277e0'.match(match) + "https://www.loom.com/share/55327cbb265743f39c2c442c029277e0".match(match) ).toBeTruthy(); expect( - 'https://www.useloom.com/share/55327cbb265743f39c2c442c029277e0'.match( + "https://www.useloom.com/share/55327cbb265743f39c2c442c029277e0".match( match ) ).toBeTruthy(); }); - test('to be enabled on embed link', () => { + test("to be enabled on embed link", () => { expect( - 'https://www.loom.com/embed/55327cbb265743f39c2c442c029277e0'.match(match) + "https://www.loom.com/embed/55327cbb265743f39c2c442c029277e0".match(match) ).toBeTruthy(); expect( - 'https://www.useloom.com/embed/55327cbb265743f39c2c442c029277e0'.match( + "https://www.useloom.com/embed/55327cbb265743f39c2c442c029277e0".match( match ) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://www.useloom.com'.match(match)).toBe(null); - expect('https://www.useloom.com/features'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://www.useloom.com".match(match)).toBe(null); + expect("https://www.useloom.com/features".match(match)).toBe(null); }); }); diff --git a/app/embeds/Lucidchart.js b/app/embeds/Lucidchart.js index e145eafb..acb40037 100644 --- a/app/embeds/Lucidchart.js +++ b/app/embeds/Lucidchart.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = /^https?:\/\/(www\.)?lucidchart.com\/documents\/(embeddedchart|view)\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(?:\/.*)?$/; diff --git a/app/embeds/Lucidchart.test.js b/app/embeds/Lucidchart.test.js index 014c835a..d4922d11 100644 --- a/app/embeds/Lucidchart.test.js +++ b/app/embeds/Lucidchart.test.js @@ -1,28 +1,28 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Lucidchart from './Lucidchart'; +import Lucidchart from "./Lucidchart"; -describe('Lucidchart', () => { +describe("Lucidchart", () => { const match = Lucidchart.ENABLED[0]; - test('to be enabled on view link', () => { + test("to be enabled on view link", () => { expect( - 'https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7'.match( + "https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7".match( match ) ).toBeTruthy(); }); - test('to be enabled on visited link', () => { + test("to be enabled on visited link", () => { expect( - 'https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7/0'.match( + "https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7/0".match( match ) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://lucidchart.com'.match(match)).toBe(null); - expect('https://www.lucidchart.com'.match(match)).toBe(null); - expect('https://www.lucidchart.com/features'.match(match)).toBe(null); - expect('https://www.lucidchart.com/documents/view'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://lucidchart.com".match(match)).toBe(null); + expect("https://www.lucidchart.com".match(match)).toBe(null); + expect("https://www.lucidchart.com/features".match(match)).toBe(null); + expect("https://www.lucidchart.com/documents/view".match(match)).toBe(null); }); }); diff --git a/app/embeds/Marvel.js b/app/embeds/Marvel.js index b99178f6..ee60cfd0 100644 --- a/app/embeds/Marvel.js +++ b/app/embeds/Marvel.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; -const URL_REGEX = new RegExp('^https://marvelapp.com/([A-Za-z0-9-]{6})/?$'); +const URL_REGEX = new RegExp("^https://marvelapp.com/([A-Za-z0-9-]{6})/?$"); type Props = {| attrs: {| diff --git a/app/embeds/Marvel.test.js b/app/embeds/Marvel.test.js index 4c4fa18e..1914f71b 100644 --- a/app/embeds/Marvel.test.js +++ b/app/embeds/Marvel.test.js @@ -1,14 +1,14 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Marvel from './Marvel'; +import Marvel from "./Marvel"; -describe('Marvel', () => { +describe("Marvel", () => { const match = Marvel.ENABLED[0]; - test('to be enabled on share link', () => { - expect('https://marvelapp.com/75hj91'.match(match)).toBeTruthy(); + test("to be enabled on share link", () => { + expect("https://marvelapp.com/75hj91".match(match)).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://marvelapp.com'.match(match)).toBe(null); - expect('https://marvelapp.com/features'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://marvelapp.com".match(match)).toBe(null); + expect("https://marvelapp.com/features".match(match)).toBe(null); }); }); diff --git a/app/embeds/Mindmeister.js b/app/embeds/Mindmeister.js index 006ffefa..f7172733 100644 --- a/app/embeds/Mindmeister.js +++ b/app/embeds/Mindmeister.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = new RegExp( - '^https://([w.-]+.)?(mindmeister.com|mm.tt)(/maps/public_map_shell)?/(\\d+)(\\?t=.*)?(/.*)?$' + "^https://([w.-]+.)?(mindmeister.com|mm.tt)(/maps/public_map_shell)?/(\\d+)(\\?t=.*)?(/.*)?$" ); type Props = {| diff --git a/app/embeds/Mindmeister.test.js b/app/embeds/Mindmeister.test.js index 9f68c397..427befa9 100644 --- a/app/embeds/Mindmeister.test.js +++ b/app/embeds/Mindmeister.test.js @@ -1,47 +1,47 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Mindmeister from './Mindmeister'; +import Mindmeister from "./Mindmeister"; -describe('Mindmeister', () => { +describe("Mindmeister", () => { const match = Mindmeister.ENABLED[0]; - test('to be enabled on mm.tt link', () => { - expect('https://mm.tt/326377934'.match(match)).toBeTruthy(); + test("to be enabled on mm.tt link", () => { + expect("https://mm.tt/326377934".match(match)).toBeTruthy(); }); - test('to be enabled on mm.tt link with token parameter', () => { - expect('https://mm.tt/326377934?t=r9NcnTRr18'.match(match)).toBeTruthy(); + test("to be enabled on mm.tt link with token parameter", () => { + expect("https://mm.tt/326377934?t=r9NcnTRr18".match(match)).toBeTruthy(); }); - test('to be enabled on embed link', () => { + test("to be enabled on embed link", () => { expect( - 'https://www.mindmeister.com/maps/public_map_shell/326377934/paper-digital-or-online-mind-mapping'.match( + "https://www.mindmeister.com/maps/public_map_shell/326377934/paper-digital-or-online-mind-mapping".match( match ) ).toBeTruthy(); }); - test('to be enabled on public link', () => { + test("to be enabled on public link", () => { expect( - 'https://www.mindmeister.com/326377934/paper-digital-or-online-mind-mapping'.match( + "https://www.mindmeister.com/326377934/paper-digital-or-online-mind-mapping".match( match ) ).toBeTruthy(); }); - test('to be enabled without www', () => { + test("to be enabled without www", () => { expect( - 'https://mindmeister.com/326377934/paper-digital-or-online-mind-mapping'.match( + "https://mindmeister.com/326377934/paper-digital-or-online-mind-mapping".match( match ) ).toBeTruthy(); }); - test('to be enabled without slug', () => { - expect('https://mindmeister.com/326377934'.match(match)).toBeTruthy(); + test("to be enabled without slug", () => { + expect("https://mindmeister.com/326377934".match(match)).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://mindmeister.com'.match(match)).toBe(null); - expect('https://www.mindmeister.com/pricing'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://mindmeister.com".match(match)).toBe(null); + expect("https://www.mindmeister.com/pricing".match(match)).toBe(null); }); }); diff --git a/app/embeds/Miro.js b/app/embeds/Miro.js index 4450366c..0c994355 100644 --- a/app/embeds/Miro.js +++ b/app/embeds/Miro.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = /^https:\/\/(?:realtimeboard|miro).com\/app\/board\/(.*)$/; diff --git a/app/embeds/Miro.test.js b/app/embeds/Miro.test.js index 090393b5..f87cb6c1 100644 --- a/app/embeds/Miro.test.js +++ b/app/embeds/Miro.test.js @@ -1,21 +1,21 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Miro from './Miro'; +import Miro from "./Miro"; -describe('Miro', () => { +describe("Miro", () => { const match = Miro.ENABLED[0]; - test('to be enabled on old domain share link', () => { + test("to be enabled on old domain share link", () => { expect( - 'https://realtimeboard.com/app/board/o9J_k0fwiss='.match(match) + "https://realtimeboard.com/app/board/o9J_k0fwiss=".match(match) ).toBeTruthy(); }); - test('to be enabled on share link', () => { - expect('https://miro.com/app/board/o9J_k0fwiss='.match(match)).toBeTruthy(); + test("to be enabled on share link", () => { + expect("https://miro.com/app/board/o9J_k0fwiss=".match(match)).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://miro.com'.match(match)).toBe(null); - expect('https://realtimeboard.com'.match(match)).toBe(null); - expect('https://realtimeboard.com/features'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://miro.com".match(match)).toBe(null); + expect("https://realtimeboard.com".match(match)).toBe(null); + expect("https://realtimeboard.com/features".match(match)).toBe(null); }); }); diff --git a/app/embeds/ModeAnalytics.js b/app/embeds/ModeAnalytics.js index e8ead632..282f2a09 100644 --- a/app/embeds/ModeAnalytics.js +++ b/app/embeds/ModeAnalytics.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = new RegExp( - '^https://([w.-]+.)?modeanalytics.com/(.*)/reports/(.*)$' + "^https://([w.-]+.)?modeanalytics.com/(.*)/reports/(.*)$" ); type Props = {| @@ -18,7 +18,7 @@ export default class ModeAnalytics extends React.Component { render() { // Allow users to paste embed or standard urls and handle them the same - const normalizedUrl = this.props.attrs.href.replace(/\/embed$/, ''); + const normalizedUrl = this.props.attrs.href.replace(/\/embed$/, ""); return ( diff --git a/app/embeds/ModeAnalytics.test.js b/app/embeds/ModeAnalytics.test.js index cacab1fd..db2cc5e9 100644 --- a/app/embeds/ModeAnalytics.test.js +++ b/app/embeds/ModeAnalytics.test.js @@ -1,17 +1,17 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import ModeAnalytics from './ModeAnalytics'; +import ModeAnalytics from "./ModeAnalytics"; -describe('ModeAnalytics', () => { +describe("ModeAnalytics", () => { const match = ModeAnalytics.ENABLED[0]; - test('to be enabled on report link', () => { + test("to be enabled on report link", () => { expect( - 'https://modeanalytics.com/outline/reports/5aca06064f56'.match(match) + "https://modeanalytics.com/outline/reports/5aca06064f56".match(match) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://modeanalytics.com'.match(match)).toBe(null); - expect('https://modeanalytics.com/outline'.match(match)).toBe(null); - expect('https://modeanalytics.com/outline/reports'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://modeanalytics.com".match(match)).toBe(null); + expect("https://modeanalytics.com/outline".match(match)).toBe(null); + expect("https://modeanalytics.com/outline/reports".match(match)).toBe(null); }); }); diff --git a/app/embeds/Prezi.js b/app/embeds/Prezi.js index 188304fa..05e35648 100644 --- a/app/embeds/Prezi.js +++ b/app/embeds/Prezi.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; -const URL_REGEX = new RegExp('^https://prezi.com/view/(.*)$'); +const URL_REGEX = new RegExp("^https://prezi.com/view/(.*)$"); type Props = {| attrs: {| @@ -15,7 +15,7 @@ export default class Prezi extends React.Component { static ENABLED = [URL_REGEX]; render() { - const url = this.props.attrs.href.replace(/\/embed$/, ''); + const url = this.props.attrs.href.replace(/\/embed$/, ""); return ; } diff --git a/app/embeds/Prezi.test.js b/app/embeds/Prezi.test.js index 7be358c2..213e1b3d 100644 --- a/app/embeds/Prezi.test.js +++ b/app/embeds/Prezi.test.js @@ -1,22 +1,22 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Prezi from './Prezi'; +import Prezi from "./Prezi"; -describe('Prezi', () => { +describe("Prezi", () => { const match = Prezi.ENABLED[0]; - test('to be enabled on share link', () => { + test("to be enabled on share link", () => { expect( - 'https://prezi.com/view/39mn8Rn1ZkoeEKQCgk5C'.match(match) + "https://prezi.com/view/39mn8Rn1ZkoeEKQCgk5C".match(match) ).toBeTruthy(); }); - test('to be enabled on embed link', () => { + test("to be enabled on embed link", () => { expect( - 'https://prezi.com/view/39mn8Rn1ZkoeEKQCgk5C/embed'.match(match) + "https://prezi.com/view/39mn8Rn1ZkoeEKQCgk5C/embed".match(match) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://prezi.com'.match(match)).toBe(null); - expect('https://prezi.com/pricing'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://prezi.com".match(match)).toBe(null); + expect("https://prezi.com/pricing".match(match)).toBe(null); }); }); diff --git a/app/embeds/Spotify.js b/app/embeds/Spotify.js index a9655565..9b08932c 100644 --- a/app/embeds/Spotify.js +++ b/app/embeds/Spotify.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; -const URL_REGEX = new RegExp('https?://open.spotify.com/(.*)$'); +const URL_REGEX = new RegExp("https?://open.spotify.com/(.*)$"); type Props = {| attrs: {| @@ -18,12 +18,12 @@ export default class Spotify extends React.Component { const parsed = new URL(this.props.attrs.href); return parsed.pathname; } catch (err) { - return ''; + return ""; } } render() { - const normalizedPath = this.pathname.replace(/^\/embed/, '/'); + const normalizedPath = this.pathname.replace(/^\/embed/, "/"); return ( { +describe("Spotify", () => { const match = Spotify.ENABLED[0]; - test('to be enabled on song link', () => { + test("to be enabled on song link", () => { expect( - 'https://open.spotify.com/track/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg'.match( + "https://open.spotify.com/track/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg".match( match ) ).toBeTruthy(); }); - test('to be enabled on playlist link', () => { + test("to be enabled on playlist link", () => { expect( - 'https://open.spotify.com/user/spotify/playlist/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg'.match( + "https://open.spotify.com/user/spotify/playlist/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg".match( match ) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://spotify.com'.match(match)).toBe(null); - expect('https://open.spotify.com'.match(match)).toBe(null); - expect('https://www.spotify.com'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://spotify.com".match(match)).toBe(null); + expect("https://open.spotify.com".match(match)).toBe(null); + expect("https://www.spotify.com".match(match)).toBe(null); }); }); diff --git a/app/embeds/Trello.js b/app/embeds/Trello.js index 18511e6d..991385a4 100644 --- a/app/embeds/Trello.js +++ b/app/embeds/Trello.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = /^https:\/\/trello.com\/(c|b)\/([^/]*)(.*)?$/; @@ -18,7 +18,7 @@ export default class Trello extends React.Component { const { matches } = this.props.attrs; const objectId = matches[2]; - if (matches[1] === 'c') { + if (matches[1] === "c") { return ( { +describe("Typeform", () => { const match = Typeform.ENABLED[0]; - test('to be enabled on share link', () => { + test("to be enabled on share link", () => { expect( - 'https://beardyman.typeform.com/to/zvlr4L'.match(match) + "https://beardyman.typeform.com/to/zvlr4L".match(match) ).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://www.typeform.com'.match(match)).toBe(null); - expect('https://typeform.com/to/zvlr4L'.match(match)).toBe(null); - expect('https://typeform.com/features'.match(match)).toBe(null); + test("to not be enabled elsewhere", () => { + expect("https://www.typeform.com".match(match)).toBe(null); + expect("https://typeform.com/to/zvlr4L".match(match)).toBe(null); + expect("https://typeform.com/features".match(match)).toBe(null); }); }); diff --git a/app/embeds/Vimeo.js b/app/embeds/Vimeo.js index 51177678..49aed7d8 100644 --- a/app/embeds/Vimeo.js +++ b/app/embeds/Vimeo.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = /(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)/; diff --git a/app/embeds/Vimeo.test.js b/app/embeds/Vimeo.test.js index 610e9dc8..1bc94dcd 100644 --- a/app/embeds/Vimeo.test.js +++ b/app/embeds/Vimeo.test.js @@ -1,17 +1,17 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import Vimeo from './Vimeo'; +import Vimeo from "./Vimeo"; -describe('Vimeo', () => { +describe("Vimeo", () => { const match = Vimeo.ENABLED[0]; - test('to be enabled on video link', () => { - expect('https://vimeo.com/265045525'.match(match)).toBeTruthy(); + test("to be enabled on video link", () => { + expect("https://vimeo.com/265045525".match(match)).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://vimeo.com'.match(match)).toBe(null); - expect('https://www.vimeo.com'.match(match)).toBe(null); - expect('https://vimeo.com/upgrade'.match(match)).toBe(null); - expect('https://vimeo.com/features/video-marketing'.match(match)).toBe( + test("to not be enabled elsewhere", () => { + expect("https://vimeo.com".match(match)).toBe(null); + expect("https://www.vimeo.com".match(match)).toBe(null); + expect("https://vimeo.com/upgrade".match(match)).toBe(null); + expect("https://vimeo.com/features/video-marketing".match(match)).toBe( null ); }); diff --git a/app/embeds/YouTube.js b/app/embeds/YouTube.js index 9c8e1c88..1de89410 100644 --- a/app/embeds/YouTube.js +++ b/app/embeds/YouTube.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import Frame from './components/Frame'; +import * as React from "react"; +import Frame from "./components/Frame"; const URL_REGEX = /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})$/i; diff --git a/app/embeds/YouTube.test.js b/app/embeds/YouTube.test.js index b4afd34e..a09b4e61 100644 --- a/app/embeds/YouTube.test.js +++ b/app/embeds/YouTube.test.js @@ -1,30 +1,30 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import YouTube from './YouTube'; +import YouTube from "./YouTube"; -describe('YouTube', () => { +describe("YouTube", () => { const match = YouTube.ENABLED[0]; - test('to be enabled on video link', () => { + test("to be enabled on video link", () => { expect( - 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'.match(match) + "https://www.youtube.com/watch?v=dQw4w9WgXcQ".match(match) ).toBeTruthy(); }); - test('to be enabled on embed link', () => { + test("to be enabled on embed link", () => { expect( - 'https://www.youtube.com/embed?v=dQw4w9WgXcQ'.match(match) + "https://www.youtube.com/embed?v=dQw4w9WgXcQ".match(match) ).toBeTruthy(); }); - test('to be enabled on shortlink', () => { - expect('https://youtu.be/dQw4w9WgXcQ'.match(match)).toBeTruthy(); + test("to be enabled on shortlink", () => { + expect("https://youtu.be/dQw4w9WgXcQ".match(match)).toBeTruthy(); }); - test('to not be enabled elsewhere', () => { - expect('https://youtu.be'.match(match)).toBe(null); - expect('https://youtube.com'.match(match)).toBe(null); - expect('https://www.youtube.com'.match(match)).toBe(null); - expect('https://www.youtube.com/logout'.match(match)).toBe(null); - expect('https://www.youtube.com/feed/subscriptions'.match(match)).toBe( + test("to not be enabled elsewhere", () => { + expect("https://youtu.be".match(match)).toBe(null); + expect("https://youtube.com".match(match)).toBe(null); + expect("https://www.youtube.com".match(match)).toBe(null); + expect("https://www.youtube.com/logout".match(match)).toBe(null); + expect("https://www.youtube.com/feed/subscriptions".match(match)).toBe( null ); }); diff --git a/app/embeds/components/Frame.js b/app/embeds/components/Frame.js index d60ed63f..1fa50144 100644 --- a/app/embeds/components/Frame.js +++ b/app/embeds/components/Frame.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import styled from 'styled-components'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import styled from "styled-components"; type Props = { src?: string, @@ -34,12 +34,12 @@ class Frame extends React.Component { render() { const { border, - width = '100%', - height = '400px', + width = "100%", + height = "400px", forwardedRef, ...rest } = this.props; - const Component = border ? StyledIframe : 'iframe'; + const Component = border ? StyledIframe : "iframe"; return ( diff --git a/app/embeds/index.js b/app/embeds/index.js index 343c011d..27319b9d 100644 --- a/app/embeds/index.js +++ b/app/embeds/index.js @@ -1,28 +1,28 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Abstract from './Abstract'; -import Airtable from './Airtable'; -import Codepen from './Codepen'; -import Figma from './Figma'; -import Framer from './Framer'; -import Gist from './Gist'; -import GoogleDocs from './GoogleDocs'; -import GoogleSheets from './GoogleSheets'; -import GoogleSlides from './GoogleSlides'; -import InVision from './InVision'; -import Loom from './Loom'; -import Lucidchart from './Lucidchart'; -import Marvel from './Marvel'; -import Mindmeister from './Mindmeister'; -import Miro from './Miro'; -import ModeAnalytics from './ModeAnalytics'; -import Prezi from './Prezi'; -import Spotify from './Spotify'; -import Trello from './Trello'; -import Typeform from './Typeform'; -import Vimeo from './Vimeo'; -import YouTube from './YouTube'; +import * as React from "react"; +import styled from "styled-components"; +import Abstract from "./Abstract"; +import Airtable from "./Airtable"; +import Codepen from "./Codepen"; +import Figma from "./Figma"; +import Framer from "./Framer"; +import Gist from "./Gist"; +import GoogleDocs from "./GoogleDocs"; +import GoogleSheets from "./GoogleSheets"; +import GoogleSlides from "./GoogleSlides"; +import InVision from "./InVision"; +import Loom from "./Loom"; +import Lucidchart from "./Lucidchart"; +import Marvel from "./Marvel"; +import Mindmeister from "./Mindmeister"; +import Miro from "./Miro"; +import ModeAnalytics from "./ModeAnalytics"; +import Prezi from "./Prezi"; +import Spotify from "./Spotify"; +import Trello from "./Trello"; +import Typeform from "./Typeform"; +import Vimeo from "./Vimeo"; +import YouTube from "./YouTube"; function matcher(Component) { return (url: string) => { @@ -44,154 +44,154 @@ const Img = styled.img` export default [ { - title: 'Abstract', - keywords: 'design', + title: "Abstract", + keywords: "design", icon: () => , component: Abstract, matcher: matcher(Abstract), }, { - title: 'Airtable', - keywords: 'spreadsheet', + title: "Airtable", + keywords: "spreadsheet", icon: () => , component: Airtable, matcher: matcher(Airtable), }, { - title: 'Codepen', - keywords: 'code editor', + title: "Codepen", + keywords: "code editor", icon: () => , component: Codepen, matcher: matcher(Codepen), }, { - title: 'Figma', - keywords: 'design svg vector', + title: "Figma", + keywords: "design svg vector", icon: () => , component: Figma, matcher: matcher(Figma), }, { - title: 'Framer', - keywords: 'design prototyping', + title: "Framer", + keywords: "design prototyping", icon: () => , component: Framer, matcher: matcher(Framer), }, { - title: 'GitHub Gist', - keywords: 'code', + title: "GitHub Gist", + keywords: "code", icon: () => , component: Gist, matcher: matcher(Gist), }, { - title: 'Google Docs', + title: "Google Docs", icon: () => , component: GoogleDocs, matcher: matcher(GoogleDocs), }, { - title: 'Google Sheets', - keywords: 'excel spreadsheet', + title: "Google Sheets", + keywords: "excel spreadsheet", icon: () => , component: GoogleSheets, matcher: matcher(GoogleSheets), }, { - title: 'Google Slides', - keywords: 'presentation slideshow', + title: "Google Slides", + keywords: "presentation slideshow", icon: () => , component: GoogleSlides, matcher: matcher(GoogleSlides), }, { - title: 'InVision', - keywords: 'design prototype', + title: "InVision", + keywords: "design prototype", icon: () => , component: InVision, matcher: matcher(InVision), }, { - title: 'Loom', - keywords: 'video screencast', + title: "Loom", + keywords: "video screencast", icon: () => , component: Loom, matcher: matcher(Loom), }, { - title: 'Lucidchart', - keywords: 'chart', + title: "Lucidchart", + keywords: "chart", icon: () => , component: Lucidchart, matcher: matcher(Lucidchart), }, { - title: 'Marvel', - keywords: 'design prototype', + title: "Marvel", + keywords: "design prototype", icon: () => , component: Marvel, matcher: matcher(Marvel), }, { - title: 'Mindmeister', - keywords: 'mindmap', + title: "Mindmeister", + keywords: "mindmap", icon: () => , component: Mindmeister, matcher: matcher(Mindmeister), }, { - title: 'Miro', - keywords: 'whiteboard', + title: "Miro", + keywords: "whiteboard", icon: () => , component: Miro, matcher: matcher(Miro), }, { - title: 'Mode', - keywords: 'analytics', + title: "Mode", + keywords: "analytics", icon: () => , component: ModeAnalytics, matcher: matcher(ModeAnalytics), }, { - title: 'Prezi', - keywords: 'presentation', + title: "Prezi", + keywords: "presentation", icon: () => , component: Prezi, matcher: matcher(Prezi), }, { - title: 'Spotify', - keywords: 'music', + title: "Spotify", + keywords: "music", icon: () => , component: Spotify, matcher: matcher(Spotify), }, { - title: 'Trello', - keywords: 'kanban', + title: "Trello", + keywords: "kanban", icon: () => , component: Trello, matcher: matcher(Trello), }, { - title: 'Typeform', - keywords: 'form survey', + title: "Typeform", + keywords: "form survey", icon: () => , component: Typeform, matcher: matcher(Typeform), }, { - title: 'Vimeo', - keywords: 'video', + title: "Vimeo", + keywords: "video", icon: () => , component: Vimeo, matcher: matcher(Vimeo), }, { - title: 'YouTube', - keywords: 'google video', + title: "YouTube", + keywords: "google video", icon: () => , component: YouTube, matcher: matcher(YouTube), diff --git a/app/index.js b/app/index.js index 57bf7e97..27a6b80a 100644 --- a/app/index.js +++ b/app/index.js @@ -1,22 +1,22 @@ // @flow -import * as React from 'react'; -import { render } from 'react-dom'; -import { Provider } from 'mobx-react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import stores from 'stores'; +import * as React from "react"; +import { render } from "react-dom"; +import { Provider } from "mobx-react"; +import { BrowserRouter as Router } from "react-router-dom"; +import stores from "stores"; -import ErrorBoundary from 'components/ErrorBoundary'; -import ScrollToTop from 'components/ScrollToTop'; -import Toasts from 'components/Toasts'; -import Theme from 'components/Theme'; -import Routes from './routes'; +import ErrorBoundary from "components/ErrorBoundary"; +import ScrollToTop from "components/ScrollToTop"; +import Toasts from "components/Toasts"; +import Theme from "components/Theme"; +import Routes from "./routes"; let DevTools; if (__DEV__) { - DevTools = require('mobx-react-devtools').default; // eslint-disable-line global-require + DevTools = require("mobx-react-devtools").default; // eslint-disable-line global-require } -const element = document.getElementById('root'); +const element = document.getElementById("root"); if (element) { render( @@ -41,17 +41,17 @@ if (element) { ); } -window.addEventListener('load', async () => { +window.addEventListener("load", async () => { // installation does not use Google Analytics, or tracking is blocked on client // no point loading the rest of the analytics bundles if (!process.env.GOOGLE_ANALYTICS_ID || !window.ga) return; // https://github.com/googleanalytics/autotrack/issues/137#issuecomment-305890099 - await import('autotrack/autotrack.js'); + await import("autotrack/autotrack.js"); - window.ga('require', 'outboundLinkTracker'); - window.ga('require', 'urlChangeTracker'); - window.ga('require', 'eventTracker', { - attributePrefix: 'data-', + window.ga("require", "outboundLinkTracker"); + window.ga("require", "urlChangeTracker"); + window.ga("require", "eventTracker", { + attributePrefix: "data-", }); }); diff --git a/app/menus/AccountMenu.js b/app/menus/AccountMenu.js index 2490100d..e5777f98 100644 --- a/app/menus/AccountMenu.js +++ b/app/menus/AccountMenu.js @@ -1,16 +1,16 @@ // @flow -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import { SunIcon, MoonIcon } from 'outline-icons'; -import styled from 'styled-components'; -import UiStore from 'stores/UiStore'; -import AuthStore from 'stores/AuthStore'; -import Flex from 'shared/components/Flex'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; -import Modal from 'components/Modal'; -import KeyboardShortcuts from 'scenes/KeyboardShortcuts'; +import * as React from "react"; +import { Link } from "react-router-dom"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import { SunIcon, MoonIcon } from "outline-icons"; +import styled from "styled-components"; +import UiStore from "stores/UiStore"; +import AuthStore from "stores/AuthStore"; +import Flex from "shared/components/Flex"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; +import Modal from "components/Modal"; +import KeyboardShortcuts from "scenes/KeyboardShortcuts"; import { developers, changelog, @@ -18,7 +18,7 @@ import { mailToUrl, spectrumUrl, settings, -} from '../../shared/utils/routeHelpers'; +} from "../../shared/utils/routeHelpers"; type Props = { label: React.Node, @@ -85,34 +85,34 @@ class AccountMenu extends React.Component { position="right" style={{ left: 170, - position: 'relative', + position: "relative", top: -34, }} label={ Appearance - {ui.resolvedTheme === 'light' ? : } + {ui.resolvedTheme === "light" ? : } } hover > ui.setTheme('system')} - selected={ui.theme === 'system'} + onClick={() => ui.setTheme("system")} + selected={ui.theme === "system"} > System ui.setTheme('light')} - selected={ui.theme === 'light'} + onClick={() => ui.setTheme("light")} + selected={ui.theme === "light"} > Light ui.setTheme('dark')} - selected={ui.theme === 'dark'} + onClick={() => ui.setTheme("dark")} + selected={ui.theme === "dark"} > Dark @@ -131,4 +131,4 @@ const ChangeTheme = styled(Flex)` width: 100%; `; -export default inject('ui', 'auth')(AccountMenu); +export default inject("ui", "auth")(AccountMenu); diff --git a/app/menus/CollectionMenu.js b/app/menus/CollectionMenu.js index 5610d359..cbb07f20 100644 --- a/app/menus/CollectionMenu.js +++ b/app/menus/CollectionMenu.js @@ -1,23 +1,23 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import Modal from 'components/Modal'; -import VisuallyHidden from 'components/VisuallyHidden'; -import CollectionMembers from 'scenes/CollectionMembers'; +import * as React from "react"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import Modal from "components/Modal"; +import VisuallyHidden from "components/VisuallyHidden"; +import CollectionMembers from "scenes/CollectionMembers"; -import { newDocumentUrl } from 'utils/routeHelpers'; -import getDataTransferFiles from 'utils/getDataTransferFiles'; -import importFile from 'utils/importFile'; -import Collection from 'models/Collection'; -import UiStore from 'stores/UiStore'; -import DocumentsStore from 'stores/DocumentsStore'; -import PoliciesStore from 'stores/PoliciesStore'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; +import { newDocumentUrl } from "utils/routeHelpers"; +import getDataTransferFiles from "utils/getDataTransferFiles"; +import importFile from "utils/importFile"; +import Collection from "models/Collection"; +import UiStore from "stores/UiStore"; +import DocumentsStore from "stores/DocumentsStore"; +import PoliciesStore from "stores/PoliciesStore"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; type Props = { - position?: 'left' | 'right' | 'center', + position?: "left" | "right" | "center", ui: UiStore, policies: PoliciesStore, documents: DocumentsStore, @@ -64,19 +64,19 @@ class CollectionMenu extends React.Component { onEdit = (ev: SyntheticEvent<>) => { ev.preventDefault(); const { collection } = this.props; - this.props.ui.setActiveModal('collection-edit', { collection }); + this.props.ui.setActiveModal("collection-edit", { collection }); }; onDelete = (ev: SyntheticEvent<>) => { ev.preventDefault(); const { collection } = this.props; - this.props.ui.setActiveModal('collection-delete', { collection }); + this.props.ui.setActiveModal("collection-delete", { collection }); }; onExport = (ev: SyntheticEvent<>) => { ev.preventDefault(); const { collection } = this.props; - this.props.ui.setActiveModal('collection-export', { collection }); + this.props.ui.setActiveModal("collection-export", { collection }); }; onPermissions = (ev: SyntheticEvent<>) => { @@ -153,6 +153,6 @@ class CollectionMenu extends React.Component { } } -export default inject('ui', 'documents', 'policies')( +export default inject("ui", "documents", "policies")( withRouter(CollectionMenu) ); diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js index 6d1a9da8..cda74fc0 100644 --- a/app/menus/DocumentMenu.js +++ b/app/menus/DocumentMenu.js @@ -1,27 +1,27 @@ // @flow -import * as React from 'react'; -import { Redirect } from 'react-router-dom'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; +import * as React from "react"; +import { Redirect } from "react-router-dom"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; -import Document from 'models/Document'; -import UiStore from 'stores/UiStore'; -import AuthStore from 'stores/AuthStore'; -import CollectionStore from 'stores/CollectionsStore'; -import PoliciesStore from 'stores/PoliciesStore'; +import Document from "models/Document"; +import UiStore from "stores/UiStore"; +import AuthStore from "stores/AuthStore"; +import CollectionStore from "stores/CollectionsStore"; +import PoliciesStore from "stores/PoliciesStore"; import { documentUrl, documentMoveUrl, documentEditUrl, documentHistoryUrl, newDocumentUrl, -} from 'utils/routeHelpers'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; +} from "utils/routeHelpers"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; type Props = { ui: UiStore, auth: AuthStore, - position?: 'left' | 'right' | 'center', + position?: "left" | "right" | "center", document: Document, collections: CollectionStore, policies: PoliciesStore, @@ -49,7 +49,7 @@ class DocumentMenu extends React.Component { handleDelete = (ev: SyntheticEvent<>) => { const { document } = this.props; - this.props.ui.setActiveModal('document-delete', { document }); + this.props.ui.setActiveModal("document-delete", { document }); }; handleDocumentHistory = () => { @@ -73,17 +73,17 @@ class DocumentMenu extends React.Component { // when duplicating, go straight to the duplicated document content this.redirectTo = duped.url; - this.props.ui.showToast('Document duplicated'); + this.props.ui.showToast("Document duplicated"); }; handleArchive = async (ev: SyntheticEvent<>) => { await this.props.document.archive(); - this.props.ui.showToast('Document archived'); + this.props.ui.showToast("Document archived"); }; handleRestore = async (ev: SyntheticEvent<>) => { await this.props.document.restore(); - this.props.ui.showToast('Document restored'); + this.props.ui.showToast("Document restored"); }; handlePin = (ev: SyntheticEvent<>) => { @@ -112,7 +112,7 @@ class DocumentMenu extends React.Component { const { document } = this.props; if (!document.shareUrl) await document.share(); - this.props.ui.setActiveModal('document-share', { document }); + this.props.ui.setActiveModal("document-share", { document }); }; render() { @@ -242,4 +242,4 @@ class DocumentMenu extends React.Component { } } -export default inject('ui', 'auth', 'collections', 'policies')(DocumentMenu); +export default inject("ui", "auth", "collections", "policies")(DocumentMenu); diff --git a/app/menus/GroupMenu.js b/app/menus/GroupMenu.js index bab5b2a9..d8ca5d07 100644 --- a/app/menus/GroupMenu.js +++ b/app/menus/GroupMenu.js @@ -1,16 +1,16 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import Modal from 'components/Modal'; -import GroupEdit from 'scenes/GroupEdit'; -import GroupDelete from 'scenes/GroupDelete'; +import * as React from "react"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import Modal from "components/Modal"; +import GroupEdit from "scenes/GroupEdit"; +import GroupDelete from "scenes/GroupDelete"; -import Group from 'models/Group'; -import UiStore from 'stores/UiStore'; -import PoliciesStore from 'stores/PoliciesStore'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; +import Group from "models/Group"; +import UiStore from "stores/UiStore"; +import PoliciesStore from "stores/PoliciesStore"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; type Props = { ui: UiStore, @@ -99,4 +99,4 @@ class GroupMenu extends React.Component { } } -export default inject('policies')(withRouter(GroupMenu)); +export default inject("policies")(withRouter(GroupMenu)); diff --git a/app/menus/NewChildDocumentMenu.js b/app/menus/NewChildDocumentMenu.js index 70bb98d6..34d556aa 100644 --- a/app/menus/NewChildDocumentMenu.js +++ b/app/menus/NewChildDocumentMenu.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { Redirect } from 'react-router-dom'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { MoreIcon } from 'outline-icons'; +import * as React from "react"; +import { Redirect } from "react-router-dom"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import { MoreIcon } from "outline-icons"; -import { newDocumentUrl } from 'utils/routeHelpers'; -import Document from 'models/Document'; -import CollectionsStore from 'stores/CollectionsStore'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; +import { newDocumentUrl } from "utils/routeHelpers"; +import Document from "models/Document"; +import CollectionsStore from "stores/CollectionsStore"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; type Props = { label?: React.Node, @@ -44,8 +44,8 @@ class NewChildDocumentMenu extends React.Component { } {...rest}> - New document in{' '} - {collection ? collection.name : 'collection'} + New document in{" "} + {collection ? collection.name : "collection"} @@ -56,4 +56,4 @@ class NewChildDocumentMenu extends React.Component { } } -export default inject('collections')(NewChildDocumentMenu); +export default inject("collections")(NewChildDocumentMenu); diff --git a/app/menus/NewDocumentMenu.js b/app/menus/NewDocumentMenu.js index 33c709ee..9d470f25 100644 --- a/app/menus/NewDocumentMenu.js +++ b/app/menus/NewDocumentMenu.js @@ -1,16 +1,16 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import { Redirect } from 'react-router-dom'; -import { PlusIcon } from 'outline-icons'; +import * as React from "react"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import { Redirect } from "react-router-dom"; +import { PlusIcon } from "outline-icons"; -import { newDocumentUrl } from 'utils/routeHelpers'; -import CollectionsStore from 'stores/CollectionsStore'; -import PoliciesStore from 'stores/PoliciesStore'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; -import Button from 'components/Button'; -import CollectionIcon from 'components/CollectionIcon'; +import { newDocumentUrl } from "utils/routeHelpers"; +import CollectionsStore from "stores/CollectionsStore"; +import PoliciesStore from "stores/PoliciesStore"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; +import Button from "components/Button"; +import CollectionIcon from "components/CollectionIcon"; type Props = { label?: React.Node, @@ -74,4 +74,4 @@ class NewDocumentMenu extends React.Component { } } -export default inject('collections', 'policies')(NewDocumentMenu); +export default inject("collections", "policies")(NewDocumentMenu); diff --git a/app/menus/RevisionMenu.js b/app/menus/RevisionMenu.js index caa37702..e780d8ad 100644 --- a/app/menus/RevisionMenu.js +++ b/app/menus/RevisionMenu.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import { inject } from 'mobx-react'; +import * as React from "react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import { inject } from "mobx-react"; -import CopyToClipboard from 'components/CopyToClipboard'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; -import { documentHistoryUrl } from 'utils/routeHelpers'; -import Revision from 'models/Revision'; -import Document from 'models/Document'; -import UiStore from 'stores/UiStore'; +import CopyToClipboard from "components/CopyToClipboard"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; +import { documentHistoryUrl } from "utils/routeHelpers"; +import Revision from "models/Revision"; +import Document from "models/Document"; +import UiStore from "stores/UiStore"; type Props = { onOpen?: () => void, @@ -25,12 +25,12 @@ class RevisionMenu extends React.Component { handleRestore = async (ev: SyntheticEvent<>) => { ev.preventDefault(); await this.props.document.restore(this.props.revision); - this.props.ui.showToast('Document restored'); + this.props.ui.showToast("Document restored"); this.props.history.push(this.props.document.url); }; handleCopy = () => { - this.props.ui.showToast('Link copied'); + this.props.ui.showToast("Link copied"); }; render() { @@ -59,4 +59,4 @@ class RevisionMenu extends React.Component { } } -export default withRouter(inject('ui')(RevisionMenu)); +export default withRouter(inject("ui")(RevisionMenu)); diff --git a/app/menus/ShareMenu.js b/app/menus/ShareMenu.js index 5da87220..dd266284 100644 --- a/app/menus/ShareMenu.js +++ b/app/menus/ShareMenu.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { Redirect } from 'react-router-dom'; -import { inject, observer } from 'mobx-react'; -import { observable } from 'mobx'; +import * as React from "react"; +import { Redirect } from "react-router-dom"; +import { inject, observer } from "mobx-react"; +import { observable } from "mobx"; -import CopyToClipboard from 'components/CopyToClipboard'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; -import SharesStore from 'stores/SharesStore'; -import UiStore from 'stores/UiStore'; -import Share from 'models/Share'; +import CopyToClipboard from "components/CopyToClipboard"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; +import SharesStore from "stores/SharesStore"; +import UiStore from "stores/UiStore"; +import Share from "models/Share"; type Props = { onOpen?: () => void, @@ -34,11 +34,11 @@ class ShareMenu extends React.Component { handleRevoke = (ev: SyntheticEvent<>) => { ev.preventDefault(); this.props.shares.revoke(this.props.share); - this.props.ui.showToast('Share link revoked'); + this.props.ui.showToast("Share link revoked"); }; handleCopy = () => { - this.props.ui.showToast('Share link copied'); + this.props.ui.showToast("Share link copied"); }; render() { @@ -63,4 +63,4 @@ class ShareMenu extends React.Component { } } -export default inject('shares', 'ui')(ShareMenu); +export default inject("shares", "ui")(ShareMenu); diff --git a/app/menus/UserMenu.js b/app/menus/UserMenu.js index eb5a90ca..a3595c88 100644 --- a/app/menus/UserMenu.js +++ b/app/menus/UserMenu.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import { inject, observer } from 'mobx-react'; +import * as React from "react"; +import { inject, observer } from "mobx-react"; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; -import UsersStore from 'stores/UsersStore'; -import User from 'models/User'; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; +import UsersStore from "stores/UsersStore"; +import User from "models/User"; type Props = { user: User, @@ -42,7 +42,7 @@ class UserMenu extends React.Component { const { user, users } = this.props; if ( !window.confirm( - 'Are you want to suspend this account? Suspended users will be prevented from logging in.' + "Are you want to suspend this account? Suspended users will be prevented from logging in." ) ) { return; @@ -98,4 +98,4 @@ class UserMenu extends React.Component { } } -export default inject('users')(UserMenu); +export default inject("users")(UserMenu); diff --git a/app/models/ApiKey.js b/app/models/ApiKey.js index f0e13e6a..bd920c08 100644 --- a/app/models/ApiKey.js +++ b/app/models/ApiKey.js @@ -1,5 +1,5 @@ // @flow -import BaseModel from './BaseModel'; +import BaseModel from "./BaseModel"; class ApiKey extends BaseModel { id: string; diff --git a/app/models/BaseModel.js b/app/models/BaseModel.js index 4a10d026..10754755 100644 --- a/app/models/BaseModel.js +++ b/app/models/BaseModel.js @@ -1,5 +1,5 @@ // @flow -import { set, observable } from 'mobx'; +import { set, observable } from "mobx"; export default class BaseModel { @observable id: string; diff --git a/app/models/Collection.js b/app/models/Collection.js index 4639226d..dab6cbb7 100644 --- a/app/models/Collection.js +++ b/app/models/Collection.js @@ -1,10 +1,10 @@ // @flow -import { pick } from 'lodash'; -import { action, computed, observable } from 'mobx'; -import BaseModel from 'models/BaseModel'; -import Document from 'models/Document'; -import { client } from 'utils/ApiClient'; -import type { NavigationNode } from 'types'; +import { pick } from "lodash"; +import { action, computed, observable } from "mobx"; +import BaseModel from "models/BaseModel"; +import Document from "models/Document"; +import { client } from "utils/ApiClient"; +import type { NavigationNode } from "types"; export default class Collection extends BaseModel { @observable isSaving: boolean; @@ -16,7 +16,7 @@ export default class Collection extends BaseModel { icon: string; color: string; private: boolean; - type: 'atlas' | 'journal'; + type: "atlas" | "journal"; documents: NavigationNode[]; createdAt: ?string; updatedAt: ?string; @@ -103,18 +103,18 @@ export default class Collection extends BaseModel { toJS = () => { return pick(this, [ - 'id', - 'name', - 'color', - 'description', - 'icon', - 'private', + "id", + "name", + "color", + "description", + "icon", + "private", ]); }; export = () => { return client.get( - '/collections.export', + "/collections.export", { id: this.id }, { download: true } ); diff --git a/app/models/CollectionGroupMembership.js b/app/models/CollectionGroupMembership.js index ef529f93..692620ee 100644 --- a/app/models/CollectionGroupMembership.js +++ b/app/models/CollectionGroupMembership.js @@ -1,6 +1,6 @@ // @flow -import { computed } from 'mobx'; -import BaseModel from './BaseModel'; +import { computed } from "mobx"; +import BaseModel from "./BaseModel"; class CollectionGroupMembership extends BaseModel { id: string; @@ -10,12 +10,12 @@ class CollectionGroupMembership extends BaseModel { @computed get isEditor(): boolean { - return this.permission === 'read_write'; + return this.permission === "read_write"; } @computed get isMaintainer(): boolean { - return this.permission === 'maintainer'; + return this.permission === "maintainer"; } } diff --git a/app/models/Document.js b/app/models/Document.js index 7c912a3e..35ebf094 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -1,14 +1,14 @@ // @flow -import { action, set, observable, computed } from 'mobx'; -import addDays from 'date-fns/add_days'; -import invariant from 'invariant'; -import { client } from 'utils/ApiClient'; -import parseTitle from 'shared/utils/parseTitle'; -import unescape from 'shared/utils/unescape'; -import BaseModel from 'models/BaseModel'; -import Revision from 'models/Revision'; -import User from 'models/User'; -import DocumentsStore from 'stores/DocumentsStore'; +import { action, set, observable, computed } from "mobx"; +import addDays from "date-fns/add_days"; +import invariant from "invariant"; +import { client } from "utils/ApiClient"; +import parseTitle from "shared/utils/parseTitle"; +import unescape from "shared/utils/unescape"; +import BaseModel from "models/BaseModel"; +import Revision from "models/Revision"; +import User from "models/User"; +import DocumentsStore from "stores/DocumentsStore"; type SaveOptions = { publish?: boolean, @@ -90,8 +90,8 @@ export default class Document extends BaseModel { @action share = async () => { - const res = await client.post('/shares.create', { documentId: this.id }); - invariant(res && res.data, 'Share data should be available'); + const res = await client.post("/shares.create", { documentId: this.id }); + invariant(res && res.data, "Share data should be available"); this.shareUrl = res.data.url; return this.shareUrl; }; @@ -125,7 +125,7 @@ export default class Document extends BaseModel { this.pinned = true; try { const res = await this.store.pin(this); - invariant(res && res.data, 'Data should be available'); + invariant(res && res.data, "Data should be available"); this.updateFromJson(res.data); } catch (err) { this.pinned = false; @@ -138,7 +138,7 @@ export default class Document extends BaseModel { this.pinned = false; try { const res = await this.store.unpin(this); - invariant(res && res.data, 'Data should be available'); + invariant(res && res.data, "Data should be available"); this.updateFromJson(res.data); } catch (err) { this.pinned = true; @@ -163,8 +163,8 @@ export default class Document extends BaseModel { @action fetch = async () => { - const res = await client.post('/documents.info', { id: this.id }); - invariant(res && res.data, 'Data should be available'); + const res = await client.post("/documents.info", { id: this.id }); + invariant(res && res.data, "Data should be available"); this.updateFromJson(res.data); }; @@ -196,7 +196,7 @@ export default class Document extends BaseModel { }); } - throw new Error('Attempting to update without a lastRevision'); + throw new Error("Attempting to update without a lastRevision"); } finally { this.isSaving = false; } @@ -216,15 +216,15 @@ export default class Document extends BaseModel { const body = unescape(this.text); const blob = new Blob([`# ${this.title}\n\n${body}`], { - type: 'text/markdown', + type: "text/markdown", }); const url = URL.createObjectURL(blob); - const a = document.createElement('a'); + const a = document.createElement("a"); // Firefox support requires the anchor tag be in the DOM to trigger the dl if (document.body) document.body.appendChild(a); a.href = url; - a.download = `${this.title || 'Untitled'}.md`; + a.download = `${this.title || "Untitled"}.md`; a.click(); }; } diff --git a/app/models/Event.js b/app/models/Event.js index f493547f..b2f175d2 100644 --- a/app/models/Event.js +++ b/app/models/Event.js @@ -1,6 +1,6 @@ // @flow -import BaseModel from './BaseModel'; -import User from './User'; +import BaseModel from "./BaseModel"; +import User from "./User"; class Event extends BaseModel { id: string; @@ -20,16 +20,16 @@ class Event extends BaseModel { }; get model() { - return this.name.split('.')[0]; + return this.name.split(".")[0]; } get verb() { - return this.name.split('.')[1]; + return this.name.split(".")[1]; } get verbPastTense() { const v = this.verb; - if (v.endsWith('e')) return `${v}d`; + if (v.endsWith("e")) return `${v}d`; return `${v}ed`; } } diff --git a/app/models/Group.js b/app/models/Group.js index 56f16a3e..cad3348e 100644 --- a/app/models/Group.js +++ b/app/models/Group.js @@ -1,5 +1,5 @@ // @flow -import BaseModel from './BaseModel'; +import BaseModel from "./BaseModel"; class Group extends BaseModel { id: string; diff --git a/app/models/GroupMembership.js b/app/models/GroupMembership.js index cd769862..c5d8a58c 100644 --- a/app/models/GroupMembership.js +++ b/app/models/GroupMembership.js @@ -1,5 +1,5 @@ // @flow -import BaseModel from './BaseModel'; +import BaseModel from "./BaseModel"; class GroupMembership extends BaseModel { id: string; diff --git a/app/models/Integration.js b/app/models/Integration.js index 6a677c44..c7e52f7d 100644 --- a/app/models/Integration.js +++ b/app/models/Integration.js @@ -1,8 +1,8 @@ // @flow -import { extendObservable, action } from 'mobx'; +import { extendObservable, action } from "mobx"; -import BaseModel from 'models/BaseModel'; -import { client } from 'utils/ApiClient'; +import BaseModel from "models/BaseModel"; +import { client } from "utils/ApiClient"; type Settings = { url: string, @@ -10,7 +10,7 @@ type Settings = { channelId: string, }; -type Events = 'documents.create' | 'collections.create'; +type Events = "documents.create" | "collections.create"; class Integration extends BaseModel { id: string; @@ -21,7 +21,7 @@ class Integration extends BaseModel { @action update = async (data: Object) => { - await client.post('/integrations.update', { id: this.id, ...data }); + await client.post("/integrations.update", { id: this.id, ...data }); extendObservable(this, data); return true; }; diff --git a/app/models/Membership.js b/app/models/Membership.js index 0aac3f90..ddd29b42 100644 --- a/app/models/Membership.js +++ b/app/models/Membership.js @@ -1,6 +1,6 @@ // @flow -import { computed } from 'mobx'; -import BaseModel from './BaseModel'; +import { computed } from "mobx"; +import BaseModel from "./BaseModel"; class Membership extends BaseModel { id: string; @@ -10,12 +10,12 @@ class Membership extends BaseModel { @computed get isEditor(): boolean { - return this.permission === 'read_write'; + return this.permission === "read_write"; } @computed get isMaintainer(): boolean { - return this.permission === 'maintainer'; + return this.permission === "maintainer"; } } diff --git a/app/models/NotificationSetting.js b/app/models/NotificationSetting.js index 5f305c93..d3569675 100644 --- a/app/models/NotificationSetting.js +++ b/app/models/NotificationSetting.js @@ -1,5 +1,5 @@ // @flow -import BaseModel from './BaseModel'; +import BaseModel from "./BaseModel"; class NotificationSetting extends BaseModel { id: string; diff --git a/app/models/Policy.js b/app/models/Policy.js index b28bfe1e..11775309 100644 --- a/app/models/Policy.js +++ b/app/models/Policy.js @@ -1,5 +1,5 @@ // @flow -import BaseModel from './BaseModel'; +import BaseModel from "./BaseModel"; class Policy extends BaseModel { id: string; diff --git a/app/models/Revision.js b/app/models/Revision.js index ce1571f9..18282d05 100644 --- a/app/models/Revision.js +++ b/app/models/Revision.js @@ -1,6 +1,6 @@ // @flow -import BaseModel from './BaseModel'; -import User from './User'; +import BaseModel from "./BaseModel"; +import User from "./User"; class Revision extends BaseModel { id: string; diff --git a/app/models/Share.js b/app/models/Share.js index 7391ded2..b3902d71 100644 --- a/app/models/Share.js +++ b/app/models/Share.js @@ -1,6 +1,6 @@ // @flow -import BaseModel from './BaseModel'; -import User from './User'; +import BaseModel from "./BaseModel"; +import User from "./User"; class Share extends BaseModel { id: string; diff --git a/app/models/Team.js b/app/models/Team.js index d149ab4b..0c4a067f 100644 --- a/app/models/Team.js +++ b/app/models/Team.js @@ -1,6 +1,6 @@ // @flow -import { computed } from 'mobx'; -import BaseModel from './BaseModel'; +import { computed } from "mobx"; +import BaseModel from "./BaseModel"; class Team extends BaseModel { id: string; @@ -17,10 +17,10 @@ class Team extends BaseModel { @computed get signinMethods(): string { if (this.slackConnected && this.googleConnected) { - return 'Slack or Google'; + return "Slack or Google"; } - if (this.slackConnected) return 'Slack'; - return 'Google'; + if (this.slackConnected) return "Slack"; + return "Google"; } } diff --git a/app/models/User.js b/app/models/User.js index 4242642b..cc732833 100644 --- a/app/models/User.js +++ b/app/models/User.js @@ -1,5 +1,5 @@ // @flow -import BaseModel from './BaseModel'; +import BaseModel from "./BaseModel"; class User extends BaseModel { avatarUrl: string; diff --git a/app/models/View.js b/app/models/View.js index 40c244aa..aa717daf 100644 --- a/app/models/View.js +++ b/app/models/View.js @@ -1,7 +1,7 @@ // @flow -import { action } from 'mobx'; -import BaseModel from './BaseModel'; -import User from './User'; +import { action } from "mobx"; +import BaseModel from "./BaseModel"; +import User from "./User"; class View extends BaseModel { id: string; diff --git a/app/routes.js b/app/routes.js index da15ad12..6f74ba2a 100644 --- a/app/routes.js +++ b/app/routes.js @@ -1,34 +1,34 @@ // @flow -import * as React from 'react'; -import { Switch, Route, Redirect } from 'react-router-dom'; -import Home from 'scenes/Home'; -import Dashboard from 'scenes/Dashboard'; -import Starred from 'scenes/Starred'; -import Drafts from 'scenes/Drafts'; -import Archive from 'scenes/Archive'; -import Trash from 'scenes/Trash'; -import Collection from 'scenes/Collection'; -import KeyedDocument from 'scenes/Document/KeyedDocument'; -import DocumentNew from 'scenes/DocumentNew'; -import Search from 'scenes/Search'; -import Settings from 'scenes/Settings'; -import Details from 'scenes/Settings/Details'; -import Notifications from 'scenes/Settings/Notifications'; -import Security from 'scenes/Settings/Security'; -import People from 'scenes/Settings/People'; -import Groups from 'scenes/Settings/Groups'; -import Slack from 'scenes/Settings/Slack'; -import Zapier from 'scenes/Settings/Zapier'; -import Shares from 'scenes/Settings/Shares'; -import Tokens from 'scenes/Settings/Tokens'; -import Export from 'scenes/Settings/Export'; -import Events from 'scenes/Settings/Events'; -import Error404 from 'scenes/Error404'; +import * as React from "react"; +import { Switch, Route, Redirect } from "react-router-dom"; +import Home from "scenes/Home"; +import Dashboard from "scenes/Dashboard"; +import Starred from "scenes/Starred"; +import Drafts from "scenes/Drafts"; +import Archive from "scenes/Archive"; +import Trash from "scenes/Trash"; +import Collection from "scenes/Collection"; +import KeyedDocument from "scenes/Document/KeyedDocument"; +import DocumentNew from "scenes/DocumentNew"; +import Search from "scenes/Search"; +import Settings from "scenes/Settings"; +import Details from "scenes/Settings/Details"; +import Notifications from "scenes/Settings/Notifications"; +import Security from "scenes/Settings/Security"; +import People from "scenes/Settings/People"; +import Groups from "scenes/Settings/Groups"; +import Slack from "scenes/Settings/Slack"; +import Zapier from "scenes/Settings/Zapier"; +import Shares from "scenes/Settings/Shares"; +import Tokens from "scenes/Settings/Tokens"; +import Export from "scenes/Settings/Export"; +import Events from "scenes/Settings/Events"; +import Error404 from "scenes/Error404"; -import Layout from 'components/Layout'; -import SocketProvider from 'components/SocketProvider'; -import Authenticated from 'components/Authenticated'; -import { matchDocumentSlug as slug } from 'utils/routeHelpers'; +import Layout from "components/Layout"; +import SocketProvider from "components/SocketProvider"; +import Authenticated from "components/Authenticated"; +import { matchDocumentSlug as slug } from "utils/routeHelpers"; const NotFound = () => ; const RedirectDocument = ({ match }: { match: Object }) => ( diff --git a/app/scenes/Archive.js b/app/scenes/Archive.js index 68053bcd..92e81f68 100644 --- a/app/scenes/Archive.js +++ b/app/scenes/Archive.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; -import CenteredContent from 'components/CenteredContent'; -import Empty from 'components/Empty'; -import PageTitle from 'components/PageTitle'; -import Heading from 'components/Heading'; -import PaginatedDocumentList from 'components/PaginatedDocumentList'; -import Subheading from 'components/Subheading'; -import DocumentsStore from 'stores/DocumentsStore'; +import CenteredContent from "components/CenteredContent"; +import Empty from "components/Empty"; +import PageTitle from "components/PageTitle"; +import Heading from "components/Heading"; +import PaginatedDocumentList from "components/PaginatedDocumentList"; +import Subheading from "components/Subheading"; +import DocumentsStore from "stores/DocumentsStore"; type Props = { documents: DocumentsStore, @@ -35,4 +35,4 @@ class Archive extends React.Component { } } -export default inject('documents')(Archive); +export default inject("documents")(Archive); diff --git a/app/scenes/Collection.js b/app/scenes/Collection.js index 1704e6b7..0b6b8445 100644 --- a/app/scenes/Collection.js +++ b/app/scenes/Collection.js @@ -1,42 +1,42 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { Redirect, Link, Switch, Route } from 'react-router-dom'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import { Redirect, Link, Switch, Route } from "react-router-dom"; -import styled, { withTheme } from 'styled-components'; -import { NewDocumentIcon, PlusIcon, PinIcon } from 'outline-icons'; -import RichMarkdownEditor from 'rich-markdown-editor'; +import styled, { withTheme } from "styled-components"; +import { NewDocumentIcon, PlusIcon, PinIcon } from "outline-icons"; +import RichMarkdownEditor from "rich-markdown-editor"; -import { newDocumentUrl, collectionUrl } from 'utils/routeHelpers'; -import CollectionsStore from 'stores/CollectionsStore'; -import DocumentsStore from 'stores/DocumentsStore'; -import PoliciesStore from 'stores/PoliciesStore'; -import UiStore from 'stores/UiStore'; -import Collection from 'models/Collection'; +import { newDocumentUrl, collectionUrl } from "utils/routeHelpers"; +import CollectionsStore from "stores/CollectionsStore"; +import DocumentsStore from "stores/DocumentsStore"; +import PoliciesStore from "stores/PoliciesStore"; +import UiStore from "stores/UiStore"; +import Collection from "models/Collection"; -import Search from 'scenes/Search'; -import CollectionEdit from 'scenes/CollectionEdit'; -import CollectionMenu from 'menus/CollectionMenu'; -import Actions, { Action, Separator } from 'components/Actions'; -import Heading from 'components/Heading'; -import Tooltip from 'components/Tooltip'; -import CenteredContent from 'components/CenteredContent'; -import { ListPlaceholder } from 'components/LoadingPlaceholder'; -import InputSearch from 'components/InputSearch'; -import Mask from 'components/Mask'; -import Button from 'components/Button'; -import HelpText from 'components/HelpText'; -import DocumentList from 'components/DocumentList'; -import Subheading from 'components/Subheading'; -import PageTitle from 'components/PageTitle'; -import Flex from 'shared/components/Flex'; -import Modal from 'components/Modal'; -import CollectionMembers from 'scenes/CollectionMembers'; -import Tabs from 'components/Tabs'; -import Tab from 'components/Tab'; -import PaginatedDocumentList from 'components/PaginatedDocumentList'; -import CollectionIcon from 'components/CollectionIcon'; +import Search from "scenes/Search"; +import CollectionEdit from "scenes/CollectionEdit"; +import CollectionMenu from "menus/CollectionMenu"; +import Actions, { Action, Separator } from "components/Actions"; +import Heading from "components/Heading"; +import Tooltip from "components/Tooltip"; +import CenteredContent from "components/CenteredContent"; +import { ListPlaceholder } from "components/LoadingPlaceholder"; +import InputSearch from "components/InputSearch"; +import Mask from "components/Mask"; +import Button from "components/Button"; +import HelpText from "components/HelpText"; +import DocumentList from "components/DocumentList"; +import Subheading from "components/Subheading"; +import PageTitle from "components/PageTitle"; +import Flex from "shared/components/Flex"; +import Modal from "components/Modal"; +import CollectionMembers from "scenes/CollectionMembers"; +import Tabs from "components/Tabs"; +import Tab from "components/Tab"; +import PaginatedDocumentList from "components/PaginatedDocumentList"; +import CollectionIcon from "components/CollectionIcon"; type Props = { ui: UiStore, @@ -205,7 +205,7 @@ class CollectionScene extends React.Component { ) : ( - {' '} + {" "} {collection.name} @@ -232,18 +232,18 @@ class CollectionScene extends React.Component { Recently updated - + Recently published - + Least recently updated - + A–Z - + { showPin /> - + { showPin /> - + {
    - Are you sure about that? Deleting the{' '} + Are you sure about that? Deleting the{" "} {collection.name} collection is permanent and will also delete all of the documents within it, so be extra careful.
    @@ -58,4 +58,4 @@ class CollectionDelete extends React.Component { } } -export default inject('collections', 'ui')(withRouter(CollectionDelete)); +export default inject("collections", "ui")(withRouter(CollectionDelete)); diff --git a/app/scenes/CollectionEdit.js b/app/scenes/CollectionEdit.js index 881da8f5..23068b5a 100644 --- a/app/scenes/CollectionEdit.js +++ b/app/scenes/CollectionEdit.js @@ -1,16 +1,16 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import Input from 'components/Input'; -import InputRich from 'components/InputRich'; -import Button from 'components/Button'; -import Switch from 'components/Switch'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import IconPicker from 'components/IconPicker'; -import Collection from 'models/Collection'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import Input from "components/Input"; +import InputRich from "components/InputRich"; +import Button from "components/Button"; +import Switch from "components/Switch"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import IconPicker from "components/IconPicker"; +import Collection from "models/Collection"; +import UiStore from "stores/UiStore"; type Props = { collection: Collection, @@ -21,9 +21,9 @@ type Props = { @observer class CollectionEdit extends React.Component { @observable name: string; - @observable description: string = ''; - @observable icon: string = ''; - @observable color: string = '#4E5C6E'; + @observable description: string = ""; + @observable icon: string = ""; + @observable color: string = "#4E5C6E"; @observable isSaving: boolean; @observable private: boolean = false; @@ -48,7 +48,7 @@ class CollectionEdit extends React.Component { private: this.private, }); this.props.onSubmit(); - this.props.ui.showToast('The collection was updated'); + this.props.ui.showToast("The collection was updated"); } catch (err) { this.props.ui.showToast(err.message); } finally { @@ -102,7 +102,7 @@ class CollectionEdit extends React.Component { id={this.props.collection.id} label="Description" onChange={this.handleDescriptionChange} - defaultValue={this.description || ''} + defaultValue={this.description || ""} placeholder="More details about this collection…" minHeight={68} maxHeight={200} @@ -120,7 +120,7 @@ class CollectionEdit extends React.Component { type="submit" disabled={this.isSaving || !this.props.collection.name} > - {this.isSaving ? 'Saving…' : 'Save'} + {this.isSaving ? "Saving…" : "Save"} @@ -128,4 +128,4 @@ class CollectionEdit extends React.Component { } } -export default inject('ui')(CollectionEdit); +export default inject("ui")(CollectionEdit); diff --git a/app/scenes/CollectionExport.js b/app/scenes/CollectionExport.js index c539cb7d..1ef04c32 100644 --- a/app/scenes/CollectionExport.js +++ b/app/scenes/CollectionExport.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import Button from 'components/Button'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import Collection from 'models/Collection'; -import AuthStore from 'stores/AuthStore'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import Button from "components/Button"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import Collection from "models/Collection"; +import AuthStore from "stores/AuthStore"; +import UiStore from "stores/UiStore"; type Props = { collection: Collection, @@ -42,7 +42,7 @@ class CollectionExport extends React.Component { with files in Markdown format. @@ -50,4 +50,4 @@ class CollectionExport extends React.Component { } } -export default inject('ui', 'auth')(CollectionExport); +export default inject("ui", "auth")(CollectionExport); diff --git a/app/scenes/CollectionMembers/AddGroupsToCollection.js b/app/scenes/CollectionMembers/AddGroupsToCollection.js index b805bad2..30d55811 100644 --- a/app/scenes/CollectionMembers/AddGroupsToCollection.js +++ b/app/scenes/CollectionMembers/AddGroupsToCollection.js @@ -1,23 +1,23 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { inject, observer } from 'mobx-react'; -import { observable } from 'mobx'; -import { debounce } from 'lodash'; -import Button from 'components/Button'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import Input from 'components/Input'; -import Modal from 'components/Modal'; -import Empty from 'components/Empty'; -import PaginatedList from 'components/PaginatedList'; -import GroupNew from 'scenes/GroupNew'; -import Collection from 'models/Collection'; -import UiStore from 'stores/UiStore'; -import AuthStore from 'stores/AuthStore'; -import GroupsStore from 'stores/GroupsStore'; -import CollectionGroupMembershipsStore from 'stores/CollectionGroupMembershipsStore'; -import GroupListItem from 'components/GroupListItem'; +import * as React from "react"; +import styled from "styled-components"; +import { inject, observer } from "mobx-react"; +import { observable } from "mobx"; +import { debounce } from "lodash"; +import Button from "components/Button"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import Input from "components/Input"; +import Modal from "components/Modal"; +import Empty from "components/Empty"; +import PaginatedList from "components/PaginatedList"; +import GroupNew from "scenes/GroupNew"; +import Collection from "models/Collection"; +import UiStore from "stores/UiStore"; +import AuthStore from "stores/AuthStore"; +import GroupsStore from "stores/GroupsStore"; +import CollectionGroupMembershipsStore from "stores/CollectionGroupMembershipsStore"; +import GroupListItem from "components/GroupListItem"; type Props = { ui: UiStore, @@ -31,7 +31,7 @@ type Props = { @observer class AddGroupsToCollection extends React.Component { @observable newGroupModalOpen: boolean = false; - @observable query: string = ''; + @observable query: string = ""; handleNewGroupModalOpen = () => { this.newGroupModalOpen = true; @@ -57,11 +57,11 @@ class AddGroupsToCollection extends React.Component { this.props.collectionGroupMemberships.create({ collectionId: this.props.collection.id, groupId: group.id, - permission: 'read_write', + permission: "read_write", }); this.props.ui.showToast(`${group.name} was added to the collection`); } catch (err) { - this.props.ui.showToast('Could not add user'); + this.props.ui.showToast("Could not add user"); console.error(err); } }; @@ -74,7 +74,7 @@ class AddGroupsToCollection extends React.Component { return ( - Can’t find the group you’re looking for?{' '} + Can’t find the group you’re looking for?{" "} Create a group . @@ -130,6 +130,6 @@ const ButtonWrap = styled.div` margin-left: 6px; `; -export default inject('auth', 'groups', 'collectionGroupMemberships', 'ui')( +export default inject("auth", "groups", "collectionGroupMemberships", "ui")( AddGroupsToCollection ); diff --git a/app/scenes/CollectionMembers/AddPeopleToCollection.js b/app/scenes/CollectionMembers/AddPeopleToCollection.js index 00b69bf3..c53499fd 100644 --- a/app/scenes/CollectionMembers/AddPeopleToCollection.js +++ b/app/scenes/CollectionMembers/AddPeopleToCollection.js @@ -1,21 +1,21 @@ // @flow -import * as React from 'react'; -import { inject, observer } from 'mobx-react'; -import { observable } from 'mobx'; -import { debounce } from 'lodash'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import Input from 'components/Input'; -import Modal from 'components/Modal'; -import Empty from 'components/Empty'; -import PaginatedList from 'components/PaginatedList'; -import Invite from 'scenes/Invite'; -import Collection from 'models/Collection'; -import UiStore from 'stores/UiStore'; -import AuthStore from 'stores/AuthStore'; -import UsersStore from 'stores/UsersStore'; -import MembershipsStore from 'stores/MembershipsStore'; -import MemberListItem from './components/MemberListItem'; +import * as React from "react"; +import { inject, observer } from "mobx-react"; +import { observable } from "mobx"; +import { debounce } from "lodash"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import Input from "components/Input"; +import Modal from "components/Modal"; +import Empty from "components/Empty"; +import PaginatedList from "components/PaginatedList"; +import Invite from "scenes/Invite"; +import Collection from "models/Collection"; +import UiStore from "stores/UiStore"; +import AuthStore from "stores/AuthStore"; +import UsersStore from "stores/UsersStore"; +import MembershipsStore from "stores/MembershipsStore"; +import MemberListItem from "./components/MemberListItem"; type Props = { ui: UiStore, @@ -29,7 +29,7 @@ type Props = { @observer class AddPeopleToCollection extends React.Component { @observable inviteModalOpen: boolean = false; - @observable query: string = ''; + @observable query: string = ""; handleInviteModalOpen = () => { this.inviteModalOpen = true; @@ -55,11 +55,11 @@ class AddPeopleToCollection extends React.Component { this.props.memberships.create({ collectionId: this.props.collection.id, userId: user.id, - permission: 'read_write', + permission: "read_write", }); this.props.ui.showToast(`${user.name} was added to the collection`); } catch (err) { - this.props.ui.showToast('Could not add user'); + this.props.ui.showToast("Could not add user"); } }; @@ -71,7 +71,7 @@ class AddPeopleToCollection extends React.Component { return ( - Need to add someone who’s not yet on the team yet?{' '} + Need to add someone who’s not yet on the team yet?{" "} Invite people to {team.name} . @@ -118,6 +118,6 @@ class AddPeopleToCollection extends React.Component { } } -export default inject('auth', 'users', 'memberships', 'ui')( +export default inject("auth", "users", "memberships", "ui")( AddPeopleToCollection ); diff --git a/app/scenes/CollectionMembers/CollectionMembers.js b/app/scenes/CollectionMembers/CollectionMembers.js index 2e56703f..2ca673a2 100644 --- a/app/scenes/CollectionMembers/CollectionMembers.js +++ b/app/scenes/CollectionMembers/CollectionMembers.js @@ -1,27 +1,27 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import styled from 'styled-components'; -import { inject, observer } from 'mobx-react'; -import { PlusIcon } from 'outline-icons'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import Subheading from 'components/Subheading'; -import Button from 'components/Button'; -import Empty from 'components/Empty'; -import PaginatedList from 'components/PaginatedList'; -import Modal from 'components/Modal'; -import CollectionGroupMemberListItem from './components/CollectionGroupMemberListItem'; -import Collection from 'models/Collection'; -import UiStore from 'stores/UiStore'; -import AuthStore from 'stores/AuthStore'; -import MembershipsStore from 'stores/MembershipsStore'; -import CollectionGroupMembershipsStore from 'stores/CollectionGroupMembershipsStore'; -import UsersStore from 'stores/UsersStore'; -import MemberListItem from './components/MemberListItem'; -import AddPeopleToCollection from './AddPeopleToCollection'; -import AddGroupsToCollection from './AddGroupsToCollection'; -import GroupsStore from 'stores/GroupsStore'; +import * as React from "react"; +import { observable } from "mobx"; +import styled from "styled-components"; +import { inject, observer } from "mobx-react"; +import { PlusIcon } from "outline-icons"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import Subheading from "components/Subheading"; +import Button from "components/Button"; +import Empty from "components/Empty"; +import PaginatedList from "components/PaginatedList"; +import Modal from "components/Modal"; +import CollectionGroupMemberListItem from "./components/CollectionGroupMemberListItem"; +import Collection from "models/Collection"; +import UiStore from "stores/UiStore"; +import AuthStore from "stores/AuthStore"; +import MembershipsStore from "stores/MembershipsStore"; +import CollectionGroupMembershipsStore from "stores/CollectionGroupMembershipsStore"; +import UsersStore from "stores/UsersStore"; +import MemberListItem from "./components/MemberListItem"; +import AddPeopleToCollection from "./AddPeopleToCollection"; +import AddGroupsToCollection from "./AddGroupsToCollection"; +import GroupsStore from "stores/GroupsStore"; type Props = { ui: UiStore, @@ -63,7 +63,7 @@ class CollectionMembers extends React.Component { }); this.props.ui.showToast(`${user.name} was removed from the collection`); } catch (err) { - this.props.ui.showToast('Could not remove user'); + this.props.ui.showToast("Could not remove user"); } }; @@ -76,7 +76,7 @@ class CollectionMembers extends React.Component { }); this.props.ui.showToast(`${user.name} permissions were updated`); } catch (err) { - this.props.ui.showToast('Could not update user'); + this.props.ui.showToast("Could not update user"); } }; @@ -88,7 +88,7 @@ class CollectionMembers extends React.Component { }); this.props.ui.showToast(`${group.name} was removed from the collection`); } catch (err) { - this.props.ui.showToast('Could not remove group'); + this.props.ui.showToast("Could not remove group"); } }; @@ -101,7 +101,7 @@ class CollectionMembers extends React.Component { }); this.props.ui.showToast(`${group.name} permissions were updated`); } catch (err) { - this.props.ui.showToast('Could not update user'); + this.props.ui.showToast("Could not update user"); } }; @@ -120,7 +120,7 @@ class CollectionMembers extends React.Component { const key = memberships.orderedData .map(m => m.permission) .concat(collection.private) - .join('-'); + .join("-"); return ( @@ -128,9 +128,9 @@ class CollectionMembers extends React.Component { Choose which groups and team members have access to view and edit - documents in the private {collection.name}{' '} + documents in the private {collection.name}{" "} collection. You can make this collection visible to the entire - team by{' '} + team by{" "} changing its visibility . @@ -150,7 +150,7 @@ class CollectionMembers extends React.Component { The {collection.name} collection is accessible by everyone on the team. If you want to limit who can view the - collection,{' '} + collection,{" "} make it private . @@ -250,10 +250,10 @@ const GroupsWrap = styled.div` `; export default inject( - 'auth', - 'users', - 'memberships', - 'collectionGroupMemberships', - 'groups', - 'ui' + "auth", + "users", + "memberships", + "collectionGroupMemberships", + "groups", + "ui" )(CollectionMembers); diff --git a/app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js b/app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js index d57e5d0d..8a111bb5 100644 --- a/app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js +++ b/app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js @@ -1,15 +1,15 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import InputSelect from 'components/InputSelect'; -import GroupListItem from 'components/GroupListItem'; -import Group from 'models/Group'; -import CollectionGroupMembership from 'models/CollectionGroupMembership'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; +import * as React from "react"; +import styled from "styled-components"; +import InputSelect from "components/InputSelect"; +import GroupListItem from "components/GroupListItem"; +import Group from "models/Group"; +import CollectionGroupMembership from "models/CollectionGroupMembership"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; const PERMISSIONS = [ - { label: 'Read only', value: 'read' }, - { label: 'Read & Edit', value: 'read_write' }, + { label: "Read only", value: "read" }, + { label: "Read & Edit", value: "read_write" }, ]; type Props = { group: Group, diff --git a/app/scenes/CollectionMembers/components/MemberListItem.js b/app/scenes/CollectionMembers/components/MemberListItem.js index 08f71d1a..26815839 100644 --- a/app/scenes/CollectionMembers/components/MemberListItem.js +++ b/app/scenes/CollectionMembers/components/MemberListItem.js @@ -1,20 +1,20 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Avatar from 'components/Avatar'; -import Flex from 'shared/components/Flex'; -import Time from 'shared/components/Time'; -import Badge from 'components/Badge'; -import Button from 'components/Button'; -import InputSelect from 'components/InputSelect'; -import ListItem from 'components/List/Item'; -import User from 'models/User'; -import Membership from 'models/Membership'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; +import * as React from "react"; +import styled from "styled-components"; +import Avatar from "components/Avatar"; +import Flex from "shared/components/Flex"; +import Time from "shared/components/Time"; +import Badge from "components/Badge"; +import Button from "components/Button"; +import InputSelect from "components/InputSelect"; +import ListItem from "components/List/Item"; +import User from "models/User"; +import Membership from "models/Membership"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; const PERMISSIONS = [ - { label: 'Read only', value: 'read' }, - { label: 'Read & Edit', value: 'read_write' }, + { label: "Read only", value: "read" }, + { label: "Read & Edit", value: "read_write" }, ]; type Props = { user: User, @@ -43,7 +43,7 @@ const MemberListItem = ({ Active ) : ( - 'Never signed in' + "Never signed in" )} {!user.lastActiveAt && Invited} {user.isAdmin && Admin} diff --git a/app/scenes/CollectionMembers/components/UserListItem.js b/app/scenes/CollectionMembers/components/UserListItem.js index 60fa848d..19663088 100644 --- a/app/scenes/CollectionMembers/components/UserListItem.js +++ b/app/scenes/CollectionMembers/components/UserListItem.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import { PlusIcon } from 'outline-icons'; -import Time from 'shared/components/Time'; -import Avatar from 'components/Avatar'; -import Button from 'components/Button'; -import Badge from 'components/Badge'; -import ListItem from 'components/List/Item'; -import User from 'models/User'; +import * as React from "react"; +import { PlusIcon } from "outline-icons"; +import Time from "shared/components/Time"; +import Avatar from "components/Avatar"; +import Button from "components/Button"; +import Badge from "components/Badge"; +import ListItem from "components/List/Item"; +import User from "models/User"; type Props = { user: User, @@ -26,7 +26,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => { Active
    ) : ( - 'Never signed in' + "Never signed in" )} {!user.lastActiveAt && Invited} {user.isAdmin && Admin} diff --git a/app/scenes/CollectionMembers/index.js b/app/scenes/CollectionMembers/index.js index d58df3c7..e1607f75 100644 --- a/app/scenes/CollectionMembers/index.js +++ b/app/scenes/CollectionMembers/index.js @@ -1,3 +1,3 @@ // @flow -import CollectionMembers from './CollectionMembers'; +import CollectionMembers from "./CollectionMembers"; export default CollectionMembers; diff --git a/app/scenes/CollectionNew.js b/app/scenes/CollectionNew.js index 9573d0ea..ed628595 100644 --- a/app/scenes/CollectionNew.js +++ b/app/scenes/CollectionNew.js @@ -1,20 +1,20 @@ // @flow -import * as React from 'react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import { intersection } from 'lodash'; -import Button from 'components/Button'; -import Switch from 'components/Switch'; -import Input from 'components/Input'; -import InputRich from 'components/InputRich'; -import IconPicker, { icons } from 'components/IconPicker'; -import HelpText from 'components/HelpText'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import { intersection } from "lodash"; +import Button from "components/Button"; +import Switch from "components/Switch"; +import Input from "components/Input"; +import InputRich from "components/InputRich"; +import IconPicker, { icons } from "components/IconPicker"; +import HelpText from "components/HelpText"; +import Flex from "shared/components/Flex"; -import Collection from 'models/Collection'; -import CollectionsStore from 'stores/CollectionsStore'; -import UiStore from 'stores/UiStore'; +import Collection from "models/Collection"; +import CollectionsStore from "stores/CollectionsStore"; +import UiStore from "stores/UiStore"; type Props = { history: RouterHistory, @@ -25,10 +25,10 @@ type Props = { @observer class CollectionNew extends React.Component { - @observable name: string = ''; - @observable description: string = ''; - @observable icon: string = ''; - @observable color: string = '#4E5C6E'; + @observable name: string = ""; + @observable description: string = ""; + @observable icon: string = ""; + @observable color: string = "#4E5C6E"; @observable private: boolean = false; @observable isSaving: boolean; hasOpenedIconPicker: boolean = false; @@ -67,8 +67,8 @@ class CollectionNew extends React.Component { const keys = Object.keys(icons); for (const key of keys) { const icon = icons[key]; - const keywords = icon.keywords.split(' '); - const namewords = this.name.toLowerCase().split(' '); + const keywords = icon.keywords.split(" "); + const namewords = this.name.toLowerCase().split(" "); const matches = intersection(namewords, keywords); if (matches.length > 0) { @@ -77,7 +77,7 @@ class CollectionNew extends React.Component { } } - this.icon = 'collection'; + this.icon = "collection"; } }; @@ -127,7 +127,7 @@ class CollectionNew extends React.Component { { ); } } -export default inject('collections', 'ui')(withRouter(CollectionNew)); +export default inject("collections", "ui")(withRouter(CollectionNew)); diff --git a/app/scenes/Dashboard.js b/app/scenes/Dashboard.js index 556a991c..c798b983 100644 --- a/app/scenes/Dashboard.js +++ b/app/scenes/Dashboard.js @@ -1,18 +1,18 @@ // @flow -import * as React from 'react'; -import { Switch, Route } from 'react-router-dom'; -import { observer, inject } from 'mobx-react'; +import * as React from "react"; +import { Switch, Route } from "react-router-dom"; +import { observer, inject } from "mobx-react"; -import DocumentsStore from 'stores/DocumentsStore'; -import AuthStore from 'stores/AuthStore'; -import NewDocumentMenu from 'menus/NewDocumentMenu'; -import Actions, { Action } from 'components/Actions'; -import InputSearch from 'components/InputSearch'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import Tabs from 'components/Tabs'; -import Tab from 'components/Tab'; -import PaginatedDocumentList from '../components/PaginatedDocumentList'; +import DocumentsStore from "stores/DocumentsStore"; +import AuthStore from "stores/AuthStore"; +import NewDocumentMenu from "menus/NewDocumentMenu"; +import Actions, { Action } from "components/Actions"; +import InputSearch from "components/InputSearch"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import Tabs from "components/Tabs"; +import Tab from "components/Tab"; +import PaginatedDocumentList from "../components/PaginatedDocumentList"; type Props = { documents: DocumentsStore, @@ -78,4 +78,4 @@ class Dashboard extends React.Component { } } -export default inject('documents', 'auth')(Dashboard); +export default inject("documents", "auth")(Dashboard); diff --git a/app/scenes/Document/KeyedDocument.js b/app/scenes/Document/KeyedDocument.js index edb2d9f8..bdcf554a 100644 --- a/app/scenes/Document/KeyedDocument.js +++ b/app/scenes/Document/KeyedDocument.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import { inject } from 'mobx-react'; -import DataLoader from './components/DataLoader'; +import * as React from "react"; +import { inject } from "mobx-react"; +import DataLoader from "./components/DataLoader"; class KeyedDocument extends React.Component<*> { componentWillUnmount() { @@ -15,11 +15,11 @@ class KeyedDocument extends React.Component<*> { // we only want to force a re-mount of the document component when the // document changes, not when the title does so only this portion is used // for the key. - const urlParts = documentSlug ? documentSlug.split('-') : []; + const urlParts = documentSlug ? documentSlug.split("-") : []; const urlId = urlParts.length ? urlParts[urlParts.length - 1] : undefined; - return ; + return ; } } -export default inject('ui')(KeyedDocument); +export default inject("ui")(KeyedDocument); diff --git a/app/scenes/Document/components/Container.js b/app/scenes/Document/components/Container.js index b60ee3fc..a248aa0d 100644 --- a/app/scenes/Document/components/Container.js +++ b/app/scenes/Document/components/Container.js @@ -1,10 +1,10 @@ // @flow -import styled from 'styled-components'; -import Flex from 'shared/components/Flex'; +import styled from "styled-components"; +import Flex from "shared/components/Flex"; const Container = styled(Flex)` position: relative; - margin-top: ${props => (props.isShare ? '50px' : '0')}; + margin-top: ${props => (props.isShare ? "50px" : "0")}; `; export default Container; diff --git a/app/scenes/Document/components/Contents.js b/app/scenes/Document/components/Contents.js index 03de590b..55df7e7c 100644 --- a/app/scenes/Document/components/Contents.js +++ b/app/scenes/Document/components/Contents.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import { darken } from 'polished'; -import breakpoint from 'styled-components-breakpoint'; -import useWindowScrollPosition from '@rehooks/window-scroll-position'; -import HelpText from 'components/HelpText'; -import styled from 'styled-components'; +import * as React from "react"; +import { darken } from "polished"; +import breakpoint from "styled-components-breakpoint"; +import useWindowScrollPosition from "@rehooks/window-scroll-position"; +import HelpText from "components/HelpText"; +import styled from "styled-components"; const HEADING_OFFSET = 20; @@ -72,7 +72,7 @@ export default function Contents({ headings }: Props) { ); } -const Wrapper = styled('div')` +const Wrapper = styled("div")` display: none; position: sticky; top: 80px; @@ -82,16 +82,16 @@ const Wrapper = styled('div')` margin-right: 2em; min-height: 40px; - ${breakpoint('desktopLarge')` + ${breakpoint("desktopLarge")` margin-left: -16em; `}; - ${breakpoint('tablet')` + ${breakpoint("tablet")` display: block; `}; `; -const Heading = styled('h3')` +const Heading = styled("h3")` font-size: 11px; font-weight: 600; text-transform: uppercase; @@ -107,16 +107,16 @@ const Empty = styled(HelpText)` font-size: 14px; `; -const ListItem = styled('li')` +const ListItem = styled("li")` margin-left: ${props => (props.level - 1) * 10}px; margin-bottom: 8px; padding-right: 2em; line-height: 1.3; border-right: 3px solid - ${props => (props.active ? props.theme.textSecondary : 'transparent')}; + ${props => (props.active ? props.theme.textSecondary : "transparent")}; `; -const Link = styled('a')` +const Link = styled("a")` color: ${props => props.theme.text}; font-size: 14px; @@ -125,7 +125,7 @@ const Link = styled('a')` } `; -const List = styled('ol')` +const List = styled("ol")` min-width: 14em; width: 14em; padding: 0; diff --git a/app/scenes/Document/components/DataLoader.js b/app/scenes/Document/components/DataLoader.js index f9025a60..5cb93ce4 100644 --- a/app/scenes/Document/components/DataLoader.js +++ b/app/scenes/Document/components/DataLoader.js @@ -1,24 +1,24 @@ // @flow -import * as React from 'react'; -import invariant from 'invariant'; -import { withRouter } from 'react-router-dom'; -import type { Location, RouterHistory } from 'react-router-dom'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { matchDocumentEdit, updateDocumentUrl } from 'utils/routeHelpers'; -import DocumentComponent from './Document'; -import Revision from 'models/Revision'; -import Document from 'models/Document'; -import SocketPresence from './SocketPresence'; -import Loading from './Loading'; -import HideSidebar from './HideSidebar'; -import Error404 from 'scenes/Error404'; -import ErrorOffline from 'scenes/ErrorOffline'; -import DocumentsStore from 'stores/DocumentsStore'; -import PoliciesStore from 'stores/PoliciesStore'; -import RevisionsStore from 'stores/RevisionsStore'; -import UiStore from 'stores/UiStore'; -import { OfflineError } from 'utils/errors'; +import * as React from "react"; +import invariant from "invariant"; +import { withRouter } from "react-router-dom"; +import type { Location, RouterHistory } from "react-router-dom"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers"; +import DocumentComponent from "./Document"; +import Revision from "models/Revision"; +import Document from "models/Document"; +import SocketPresence from "./SocketPresence"; +import Loading from "./Loading"; +import HideSidebar from "./HideSidebar"; +import Error404 from "scenes/Error404"; +import ErrorOffline from "scenes/ErrorOffline"; +import DocumentsStore from "stores/DocumentsStore"; +import PoliciesStore from "stores/PoliciesStore"; +import RevisionsStore from "stores/RevisionsStore"; +import UiStore from "stores/UiStore"; +import { OfflineError } from "utils/errors"; type Props = {| match: Object, @@ -84,13 +84,13 @@ class DataLoader extends React.Component { onCreateLink = async (title: string) => { const document = this.document; - invariant(document, 'document must be loaded to create link'); + invariant(document, "document must be loaded to create link"); const newDocument = await this.props.documents.create({ collectionId: document.collectionId, parentDocumentId: document.parentDocumentId, title, - text: '', + text: "", }); return newDocument.url; @@ -166,7 +166,7 @@ class DataLoader extends React.Component { } const abilities = policies.abilities(document.id); - const key = this.isEditing ? 'editing' : 'read-only'; + const key = this.isEditing ? "editing" : "read-only"; return ( @@ -187,5 +187,5 @@ class DataLoader extends React.Component { } export default withRouter( - inject('ui', 'auth', 'documents', 'revisions', 'policies')(DataLoader) + inject("ui", "auth", "documents", "revisions", "policies")(DataLoader) ); diff --git a/app/scenes/Document/components/Document.js b/app/scenes/Document/components/Document.js index 84828ac2..7fc5509c 100644 --- a/app/scenes/Document/components/Document.js +++ b/app/scenes/Document/components/Document.js @@ -1,42 +1,42 @@ // @flow -import * as React from 'react'; -import { debounce } from 'lodash'; -import styled, { withTheme } from 'styled-components'; -import breakpoint from 'styled-components-breakpoint'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { Prompt, Route, withRouter } from 'react-router-dom'; -import type { Location, RouterHistory } from 'react-router-dom'; -import keydown from 'react-keydown'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { debounce } from "lodash"; +import styled, { withTheme } from "styled-components"; +import breakpoint from "styled-components-breakpoint"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import { Prompt, Route, withRouter } from "react-router-dom"; +import type { Location, RouterHistory } from "react-router-dom"; +import keydown from "react-keydown"; +import Flex from "shared/components/Flex"; import { collectionUrl, documentMoveUrl, documentHistoryUrl, documentEditUrl, documentUrl, -} from 'utils/routeHelpers'; -import { emojiToUrl } from 'utils/emoji'; +} from "utils/routeHelpers"; +import { emojiToUrl } from "utils/emoji"; -import Header from './Header'; -import DocumentMove from './DocumentMove'; -import KeyboardShortcutsButton from './KeyboardShortcutsButton'; -import References from './References'; -import Loading from './Loading'; -import Container from './Container'; -import Contents from './Contents'; -import MarkAsViewed from './MarkAsViewed'; -import ErrorBoundary from 'components/ErrorBoundary'; -import LoadingIndicator from 'components/LoadingIndicator'; -import PageTitle from 'components/PageTitle'; -import Branding from 'shared/components/Branding'; -import Notice from 'shared/components/Notice'; -import Time from 'shared/components/Time'; +import Header from "./Header"; +import DocumentMove from "./DocumentMove"; +import KeyboardShortcutsButton from "./KeyboardShortcutsButton"; +import References from "./References"; +import Loading from "./Loading"; +import Container from "./Container"; +import Contents from "./Contents"; +import MarkAsViewed from "./MarkAsViewed"; +import ErrorBoundary from "components/ErrorBoundary"; +import LoadingIndicator from "components/LoadingIndicator"; +import PageTitle from "components/PageTitle"; +import Branding from "shared/components/Branding"; +import Notice from "shared/components/Notice"; +import Time from "shared/components/Time"; -import UiStore from 'stores/UiStore'; -import AuthStore from 'stores/AuthStore'; -import Document from 'models/Document'; -import Revision from 'models/Revision'; +import UiStore from "stores/UiStore"; +import AuthStore from "stores/AuthStore"; +import Document from "models/Document"; +import Revision from "models/Revision"; let EditorImport; const AUTOSAVE_DELAY = 3000; @@ -104,7 +104,7 @@ class DocumentScene extends React.Component { { timeout: 30 * 1000, action: { - text: 'Reload', + text: "Reload", onClick: () => { window.location.href = documentUrl(document); }, @@ -123,7 +123,7 @@ class DocumentScene extends React.Component { window.document.body.style.background = this.props.theme.background; } - @keydown('m') + @keydown("m") goToMove(ev) { if (!this.props.readOnly) return; @@ -135,7 +135,7 @@ class DocumentScene extends React.Component { } } - @keydown('e') + @keydown("e") goToEdit(ev) { if (!this.props.readOnly) return; @@ -147,7 +147,7 @@ class DocumentScene extends React.Component { } } - @keydown('esc') + @keydown("esc") goBack(ev) { if (this.props.readOnly) return; @@ -155,7 +155,7 @@ class DocumentScene extends React.Component { this.props.history.goBack(); } - @keydown('h') + @keydown("h") goToHistory(ev) { if (!this.props.readOnly) return; @@ -169,7 +169,7 @@ class DocumentScene extends React.Component { } } - @keydown('meta+shift+p') + @keydown("meta+shift+p") onPublish(ev) { ev.preventDefault(); const { document } = this.props; @@ -177,7 +177,7 @@ class DocumentScene extends React.Component { this.onSave({ publish: true, done: true }); } - @keydown('meta+ctrl+h') + @keydown("meta+ctrl+h") onToggleTableOfContents(ev) { if (!this.props.readOnly) return; @@ -195,7 +195,7 @@ class DocumentScene extends React.Component { if (this.editorComponent) return; try { - const EditorImport = await import('./Editor'); + const EditorImport = await import("./Editor"); this.editorComponent = EditorImport.default; } catch (err) { console.error(err); @@ -227,7 +227,7 @@ class DocumentScene extends React.Component { const title = this.title; // prevent save before anything has been written (single hash is empty doc) - if (text.trim() === '' && title.trim === '') return; + if (text.trim() === "" && title.trim === "") return; // prevent autosave if nothing has changed if ( @@ -278,7 +278,7 @@ class DocumentScene extends React.Component { const bodyChanged = editorText !== document.text.trim(); // a single hash is a doc with just an empty title - this.isEmpty = (!editorText || editorText === '#') && !this.title; + this.isEmpty = (!editorText || editorText === "#") && !this.title; this.isDirty = bodyChanged || titleChanged; }; @@ -351,7 +351,7 @@ class DocumentScene extends React.Component { )} /> {(this.isUploading || this.isSaving) && } @@ -394,18 +394,18 @@ class DocumentScene extends React.Component { {document.archivedAt && !document.deletedAt && ( - Archived by {document.updatedBy.name}{' '} + Archived by {document.updatedBy.name}{" "} )} {document.deletedAt && ( - Deleted by {document.updatedBy.name}{' '} + Deleted by {document.updatedBy.name}{" "}
    @@ -237,7 +237,7 @@ class Header extends React.Component { disabled={publishingIsDisabled} small > - {isPublishing ? 'Publishing…' : 'Publish'} + {isPublishing ? "Publishing…" : "Publish"} @@ -307,7 +307,7 @@ const Status = styled.div` const BreadcrumbAndContents = styled(Flex)` display: none; - ${breakpoint('tablet')` + ${breakpoint("tablet")` display: flex; width: 33.3%; `}; @@ -318,7 +318,7 @@ const Wrapper = styled(Flex)` align-self: flex-end; height: 32px; - ${breakpoint('tablet')` + ${breakpoint("tablet")` width: 33.3%; `}; `; @@ -334,7 +334,7 @@ const Actions = styled(Flex)` ${props => props.isCompact ? darken(0.05, props.theme.sidebarBackground) - : 'transparent'}; + : "transparent"}; padding: 12px; transition: all 100ms ease-out; transform: translate3d(0, 0, 0); @@ -344,8 +344,8 @@ const Actions = styled(Flex)` display: none; } - ${breakpoint('tablet')` - padding: ${props => (props.isCompact ? '12px' : `24px 24px 0`)}; + ${breakpoint("tablet")` + padding: ${props => (props.isCompact ? "12px" : `24px 24px 0`)}; `}; `; @@ -361,10 +361,10 @@ const Title = styled.div` display: none; width: 0; - ${breakpoint('tablet')` + ${breakpoint("tablet")` display: flex; flex-grow: 1; `}; `; -export default inject('auth', 'ui', 'policies')(Header); +export default inject("auth", "ui", "policies")(Header); diff --git a/app/scenes/Document/components/HideSidebar.js b/app/scenes/Document/components/HideSidebar.js index 98a6f118..05869b6b 100644 --- a/app/scenes/Document/components/HideSidebar.js +++ b/app/scenes/Document/components/HideSidebar.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import UiStore from "stores/UiStore"; type Props = { ui: UiStore, diff --git a/app/scenes/Document/components/KeyboardShortcutsButton.js b/app/scenes/Document/components/KeyboardShortcutsButton.js index a8dceea3..0647711b 100644 --- a/app/scenes/Document/components/KeyboardShortcutsButton.js +++ b/app/scenes/Document/components/KeyboardShortcutsButton.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import breakpoint from 'styled-components-breakpoint'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import { KeyboardIcon } from 'outline-icons'; -import Modal from 'components/Modal'; -import Tooltip from 'components/Tooltip'; -import NudeButton from 'components/NudeButton'; -import KeyboardShortcuts from 'scenes/KeyboardShortcuts'; +import * as React from "react"; +import styled from "styled-components"; +import breakpoint from "styled-components-breakpoint"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import { KeyboardIcon } from "outline-icons"; +import Modal from "components/Modal"; +import Tooltip from "components/Tooltip"; +import NudeButton from "components/NudeButton"; +import KeyboardShortcuts from "scenes/KeyboardShortcuts"; type Props = {}; @@ -56,7 +56,7 @@ const Button = styled(NudeButton)` right: 0; margin: 24px; - ${breakpoint('tablet')` + ${breakpoint("tablet")` display: block; `}; diff --git a/app/scenes/Document/components/Loading.js b/app/scenes/Document/components/Loading.js index 10b4e243..a717215c 100644 --- a/app/scenes/Document/components/Loading.js +++ b/app/scenes/Document/components/Loading.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import type { Location } from 'react-router-dom'; -import Container from './Container'; -import LoadingPlaceholder from 'components/LoadingPlaceholder'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; +import * as React from "react"; +import type { Location } from "react-router-dom"; +import Container from "./Container"; +import LoadingPlaceholder from "components/LoadingPlaceholder"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; type Props = {| location: Location, @@ -13,7 +13,7 @@ type Props = {| export default function Loading({ location }: Props) { return ( - + diff --git a/app/scenes/Document/components/MarkAsViewed.js b/app/scenes/Document/components/MarkAsViewed.js index 05c045f6..8a12504b 100644 --- a/app/scenes/Document/components/MarkAsViewed.js +++ b/app/scenes/Document/components/MarkAsViewed.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import Document from 'models/Document'; +import * as React from "react"; +import Document from "models/Document"; const MARK_AS_VIEWED_AFTER = 3 * 1000; diff --git a/app/scenes/Document/components/ReferenceListItem.js b/app/scenes/Document/components/ReferenceListItem.js index e3913273..148f04bc 100644 --- a/app/scenes/Document/components/ReferenceListItem.js +++ b/app/scenes/Document/components/ReferenceListItem.js @@ -1,11 +1,11 @@ // @flow -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { Link } from 'react-router-dom'; -import styled from 'styled-components'; -import PublishingInfo from 'components/PublishingInfo'; -import Document from 'models/Document'; -import type { NavigationNode } from 'types'; +import * as React from "react"; +import { observer } from "mobx-react"; +import { Link } from "react-router-dom"; +import styled from "styled-components"; +import PublishingInfo from "components/PublishingInfo"; +import Document from "models/Document"; +import type { NavigationNode } from "types"; type Props = { document: Document | NavigationNode, @@ -39,8 +39,8 @@ const Title = styled.h3` margin-top: 0; margin-bottom: 0.25em; white-space: nowrap; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, - Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; `; @observer diff --git a/app/scenes/Document/components/References.js b/app/scenes/Document/components/References.js index 2fcca1b0..3e6f7324 100644 --- a/app/scenes/Document/components/References.js +++ b/app/scenes/Document/components/References.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import { withRouter, type Location } from 'react-router-dom'; -import Fade from 'components/Fade'; -import Tabs from 'components/Tabs'; -import Tab from 'components/Tab'; -import DocumentsStore from 'stores/DocumentsStore'; -import CollectionsStore from 'stores/CollectionsStore'; -import Document from 'models/Document'; -import ReferenceListItem from './ReferenceListItem'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import { withRouter, type Location } from "react-router-dom"; +import Fade from "components/Fade"; +import Tabs from "components/Tabs"; +import Tab from "components/Tab"; +import DocumentsStore from "stores/DocumentsStore"; +import CollectionsStore from "stores/CollectionsStore"; +import Document from "models/Document"; +import ReferenceListItem from "./ReferenceListItem"; type Props = { document: Document, @@ -34,7 +34,7 @@ class References extends React.Component { const showBacklinks = !!backlinks.length; const showChildren = !!children.length; const isBacklinksTab = - this.props.location.hash === '#backlinks' || !showChildren; + this.props.location.hash === "#backlinks" || !showChildren; return ( (showBacklinks || showChildren) && ( @@ -80,4 +80,4 @@ class References extends React.Component { } } -export default withRouter(inject('documents', 'collections')(References)); +export default withRouter(inject("documents", "collections")(References)); diff --git a/app/scenes/Document/components/SocketPresence.js b/app/scenes/Document/components/SocketPresence.js index 2994dd3c..483a55f6 100644 --- a/app/scenes/Document/components/SocketPresence.js +++ b/app/scenes/Document/components/SocketPresence.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import { SocketContext } from 'components/SocketProvider'; -import { USER_PRESENCE_INTERVAL } from 'shared/constants'; +import * as React from "react"; +import { SocketContext } from "components/SocketProvider"; +import { USER_PRESENCE_INTERVAL } from "shared/constants"; type Props = { children?: React.Node, @@ -33,8 +33,8 @@ export default class SocketPresence extends React.Component { componentWillUnmount() { if (this.context) { - this.context.emit('leave', { documentId: this.props.documentId }); - this.context.off('authenticated', this.emitJoin); + this.context.emit("leave", { documentId: this.props.documentId }); + this.context.off("authenticated", this.emitJoin); } clearInterval(this.editingInterval); @@ -47,7 +47,7 @@ export default class SocketPresence extends React.Component { if (this.context.authenticated) { this.emitJoin(); } - this.context.on('authenticated', () => { + this.context.on("authenticated", () => { this.emitJoin(); }); } @@ -56,7 +56,7 @@ export default class SocketPresence extends React.Component { emitJoin = () => { if (!this.context) return; - this.context.emit('join', { + this.context.emit("join", { documentId: this.props.documentId, isEditing: this.props.isEditing, }); @@ -65,7 +65,7 @@ export default class SocketPresence extends React.Component { emitPresence = () => { if (!this.context) return; - this.context.emit('presence', { + this.context.emit("presence", { documentId: this.props.documentId, isEditing: this.props.isEditing, }); diff --git a/app/scenes/Document/index.js b/app/scenes/Document/index.js index 8b8fbddd..7af896b2 100644 --- a/app/scenes/Document/index.js +++ b/app/scenes/Document/index.js @@ -1,3 +1,3 @@ // @flow -import DataLoader from './components/DataLoader'; +import DataLoader from "./components/DataLoader"; export default DataLoader; diff --git a/app/scenes/DocumentDelete.js b/app/scenes/DocumentDelete.js index 451802ad..2ba61e0b 100644 --- a/app/scenes/DocumentDelete.js +++ b/app/scenes/DocumentDelete.js @@ -1,15 +1,15 @@ // @flow -import * as React from 'react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import Button from 'components/Button'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import Document from 'models/Document'; -import DocumentsStore from 'stores/DocumentsStore'; -import UiStore from 'stores/UiStore'; -import { collectionUrl } from 'utils/routeHelpers'; +import * as React from "react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import Button from "components/Button"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import Document from "models/Document"; +import DocumentsStore from "stores/DocumentsStore"; +import UiStore from "stores/UiStore"; +import { collectionUrl } from "utils/routeHelpers"; type Props = { history: RouterHistory, @@ -49,7 +49,7 @@ class DocumentDelete extends React.Component {
    - Are you sure about that? Deleting the{' '} + Are you sure about that? Deleting the{" "} {document.title} document will delete all of its history, and any nested documents. @@ -61,7 +61,7 @@ class DocumentDelete extends React.Component { )}
    @@ -69,4 +69,4 @@ class DocumentDelete extends React.Component { } } -export default inject('documents', 'ui')(withRouter(DocumentDelete)); +export default inject("documents", "ui")(withRouter(DocumentDelete)); diff --git a/app/scenes/DocumentNew.js b/app/scenes/DocumentNew.js index 53cfdaf2..1bbe6b08 100644 --- a/app/scenes/DocumentNew.js +++ b/app/scenes/DocumentNew.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import { inject } from 'mobx-react'; -import type { RouterHistory, Location } from 'react-router-dom'; -import Flex from 'shared/components/Flex'; -import CenteredContent from 'components/CenteredContent'; -import LoadingPlaceholder from 'components/LoadingPlaceholder'; -import DocumentsStore from 'stores/DocumentsStore'; -import UiStore from 'stores/UiStore'; -import { documentEditUrl } from 'utils/routeHelpers'; +import * as React from "react"; +import { inject } from "mobx-react"; +import type { RouterHistory, Location } from "react-router-dom"; +import Flex from "shared/components/Flex"; +import CenteredContent from "components/CenteredContent"; +import LoadingPlaceholder from "components/LoadingPlaceholder"; +import DocumentsStore from "stores/DocumentsStore"; +import UiStore from "stores/UiStore"; +import { documentEditUrl } from "utils/routeHelpers"; type Props = { history: RouterHistory, @@ -23,14 +23,14 @@ class DocumentNew extends React.Component { const document = await this.props.documents.create({ collectionId: this.props.match.params.id, parentDocumentId: new URLSearchParams(this.props.location.search).get( - 'parentDocumentId' + "parentDocumentId" ), - title: '', - text: '', + title: "", + text: "", }); this.props.history.replace(documentEditUrl(document)); } catch (err) { - this.props.ui.showToast('Couldn’t create the document, try again?'); + this.props.ui.showToast("Couldn’t create the document, try again?"); this.props.history.goBack(); } } @@ -46,4 +46,4 @@ class DocumentNew extends React.Component { } } -export default inject('documents', 'ui')(DocumentNew); +export default inject("documents", "ui")(DocumentNew); diff --git a/app/scenes/DocumentShare.js b/app/scenes/DocumentShare.js index a2d3f30d..a79c753e 100644 --- a/app/scenes/DocumentShare.js +++ b/app/scenes/DocumentShare.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import { Link } from 'react-router-dom'; -import Input from 'components/Input'; -import Button from 'components/Button'; -import CopyToClipboard from 'components/CopyToClipboard'; -import HelpText from 'components/HelpText'; -import Document from 'models/Document'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import { Link } from "react-router-dom"; +import Input from "components/Input"; +import Button from "components/Button"; +import CopyToClipboard from "components/CopyToClipboard"; +import HelpText from "components/HelpText"; +import Document from "models/Document"; type Props = { document: Document, @@ -40,7 +40,7 @@ class DocumentShare extends React.Component { The link below allows anyone in the world to access a read-only version of the document {document.title}. You can - revoke this link in settings at any time.{' '} + revoke this link in settings at any time.{" "} Manage share links . @@ -48,15 +48,15 @@ class DocumentShare extends React.Component { diff --git a/app/scenes/Drafts.js b/app/scenes/Drafts.js index 451d0296..7914275c 100644 --- a/app/scenes/Drafts.js +++ b/app/scenes/Drafts.js @@ -1,17 +1,17 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; -import Heading from 'components/Heading'; -import CenteredContent from 'components/CenteredContent'; -import Empty from 'components/Empty'; -import PageTitle from 'components/PageTitle'; -import PaginatedDocumentList from 'components/PaginatedDocumentList'; -import Subheading from 'components/Subheading'; -import InputSearch from 'components/InputSearch'; -import NewDocumentMenu from 'menus/NewDocumentMenu'; -import Actions, { Action } from 'components/Actions'; -import DocumentsStore from 'stores/DocumentsStore'; +import Heading from "components/Heading"; +import CenteredContent from "components/CenteredContent"; +import Empty from "components/Empty"; +import PageTitle from "components/PageTitle"; +import PaginatedDocumentList from "components/PaginatedDocumentList"; +import Subheading from "components/Subheading"; +import InputSearch from "components/InputSearch"; +import NewDocumentMenu from "menus/NewDocumentMenu"; +import Actions, { Action } from "components/Actions"; +import DocumentsStore from "stores/DocumentsStore"; type Props = { documents: DocumentsStore, @@ -48,4 +48,4 @@ class Drafts extends React.Component { } } -export default inject('documents')(Drafts); +export default inject("documents")(Drafts); diff --git a/app/scenes/Error404.js b/app/scenes/Error404.js index 6d8e9a30..f5d5f988 100644 --- a/app/scenes/Error404.js +++ b/app/scenes/Error404.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import Empty from 'components/Empty'; +import * as React from "react"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import Empty from "components/Empty"; const Error404 = () => { return ( diff --git a/app/scenes/ErrorOffline.js b/app/scenes/ErrorOffline.js index 57d4c827..f40ac5f4 100644 --- a/app/scenes/ErrorOffline.js +++ b/app/scenes/ErrorOffline.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import Empty from 'components/Empty'; +import * as React from "react"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import Empty from "components/Empty"; const ErrorOffline = () => { return ( diff --git a/app/scenes/ErrorSuspended.js b/app/scenes/ErrorSuspended.js index 3f8041cd..009d9cd1 100644 --- a/app/scenes/ErrorSuspended.js +++ b/app/scenes/ErrorSuspended.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import { inject, observer } from 'mobx-react'; +import * as React from "react"; +import { inject, observer } from "mobx-react"; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import AuthStore from 'stores/AuthStore'; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import AuthStore from "stores/AuthStore"; const ErrorSuspended = observer(({ auth }: { auth: AuthStore }) => { return ( @@ -13,7 +13,7 @@ const ErrorSuspended = observer(({ auth }: { auth: AuthStore }) => {

    ⚠️ - {' '} + {" "} Your account has been suspended

    @@ -26,4 +26,4 @@ const ErrorSuspended = observer(({ auth }: { auth: AuthStore }) => { ); }); -export default inject('auth')(ErrorSuspended); +export default inject("auth")(ErrorSuspended); diff --git a/app/scenes/GroupDelete.js b/app/scenes/GroupDelete.js index 6377dda8..ff9aa6c9 100644 --- a/app/scenes/GroupDelete.js +++ b/app/scenes/GroupDelete.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import { groupSettings } from 'shared/utils/routeHelpers'; -import Button from 'components/Button'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import Group from 'models/Group'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import { groupSettings } from "shared/utils/routeHelpers"; +import Button from "components/Button"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import Group from "models/Group"; +import UiStore from "stores/UiStore"; type Props = { history: RouterHistory, @@ -43,12 +43,12 @@ class GroupDelete extends React.Component {
    - Are you sure about that? Deleting the {group.name}{' '} + Are you sure about that? Deleting the {group.name}{" "} group will cause its members to lose access to collections and documents that it is associated with.
    @@ -56,4 +56,4 @@ class GroupDelete extends React.Component { } } -export default inject('ui')(withRouter(GroupDelete)); +export default inject("ui")(withRouter(GroupDelete)); diff --git a/app/scenes/GroupEdit.js b/app/scenes/GroupEdit.js index 2ddbd2ea..7efb4de1 100644 --- a/app/scenes/GroupEdit.js +++ b/app/scenes/GroupEdit.js @@ -1,15 +1,15 @@ // @flow -import * as React from 'react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import Button from 'components/Button'; -import Input from 'components/Input'; -import HelpText from 'components/HelpText'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import Button from "components/Button"; +import Input from "components/Input"; +import HelpText from "components/HelpText"; +import Flex from "shared/components/Flex"; -import Group from 'models/Group'; -import UiStore from 'stores/UiStore'; +import Group from "models/Group"; +import UiStore from "stores/UiStore"; type Props = { history: RouterHistory, @@ -61,11 +61,11 @@ class GroupEdit extends React.Component { ); } } -export default inject('ui')(withRouter(GroupEdit)); +export default inject("ui")(withRouter(GroupEdit)); diff --git a/app/scenes/GroupMembers/AddPeopleToGroup.js b/app/scenes/GroupMembers/AddPeopleToGroup.js index 3b77e46e..dc274bba 100644 --- a/app/scenes/GroupMembers/AddPeopleToGroup.js +++ b/app/scenes/GroupMembers/AddPeopleToGroup.js @@ -1,21 +1,21 @@ // @flow -import * as React from 'react'; -import { inject, observer } from 'mobx-react'; -import { observable } from 'mobx'; -import { debounce } from 'lodash'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import Input from 'components/Input'; -import Modal from 'components/Modal'; -import Empty from 'components/Empty'; -import PaginatedList from 'components/PaginatedList'; -import Invite from 'scenes/Invite'; -import Group from 'models/Group'; -import UiStore from 'stores/UiStore'; -import AuthStore from 'stores/AuthStore'; -import UsersStore from 'stores/UsersStore'; -import GroupMembershipsStore from 'stores/GroupMembershipsStore'; -import GroupMemberListItem from './components/GroupMemberListItem'; +import * as React from "react"; +import { inject, observer } from "mobx-react"; +import { observable } from "mobx"; +import { debounce } from "lodash"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import Input from "components/Input"; +import Modal from "components/Modal"; +import Empty from "components/Empty"; +import PaginatedList from "components/PaginatedList"; +import Invite from "scenes/Invite"; +import Group from "models/Group"; +import UiStore from "stores/UiStore"; +import AuthStore from "stores/AuthStore"; +import UsersStore from "stores/UsersStore"; +import GroupMembershipsStore from "stores/GroupMembershipsStore"; +import GroupMemberListItem from "./components/GroupMemberListItem"; type Props = { ui: UiStore, @@ -29,7 +29,7 @@ type Props = { @observer class AddPeopleToGroup extends React.Component { @observable inviteModalOpen: boolean = false; - @observable query: string = ''; + @observable query: string = ""; handleInviteModalOpen = () => { this.inviteModalOpen = true; @@ -58,7 +58,7 @@ class AddPeopleToGroup extends React.Component { }); this.props.ui.showToast(`${user.name} was added to the group`); } catch (err) { - this.props.ui.showToast('Could not add user'); + this.props.ui.showToast("Could not add user"); } }; @@ -71,7 +71,7 @@ class AddPeopleToGroup extends React.Component { Add team members below to give them access to the group. Need to add - someone who’s not yet on the team yet?{' '} + someone who’s not yet on the team yet?{" "} Invite them to {team.name} . @@ -118,6 +118,6 @@ class AddPeopleToGroup extends React.Component { } } -export default inject('auth', 'users', 'groupMemberships', 'ui')( +export default inject("auth", "users", "groupMemberships", "ui")( AddPeopleToGroup ); diff --git a/app/scenes/GroupMembers/GroupMembers.js b/app/scenes/GroupMembers/GroupMembers.js index 7ecc6aab..cd8d9147 100644 --- a/app/scenes/GroupMembers/GroupMembers.js +++ b/app/scenes/GroupMembers/GroupMembers.js @@ -1,23 +1,23 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import { PlusIcon } from 'outline-icons'; -import Flex from 'shared/components/Flex'; -import Empty from 'components/Empty'; -import HelpText from 'components/HelpText'; -import Subheading from 'components/Subheading'; -import Button from 'components/Button'; -import PaginatedList from 'components/PaginatedList'; -import Modal from 'components/Modal'; -import Group from 'models/Group'; -import UiStore from 'stores/UiStore'; -import AuthStore from 'stores/AuthStore'; -import GroupMembershipsStore from 'stores/GroupMembershipsStore'; -import UsersStore from 'stores/UsersStore'; -import PoliciesStore from 'stores/PoliciesStore'; -import GroupMemberListItem from './components/GroupMemberListItem'; -import AddPeopleToGroup from './AddPeopleToGroup'; +import * as React from "react"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import { PlusIcon } from "outline-icons"; +import Flex from "shared/components/Flex"; +import Empty from "components/Empty"; +import HelpText from "components/HelpText"; +import Subheading from "components/Subheading"; +import Button from "components/Button"; +import PaginatedList from "components/PaginatedList"; +import Modal from "components/Modal"; +import Group from "models/Group"; +import UiStore from "stores/UiStore"; +import AuthStore from "stores/AuthStore"; +import GroupMembershipsStore from "stores/GroupMembershipsStore"; +import UsersStore from "stores/UsersStore"; +import PoliciesStore from "stores/PoliciesStore"; +import GroupMemberListItem from "./components/GroupMemberListItem"; +import AddPeopleToGroup from "./AddPeopleToGroup"; type Props = { ui: UiStore, @@ -48,7 +48,7 @@ class GroupMembers extends React.Component { }); this.props.ui.showToast(`${user.name} was removed from the group`); } catch (err) { - this.props.ui.showToast('Could not remove user'); + this.props.ui.showToast("Could not remove user"); } }; @@ -64,7 +64,7 @@ class GroupMembers extends React.Component { {can.update ? ( - Add and remove team members in the {group.name}{' '} + Add and remove team members in the {group.name}{" "} group. Adding people to the group will give them access to any collections this group has been given access to. @@ -119,6 +119,6 @@ class GroupMembers extends React.Component { } } -export default inject('auth', 'users', 'policies', 'groupMemberships', 'ui')( +export default inject("auth", "users", "policies", "groupMemberships", "ui")( GroupMembers ); diff --git a/app/scenes/GroupMembers/components/GroupMemberListItem.js b/app/scenes/GroupMembers/components/GroupMemberListItem.js index 7f5d3d04..623671a3 100644 --- a/app/scenes/GroupMembers/components/GroupMemberListItem.js +++ b/app/scenes/GroupMembers/components/GroupMemberListItem.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import Avatar from 'components/Avatar'; -import Flex from 'shared/components/Flex'; -import Time from 'shared/components/Time'; -import Badge from 'components/Badge'; -import Button from 'components/Button'; -import ListItem from 'components/List/Item'; -import User from 'models/User'; -import GroupMembership from 'models/GroupMembership'; -import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; +import * as React from "react"; +import Avatar from "components/Avatar"; +import Flex from "shared/components/Flex"; +import Time from "shared/components/Time"; +import Badge from "components/Badge"; +import Button from "components/Button"; +import ListItem from "components/List/Item"; +import User from "models/User"; +import GroupMembership from "models/GroupMembership"; +import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; type Props = { user: User, @@ -33,7 +33,7 @@ const GroupMemberListItem = ({ Active ) : ( - 'Never signed in' + "Never signed in" )} {!user.lastActiveAt && Invited} {user.isAdmin && Admin} diff --git a/app/scenes/GroupMembers/components/UserListItem.js b/app/scenes/GroupMembers/components/UserListItem.js index a12bfe5e..cf7202c3 100644 --- a/app/scenes/GroupMembers/components/UserListItem.js +++ b/app/scenes/GroupMembers/components/UserListItem.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import { PlusIcon } from 'outline-icons'; -import Time from 'shared/components/Time'; -import Avatar from 'components/Avatar'; -import Button from 'components/Button'; -import Badge from 'components/Badge'; -import ListItem from 'components/List/Item'; -import User from 'models/User'; +import * as React from "react"; +import { PlusIcon } from "outline-icons"; +import Time from "shared/components/Time"; +import Avatar from "components/Avatar"; +import Button from "components/Button"; +import Badge from "components/Badge"; +import ListItem from "components/List/Item"; +import User from "models/User"; type Props = { user: User, @@ -26,7 +26,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => { Active @@ -165,7 +165,7 @@ class Invite extends React.Component { - Guests can sign in with email and
    do not require{' '} + Guests can sign in with email and
    do not require{" "} {team.signinMethods} accounts } @@ -210,7 +210,7 @@ class Invite extends React.Component { data-event-category="invite" data-event-action="sendInvites" > - {this.isSaving ? 'Inviting…' : 'Send Invites'} + {this.isSaving ? "Inviting…" : "Send Invites"}
    @@ -219,7 +219,7 @@ class Invite extends React.Component { } } -const CopyBlock = styled('div')` +const CopyBlock = styled("div")` font-size: 14px; background: ${props => props.theme.secondaryBackground}; padding: 8px 16px 4px; @@ -232,16 +232,16 @@ const CopyBlock = styled('div')` } `; -const Guest = styled('div')` +const Guest = styled("div")` padding-top: 4px; margin: 0 4px 16px; align-self: flex-end; `; -const Remove = styled('div')` +const Remove = styled("div")` margin-top: 6px; position: absolute; right: -32px; `; -export default inject('auth', 'users', 'policies', 'ui')(withRouter(Invite)); +export default inject("auth", "users", "policies", "ui")(withRouter(Invite)); diff --git a/app/scenes/KeyboardShortcuts.js b/app/scenes/KeyboardShortcuts.js index 2aff9ab1..86248f41 100644 --- a/app/scenes/KeyboardShortcuts.js +++ b/app/scenes/KeyboardShortcuts.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Key from 'components/Key'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import { meta } from 'utils/keyboard'; +import * as React from "react"; +import styled from "styled-components"; +import Key from "components/Key"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import { meta } from "utils/keyboard"; function KeyboardShortcuts() { return ( @@ -136,7 +136,7 @@ function KeyboardShortcuts() { - {'```'} + {"```"} @@ -146,7 +146,7 @@ function KeyboardShortcuts() { ~~strikethrough~~ - {'`code`'} + {"`code`"} ==highlight== diff --git a/app/scenes/Search/Search.js b/app/scenes/Search/Search.js index 3bf3e234..84b73862 100644 --- a/app/scenes/Search/Search.js +++ b/app/scenes/Search/Search.js @@ -1,39 +1,39 @@ // @flow -import * as React from 'react'; -import ReactDOM from 'react-dom'; -import keydown from 'react-keydown'; -import { Waypoint } from 'react-waypoint'; -import { withRouter, Link } from 'react-router-dom'; -import type { Location, RouterHistory } from 'react-router-dom'; -import { PlusIcon } from 'outline-icons'; -import { observable, action } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { debounce } from 'lodash'; -import queryString from 'query-string'; -import styled from 'styled-components'; -import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; +import * as React from "react"; +import ReactDOM from "react-dom"; +import keydown from "react-keydown"; +import { Waypoint } from "react-waypoint"; +import { withRouter, Link } from "react-router-dom"; +import type { Location, RouterHistory } from "react-router-dom"; +import { PlusIcon } from "outline-icons"; +import { observable, action } from "mobx"; +import { observer, inject } from "mobx-react"; +import { debounce } from "lodash"; +import queryString from "query-string"; +import styled from "styled-components"; +import ArrowKeyNavigation from "boundless-arrow-key-navigation"; -import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore'; -import DocumentsStore from 'stores/DocumentsStore'; -import UsersStore from 'stores/UsersStore'; -import { newDocumentUrl, searchUrl } from 'utils/routeHelpers'; -import { meta } from 'utils/keyboard'; +import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore"; +import DocumentsStore from "stores/DocumentsStore"; +import UsersStore from "stores/UsersStore"; +import { newDocumentUrl, searchUrl } from "utils/routeHelpers"; +import { meta } from "utils/keyboard"; -import Flex from 'shared/components/Flex'; -import Button from 'components/Button'; -import Empty from 'components/Empty'; -import Fade from 'components/Fade'; -import HelpText from 'components/HelpText'; -import CenteredContent from 'components/CenteredContent'; -import LoadingIndicator from 'components/LoadingIndicator'; -import DocumentPreview from 'components/DocumentPreview'; -import NewDocumentMenu from 'menus/NewDocumentMenu'; -import PageTitle from 'components/PageTitle'; -import SearchField from './components/SearchField'; -import StatusFilter from './components/StatusFilter'; -import CollectionFilter from './components/CollectionFilter'; -import UserFilter from './components/UserFilter'; -import DateFilter from './components/DateFilter'; +import Flex from "shared/components/Flex"; +import Button from "components/Button"; +import Empty from "components/Empty"; +import Fade from "components/Fade"; +import HelpText from "components/HelpText"; +import CenteredContent from "components/CenteredContent"; +import LoadingIndicator from "components/LoadingIndicator"; +import DocumentPreview from "components/DocumentPreview"; +import NewDocumentMenu from "menus/NewDocumentMenu"; +import PageTitle from "components/PageTitle"; +import SearchField from "./components/SearchField"; +import StatusFilter from "./components/StatusFilter"; +import CollectionFilter from "./components/CollectionFilter"; +import UserFilter from "./components/UserFilter"; +import DateFilter from "./components/DateFilter"; type Props = { history: RouterHistory, @@ -49,7 +49,7 @@ class Search extends React.Component { firstDocument: ?DocumentPreview; @observable - query: string = decodeURIComponent(this.props.match.params.term || ''); + query: string = decodeURIComponent(this.props.match.params.term || ""); @observable params: URLSearchParams = new URLSearchParams(); @observable offset: number = 0; @observable allowLoadMore: boolean = true; @@ -73,7 +73,7 @@ class Search extends React.Component { } } - @keydown('esc') + @keydown("esc") goBack() { this.props.history.goBack(); } @@ -107,8 +107,8 @@ class Search extends React.Component { }; handleTermChange = () => { - const query = decodeURIComponent(this.props.match.params.term || ''); - this.query = query ? query : ''; + const query = decodeURIComponent(this.props.match.params.term || ""); + this.query = query ? query : ""; this.offset = 0; this.allowLoadMore = true; @@ -135,21 +135,21 @@ class Search extends React.Component { }; get includeArchived() { - return this.params.get('includeArchived') === 'true'; + return this.params.get("includeArchived") === "true"; } get collectionId() { - const id = this.params.get('collectionId'); + const id = this.params.get("collectionId"); return id ? id : undefined; } get userId() { - const id = this.params.get('userId'); + const id = this.params.get("userId"); return id ? id : undefined; } get dateFilter() { - const id = this.params.get('dateFilter'); + const id = this.params.get("dateFilter"); return id ? id : undefined; } @@ -164,7 +164,7 @@ class Search extends React.Component { get title() { const query = this.query; - const title = 'Search'; + const title = "Search"; if (query) return `${query} – ${title}`; return title; } @@ -361,14 +361,14 @@ const Container = styled(CenteredContent)` const ResultsWrapper = styled(Flex)` position: absolute; transition: all 300ms cubic-bezier(0.65, 0.05, 0.36, 1); - top: ${props => (props.pinToTop ? '0%' : '50%')}; - margin-top: ${props => (props.pinToTop ? '40px' : '-75px')}; + top: ${props => (props.pinToTop ? "0%" : "50%")}; + margin-top: ${props => (props.pinToTop ? "40px" : "-75px")}; width: 100%; `; const ResultList = styled(Flex)` margin-bottom: 150px; - opacity: ${props => (props.visible ? '1' : '0')}; + opacity: ${props => (props.visible ? "1" : "0")}; transition: all 400ms cubic-bezier(0.65, 0.05, 0.36, 1); `; @@ -388,4 +388,4 @@ const Filters = styled(Flex)` } `; -export default withRouter(inject('documents')(Search)); +export default withRouter(inject("documents")(Search)); diff --git a/app/scenes/Search/components/CollectionFilter.js b/app/scenes/Search/components/CollectionFilter.js index 91905bed..4191452e 100644 --- a/app/scenes/Search/components/CollectionFilter.js +++ b/app/scenes/Search/components/CollectionFilter.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import FilterOptions from './FilterOptions'; -import CollectionsStore from 'stores/CollectionsStore'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import FilterOptions from "./FilterOptions"; +import CollectionsStore from "stores/CollectionsStore"; const defaultOption = { key: undefined, - label: 'Any collection', + label: "Any collection", }; type Props = { @@ -36,4 +36,4 @@ class CollectionFilter extends React.Component { } } -export default inject('collections')(CollectionFilter); +export default inject("collections")(CollectionFilter); diff --git a/app/scenes/Search/components/DateFilter.js b/app/scenes/Search/components/DateFilter.js index 605416d0..8fca6047 100644 --- a/app/scenes/Search/components/DateFilter.js +++ b/app/scenes/Search/components/DateFilter.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import FilterOptions from './FilterOptions'; +import * as React from "react"; +import FilterOptions from "./FilterOptions"; const options = [ - { key: undefined, label: 'Any time' }, - { key: 'day', label: 'Past day' }, - { key: 'week', label: 'Past week' }, - { key: 'month', label: 'Past month' }, - { key: 'year', label: 'Past year' }, + { key: undefined, label: "Any time" }, + { key: "day", label: "Past day" }, + { key: "week", label: "Past week" }, + { key: "month", label: "Past month" }, + { key: "year", label: "Past year" }, ]; type Props = { diff --git a/app/scenes/Search/components/FilterOption.js b/app/scenes/Search/components/FilterOption.js index 627597d9..f6725b61 100644 --- a/app/scenes/Search/components/FilterOption.js +++ b/app/scenes/Search/components/FilterOption.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import { CheckmarkIcon } from 'outline-icons'; -import styled from 'styled-components'; -import HelpText from 'components/HelpText'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import { CheckmarkIcon } from "outline-icons"; +import styled from "styled-components"; +import HelpText from "components/HelpText"; +import Flex from "shared/components/Flex"; type Props = { label: string, @@ -34,7 +34,7 @@ const Checkmark = styled(CheckmarkIcon)` fill: ${props => props.theme.text}; `; -const Anchor = styled('a')` +const Anchor = styled("a")` display: flex; flex-direction: column; font-size: 15px; @@ -52,9 +52,9 @@ const Anchor = styled('a')` } `; -const ListItem = styled('li')` +const ListItem = styled("li")` list-style: none; - font-weight: ${props => (props.active ? '600' : 'normal')}; + font-weight: ${props => (props.active ? "600" : "normal")}; `; export default FilterOption; diff --git a/app/scenes/Search/components/FilterOptions.js b/app/scenes/Search/components/FilterOptions.js index 7346826c..7c45d7ab 100644 --- a/app/scenes/Search/components/FilterOptions.js +++ b/app/scenes/Search/components/FilterOptions.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import { find } from 'lodash'; -import styled from 'styled-components'; -import Button, { Inner } from 'components/Button'; -import { DropdownMenu } from 'components/DropdownMenu'; -import FilterOption from './FilterOption'; +import * as React from "react"; +import { find } from "lodash"; +import styled from "styled-components"; +import Button, { Inner } from "components/Button"; +import { DropdownMenu } from "components/DropdownMenu"; +import FilterOption from "./FilterOption"; type Props = { options: { @@ -22,11 +22,11 @@ const FilterOptions = ({ options, activeKey, defaultLabel, - selectedPrefix = '', + selectedPrefix = "", onSelect, }: Props) => { const selected = find(options, { key: activeKey }) || options[0]; - const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : ''; + const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : ""; return ( @@ -49,7 +49,7 @@ const FilterOptions = ({ ); }; -const Content = styled('div')` +const Content = styled("div")` padding: 0 8px; width: 250px; @@ -95,7 +95,7 @@ const DropdownButton = styled(SearchFilter)` margin-right: 8px; `; -const List = styled('ol')` +const List = styled("ol")` list-style: none; margin: 0; padding: 0; diff --git a/app/scenes/Search/components/SearchField.js b/app/scenes/Search/components/SearchField.js index 5816dcce..6bed3cfb 100644 --- a/app/scenes/Search/components/SearchField.js +++ b/app/scenes/Search/components/SearchField.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import styled, { withTheme } from 'styled-components'; -import { SearchIcon } from 'outline-icons'; -import Flex from 'shared/components/Flex'; +import * as React from "react"; +import styled, { withTheme } from "styled-components"; +import { SearchIcon } from "outline-icons"; +import Flex from "shared/components/Flex"; type Props = { onChange: string => void, @@ -16,13 +16,13 @@ class SearchField extends React.Component { componentDidMount() { if (this.props && this.input) { // ensure that focus is placed at end of input - const len = (this.props.defaultValue || '').length; + const len = (this.props.defaultValue || "").length; this.input.setSelectionRange(len, len); } } handleChange = (ev: SyntheticEvent) => { - this.props.onChange(ev.currentTarget.value ? ev.currentTarget.value : ''); + this.props.onChange(ev.currentTarget.value ? ev.currentTarget.value : ""); }; focusInput = (ev: SyntheticEvent<>) => { diff --git a/app/scenes/Search/components/StatusFilter.js b/app/scenes/Search/components/StatusFilter.js index 01622454..2a17b550 100644 --- a/app/scenes/Search/components/StatusFilter.js +++ b/app/scenes/Search/components/StatusFilter.js @@ -1,17 +1,17 @@ // @flow -import * as React from 'react'; -import FilterOptions from './FilterOptions'; +import * as React from "react"; +import FilterOptions from "./FilterOptions"; const options = [ { key: undefined, - label: 'Active documents', - note: 'Documents in collections you are able to access', + label: "Active documents", + note: "Documents in collections you are able to access", }, { - key: 'true', - label: 'All documents', - note: 'Include documents that are in the archive', + key: "true", + label: "All documents", + note: "Include documents that are in the archive", }, ]; @@ -24,7 +24,7 @@ const StatusFilter = ({ includeArchived, onSelect }: Props) => { return ( diff --git a/app/scenes/Search/components/UserFilter.js b/app/scenes/Search/components/UserFilter.js index f3837259..9c293165 100644 --- a/app/scenes/Search/components/UserFilter.js +++ b/app/scenes/Search/components/UserFilter.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import FilterOptions from './FilterOptions'; -import UsersStore from 'stores/UsersStore'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import FilterOptions from "./FilterOptions"; +import UsersStore from "stores/UsersStore"; const defaultOption = { key: undefined, - label: 'Any author', + label: "Any author", }; type Props = { @@ -40,4 +40,4 @@ class UserFilter extends React.Component { } } -export default inject('users')(UserFilter); +export default inject("users")(UserFilter); diff --git a/app/scenes/Search/index.js b/app/scenes/Search/index.js index cab60251..1fa6535a 100644 --- a/app/scenes/Search/index.js +++ b/app/scenes/Search/index.js @@ -1,3 +1,3 @@ // @flow -import Search from './Search'; +import Search from "./Search"; export default Search; diff --git a/app/scenes/Settings/Details.js b/app/scenes/Settings/Details.js index cdcf0294..120e1e66 100644 --- a/app/scenes/Settings/Details.js +++ b/app/scenes/Settings/Details.js @@ -1,18 +1,18 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import styled from 'styled-components'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import styled from "styled-components"; -import AuthStore from 'stores/AuthStore'; -import UiStore from 'stores/UiStore'; -import ImageUpload from './components/ImageUpload'; -import Input, { LabelText } from 'components/Input'; -import Button from 'components/Button'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; -import Flex from 'shared/components/Flex'; +import AuthStore from "stores/AuthStore"; +import UiStore from "stores/UiStore"; +import ImageUpload from "./components/ImageUpload"; +import Input, { LabelText } from "components/Input"; +import Button from "components/Button"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import HelpText from "components/HelpText"; +import Flex from "shared/components/Flex"; type Props = { auth: AuthStore, @@ -49,7 +49,7 @@ class Details extends React.Component { avatarUrl: this.avatarUrl, subdomain: this.subdomain, }); - this.props.ui.showToast('Settings saved'); + this.props.ui.showToast("Settings saved"); } catch (err) { this.props.ui.showToast(err.message); } @@ -68,7 +68,7 @@ class Details extends React.Component { }; handleAvatarError = (error: ?string) => { - this.props.ui.showToast(error || 'Unable to upload new logo'); + this.props.ui.showToast(error || "Unable to upload new logo"); }; get isValid() { @@ -120,7 +120,7 @@ class Details extends React.Component { { /> {this.subdomain && ( - Your knowledgebase will be accessible at{' '} + Your knowledgebase will be accessible at{" "} {this.subdomain}.getoutline.com )} )} @@ -183,4 +183,4 @@ const Avatar = styled.img` ${avatarStyles}; `; -export default inject('auth', 'ui')(Details); +export default inject("auth", "ui")(Details); diff --git a/app/scenes/Settings/Events.js b/app/scenes/Settings/Events.js index 3d7be421..c49c6387 100644 --- a/app/scenes/Settings/Events.js +++ b/app/scenes/Settings/Events.js @@ -1,19 +1,19 @@ // @flow -import * as React from 'react'; -import { observable, action } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { Waypoint } from 'react-waypoint'; +import * as React from "react"; +import { observable, action } from "mobx"; +import { observer, inject } from "mobx-react"; +import { Waypoint } from "react-waypoint"; -import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore'; -import EventsStore from 'stores/EventsStore'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; -import List from 'components/List'; -import Tabs from 'components/Tabs'; -import Tab from 'components/Tab'; -import { ListPlaceholder } from 'components/LoadingPlaceholder'; -import EventListItem from './components/EventListItem'; +import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore"; +import EventsStore from "stores/EventsStore"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import HelpText from "components/HelpText"; +import List from "components/List"; +import Tabs from "components/Tabs"; +import Tab from "components/Tab"; +import { ListPlaceholder } from "components/LoadingPlaceholder"; +import EventListItem from "./components/EventListItem"; type Props = { events: EventsStore, @@ -96,4 +96,4 @@ class Events extends React.Component { } } -export default inject('events')(Events); +export default inject("events")(Events); diff --git a/app/scenes/Settings/Export.js b/app/scenes/Settings/Export.js index 417a32a1..cbda73ab 100644 --- a/app/scenes/Settings/Export.js +++ b/app/scenes/Settings/Export.js @@ -1,15 +1,15 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import AuthStore from 'stores/AuthStore'; -import CollectionsStore from 'stores/CollectionsStore'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import AuthStore from "stores/AuthStore"; +import CollectionsStore from "stores/CollectionsStore"; +import UiStore from "stores/UiStore"; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; -import Button from 'components/Button'; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import HelpText from "components/HelpText"; +import Button from "components/Button"; type Props = { auth: AuthStore, @@ -29,7 +29,7 @@ class Export extends React.Component { try { await this.props.collections.export(); this.isExporting = true; - this.props.ui.showToast('Export in progress…'); + this.props.ui.showToast("Export in progress…"); } finally { this.isLoading = false; } @@ -60,12 +60,12 @@ class Export extends React.Component { primary > {this.isExporting - ? 'Export Requested' - : this.isLoading ? 'Requesting Export…' : 'Export All Data'} + ? "Export Requested" + : this.isLoading ? "Requesting Export…" : "Export All Data"} ); } } -export default inject('auth', 'ui', 'collections')(Export); +export default inject("auth", "ui", "collections")(Export); diff --git a/app/scenes/Settings/Groups.js b/app/scenes/Settings/Groups.js index 9d040b74..2d007705 100644 --- a/app/scenes/Settings/Groups.js +++ b/app/scenes/Settings/Groups.js @@ -1,27 +1,27 @@ // @flow -import * as React from 'react'; -import invariant from 'invariant'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { PlusIcon } from 'outline-icons'; +import * as React from "react"; +import invariant from "invariant"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import { PlusIcon } from "outline-icons"; -import Empty from 'components/Empty'; -import { ListPlaceholder } from 'components/LoadingPlaceholder'; -import Modal from 'components/Modal'; -import Button from 'components/Button'; -import GroupNew from 'scenes/GroupNew'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; -import GroupListItem from 'components/GroupListItem'; -import List from 'components/List'; -import Tabs from 'components/Tabs'; -import Tab from 'components/Tab'; -import GroupMenu from 'menus/GroupMenu'; +import Empty from "components/Empty"; +import { ListPlaceholder } from "components/LoadingPlaceholder"; +import Modal from "components/Modal"; +import Button from "components/Button"; +import GroupNew from "scenes/GroupNew"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import HelpText from "components/HelpText"; +import GroupListItem from "components/GroupListItem"; +import List from "components/List"; +import Tabs from "components/Tabs"; +import Tab from "components/Tab"; +import GroupMenu from "menus/GroupMenu"; -import AuthStore from 'stores/AuthStore'; -import GroupsStore from 'stores/GroupsStore'; -import PoliciesStore from 'stores/PoliciesStore'; +import AuthStore from "stores/AuthStore"; +import GroupsStore from "stores/GroupsStore"; +import PoliciesStore from "stores/PoliciesStore"; type Props = { auth: AuthStore, @@ -51,8 +51,8 @@ class Groups extends React.Component { const currentUser = auth.user; const team = auth.team; - invariant(currentUser, 'User should exist'); - invariant(team, 'Team should exist'); + invariant(currentUser, "User should exist"); + invariant(team, "Team should exist"); const showLoading = groups.isFetching && !groups.orderedData.length; const showEmpty = groups.isLoaded && !groups.orderedData.length; @@ -111,4 +111,4 @@ class Groups extends React.Component { } } -export default inject('auth', 'groups', 'policies')(Groups); +export default inject("auth", "groups", "policies")(Groups); diff --git a/app/scenes/Settings/Notifications.js b/app/scenes/Settings/Notifications.js index ef765646..94ba1a2c 100644 --- a/app/scenes/Settings/Notifications.js +++ b/app/scenes/Settings/Notifications.js @@ -1,19 +1,19 @@ // @flow -import * as React from 'react'; -import { debounce } from 'lodash'; -import { observer, inject } from 'mobx-react'; -import styled from 'styled-components'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; -import Input from 'components/Input'; -import Subheading from 'components/Subheading'; -import NotificationListItem from './components/NotificationListItem'; -import Notice from 'shared/components/Notice'; +import * as React from "react"; +import { debounce } from "lodash"; +import { observer, inject } from "mobx-react"; +import styled from "styled-components"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import HelpText from "components/HelpText"; +import Input from "components/Input"; +import Subheading from "components/Subheading"; +import NotificationListItem from "./components/NotificationListItem"; +import Notice from "shared/components/Notice"; -import UiStore from 'stores/UiStore'; -import AuthStore from 'stores/AuthStore'; -import NotificationSettingsStore from 'stores/NotificationSettingsStore'; +import UiStore from "stores/UiStore"; +import AuthStore from "stores/AuthStore"; +import NotificationSettingsStore from "stores/NotificationSettingsStore"; type Props = { ui: UiStore, @@ -23,33 +23,33 @@ type Props = { const options = [ { - event: 'documents.publish', - title: 'Document published', - description: 'Receive a notification whenever a new document is published', + event: "documents.publish", + title: "Document published", + description: "Receive a notification whenever a new document is published", }, { - event: 'documents.update', - title: 'Document updated', - description: 'Receive a notification when a document you created is edited', + event: "documents.update", + title: "Document updated", + description: "Receive a notification when a document you created is edited", }, { - event: 'collections.create', - title: 'Collection created', - description: 'Receive a notification whenever a new collection is created', + event: "collections.create", + title: "Collection created", + description: "Receive a notification whenever a new collection is created", }, { separator: true, }, { - event: 'emails.onboarding', - title: 'Getting started', + event: "emails.onboarding", + title: "Getting started", description: - 'Tips on getting started with Outline`s features and functionality', + "Tips on getting started with Outline`s features and functionality", }, { - event: 'emails.features', - title: 'New features', - description: 'Receive an email when new features of note are added', + event: "emails.features", + title: "New features", + description: "Receive an email when new features of note are added", }, ]; @@ -75,12 +75,12 @@ class Notifications extends React.Component { }; showSuccessMessage = debounce(() => { - this.props.ui.showToast('Notifications saved'); + this.props.ui.showToast("Notifications saved"); }, 500); render() { const { notificationSettings, auth } = this.props; - const showSuccessNotice = window.location.search === '?success'; + const showSuccessNotice = window.location.search === "?success"; const { user, team } = auth; if (!team || !user) return null; @@ -97,8 +97,8 @@ class Notifications extends React.Component { Manage when and where you receive email notifications from Outline. - Your email address can be updated in your{' '} - {team.slackConnected ? 'Slack' : 'Google'} account. + Your email address can be updated in your{" "} + {team.slackConnected ? "Slack" : "Google"} account. { const { filter } = match.params; const currentUser = auth.user; const team = auth.team; - invariant(currentUser, 'User should exist'); - invariant(team, 'Team should exist'); + invariant(currentUser, "User should exist"); + invariant(team, "Team should exist"); let users = this.props.users.active; - if (filter === 'all') { + if (filter === "all") { users = this.props.users.all; - } else if (filter === 'admins') { + } else if (filter === "admins") { users = this.props.users.admins; - } else if (filter === 'suspended') { + } else if (filter === "suspended") { users = this.props.users.suspended; - } else if (filter === 'invited') { + } else if (filter === "invited") { users = this.props.users.invited; } @@ -136,4 +136,4 @@ class People extends React.Component { } } -export default inject('auth', 'users', 'policies')(People); +export default inject("auth", "users", "policies")(People); diff --git a/app/scenes/Settings/Profile.js b/app/scenes/Settings/Profile.js index a0b35069..71ad9276 100644 --- a/app/scenes/Settings/Profile.js +++ b/app/scenes/Settings/Profile.js @@ -1,18 +1,18 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import styled from 'styled-components'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import styled from "styled-components"; -import AuthStore from 'stores/AuthStore'; -import UiStore from 'stores/UiStore'; -import ImageUpload from './components/ImageUpload'; -import Input, { LabelText } from 'components/Input'; -import Button from 'components/Button'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import UserDelete from 'scenes/UserDelete'; -import Flex from 'shared/components/Flex'; +import AuthStore from "stores/AuthStore"; +import UiStore from "stores/UiStore"; +import ImageUpload from "./components/ImageUpload"; +import Input, { LabelText } from "components/Input"; +import Button from "components/Button"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import UserDelete from "scenes/UserDelete"; +import Flex from "shared/components/Flex"; type Props = { auth: AuthStore, @@ -45,7 +45,7 @@ class Profile extends React.Component { name: this.name, avatarUrl: this.avatarUrl, }); - this.props.ui.showToast('Profile saved'); + this.props.ui.showToast("Profile saved"); }; handleNameChange = (ev: SyntheticInputEvent<*>) => { @@ -58,11 +58,11 @@ class Profile extends React.Component { await this.props.auth.updateUser({ avatarUrl: this.avatarUrl, }); - this.props.ui.showToast('Profile picture updated'); + this.props.ui.showToast("Profile picture updated"); }; handleAvatarError = (error: ?string) => { - this.props.ui.showToast(error || 'Unable to upload new avatar'); + this.props.ui.showToast(error || "Unable to upload new avatar"); }; toggleDeleteAccount = () => { @@ -106,7 +106,7 @@ class Profile extends React.Component { short /> @@ -114,7 +114,7 @@ class Profile extends React.Component { Delete Account

    You may delete your account at any time, note that this is - unrecoverable.{' '} + unrecoverable.{" "} Delete account.

    @@ -170,4 +170,4 @@ const Avatar = styled.img` ${avatarStyles}; `; -export default inject('auth', 'ui')(Profile); +export default inject("auth", "ui")(Profile); diff --git a/app/scenes/Settings/Security.js b/app/scenes/Settings/Security.js index 277e85e2..b4c1bcc1 100644 --- a/app/scenes/Settings/Security.js +++ b/app/scenes/Settings/Security.js @@ -1,15 +1,15 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import { debounce } from 'lodash'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import { debounce } from "lodash"; -import AuthStore from 'stores/AuthStore'; -import UiStore from 'stores/UiStore'; -import Checkbox from 'components/Checkbox'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; +import AuthStore from "stores/AuthStore"; +import UiStore from "stores/UiStore"; +import Checkbox from "components/Checkbox"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import HelpText from "components/HelpText"; type Props = { auth: AuthStore, @@ -35,13 +35,13 @@ class Security extends React.Component { handleChange = async (ev: SyntheticInputEvent<*>) => { switch (ev.target.name) { - case 'sharing': + case "sharing": this.sharing = ev.target.checked; break; - case 'documentEmbeds': + case "documentEmbeds": this.documentEmbeds = ev.target.checked; break; - case 'guestSignin': + case "guestSignin": this.guestSignin = ev.target.checked; break; default: @@ -56,7 +56,7 @@ class Security extends React.Component { }; showSuccessMessage = debounce(() => { - this.props.ui.showToast('Settings saved'); + this.props.ui.showToast("Settings saved"); }, 500); render() { @@ -77,7 +77,7 @@ class Security extends React.Component { checked={this.guestSignin} onChange={this.handleChange} note={`When enabled guests can be invited by email address and are able to signin without ${ - team ? team.signinMethods : 'SSO' + team ? team.signinMethods : "SSO" }`} /> { } } -export default inject('auth', 'ui')(Security); +export default inject("auth", "ui")(Security); diff --git a/app/scenes/Settings/Shares.js b/app/scenes/Settings/Shares.js index 2dee6c8f..a164dde3 100644 --- a/app/scenes/Settings/Shares.js +++ b/app/scenes/Settings/Shares.js @@ -1,17 +1,17 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; -import { Link } from 'react-router-dom'; -import SharesStore from 'stores/SharesStore'; -import AuthStore from 'stores/AuthStore'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; +import { Link } from "react-router-dom"; +import SharesStore from "stores/SharesStore"; +import AuthStore from "stores/AuthStore"; -import ShareListItem from './components/ShareListItem'; -import Empty from 'components/Empty'; -import List from 'components/List'; -import CenteredContent from 'components/CenteredContent'; -import Subheading from 'components/Subheading'; -import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; +import ShareListItem from "./components/ShareListItem"; +import Empty from "components/Empty"; +import List from "components/List"; +import CenteredContent from "components/CenteredContent"; +import Subheading from "components/Subheading"; +import PageTitle from "components/PageTitle"; +import HelpText from "components/HelpText"; type Props = { shares: SharesStore, @@ -44,8 +44,8 @@ class Shares extends React.Component { {!canShareDocuments && ( Sharing is currently disabled. - )}{' '} - You can turn {canShareDocuments ? 'off' : 'on'} public document + )}{" "} + You can turn {canShareDocuments ? "off" : "on"} public document sharing in security settings. )} @@ -64,4 +64,4 @@ class Shares extends React.Component { } } -export default inject('shares', 'auth')(Shares); +export default inject("shares", "auth")(Shares); diff --git a/app/scenes/Settings/Slack.js b/app/scenes/Settings/Slack.js index 9f636b43..cdbe05b5 100644 --- a/app/scenes/Settings/Slack.js +++ b/app/scenes/Settings/Slack.js @@ -1,19 +1,19 @@ // @flow -import * as React from 'react'; -import { inject, observer } from 'mobx-react'; -import { find } from 'lodash'; -import styled from 'styled-components'; +import * as React from "react"; +import { inject, observer } from "mobx-react"; +import { find } from "lodash"; +import styled from "styled-components"; -import Button from 'components/Button'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; -import SlackButton from './components/SlackButton'; -import CollectionsStore from 'stores/CollectionsStore'; -import IntegrationsStore from 'stores/IntegrationsStore'; -import AuthStore from 'stores/AuthStore'; -import Notice from 'shared/components/Notice'; -import getQueryVariable from 'shared/utils/getQueryVariable'; +import Button from "components/Button"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import HelpText from "components/HelpText"; +import SlackButton from "./components/SlackButton"; +import CollectionsStore from "stores/CollectionsStore"; +import IntegrationsStore from "stores/IntegrationsStore"; +import AuthStore from "stores/AuthStore"; +import Notice from "shared/components/Notice"; +import getQueryVariable from "shared/utils/getQueryVariable"; type Props = { collections: CollectionsStore, @@ -26,39 +26,39 @@ class Slack extends React.Component { error: ?string; componentDidMount() { - this.error = getQueryVariable('error'); + this.error = getQueryVariable("error"); this.props.collections.fetchPage({ limit: 100 }); this.props.integrations.fetchPage(); } get commandIntegration() { return find(this.props.integrations.slackIntegrations, { - type: 'command', + type: "command", }); } render() { const { collections, integrations, auth } = this.props; - const teamId = auth.team ? auth.team.id : ''; + const teamId = auth.team ? auth.team.id : ""; return (

    Slack

    - {this.error === 'access_denied' && ( + {this.error === "access_denied" && ( Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again? )} - {this.error === 'unauthenticated' && ( + {this.error === "unauthenticated" && ( Something went wrong while authenticating your request. Please try logging in again? )} - Preview Outline links your team mates share and use the{' '} + Preview Outline links your team mates share and use the{" "} /outline slash command in Slack to search for documents in your team’s wiki. @@ -67,7 +67,7 @@ class Slack extends React.Component { ) : ( @@ -91,7 +91,7 @@ class Slack extends React.Component { return ( - {collection.name} posting activity to the{' '} + {collection.name} posting activity to the{" "} {integration.settings.channel} Slack channel @@ -104,7 +104,7 @@ class Slack extends React.Component { {collection.name} { - @observable name: string = ''; + @observable name: string = ""; componentDidMount() { this.props.apiKeys.fetchPage({ limit: 100 }); @@ -31,7 +31,7 @@ class Tokens extends React.Component { handleSubmit = async (ev: SyntheticEvent<>) => { ev.preventDefault(); await this.props.apiKeys.create({ name: this.name }); - this.name = ''; + this.name = ""; }; render() { @@ -45,7 +45,7 @@ class Tokens extends React.Component { You can create an unlimited amount of personal tokens to authenticate - with the API. For more details about the API take a look at the{' '} + with the API. For more details about the API take a look at the{" "} developer documentation. @@ -79,4 +79,4 @@ class Tokens extends React.Component { } } -export default inject('apiKeys')(Tokens); +export default inject("apiKeys")(Tokens); diff --git a/app/scenes/Settings/Zapier.js b/app/scenes/Settings/Zapier.js index ff047bf8..b9011867 100644 --- a/app/scenes/Settings/Zapier.js +++ b/app/scenes/Settings/Zapier.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import CenteredContent from 'components/CenteredContent'; -import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; -import Button from 'components/Button'; +import * as React from "react"; +import CenteredContent from "components/CenteredContent"; +import PageTitle from "components/PageTitle"; +import HelpText from "components/HelpText"; +import Button from "components/Button"; class Zapier extends React.Component<*> { goToZapier = () => { window.open( - 'https://zapier.com/platform/public-invite/5927/a0b2747dbb017723b55fc54f4f0cdcae/' + "https://zapier.com/platform/public-invite/5927/a0b2747dbb017723b55fc54f4f0cdcae/" ); }; render() { @@ -17,14 +17,14 @@ class Zapier extends React.Component<*> {

    Zapier

    - There is now an Outline app on{' '} + There is now an Outline app on{" "} Zapier - {' '} + {" "} to allow easy integration with hundreds of other business services. It is currently in early access, to use the integration and hook up to your wiki simply accept the public invite below. All configuration is diff --git a/app/scenes/Settings/components/EventListItem.js b/app/scenes/Settings/components/EventListItem.js index a230e538..08abbffb 100644 --- a/app/scenes/Settings/components/EventListItem.js +++ b/app/scenes/Settings/components/EventListItem.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { capitalize } from 'lodash'; -import styled from 'styled-components'; -import Time from 'shared/components/Time'; -import ListItem from 'components/List/Item'; -import Avatar from 'components/Avatar'; -import Event from 'models/Event'; +import * as React from "react"; +import { Link } from "react-router-dom"; +import { capitalize } from "lodash"; +import styled from "styled-components"; +import Time from "shared/components/Time"; +import ListItem from "components/List/Item"; +import Avatar from "components/Avatar"; +import Event from "models/Event"; type Props = { event: Event, @@ -14,104 +14,104 @@ type Props = { const description = event => { switch (event.name) { - case 'api_keys.create': + case "api_keys.create": return ( Created the API token {event.data.name} ); - case 'api_keys.delete': + case "api_keys.delete": return ( Revoked the API token {event.data.name} ); - case 'teams.create': - return 'Created the team'; - case 'shares.create': - case 'shares.revoke': + case "teams.create": + return "Created the team"; + case "shares.create": + case "shares.revoke": return ( - {capitalize(event.verbPastTense)} a{' '} - public link to the{' '} - {event.data.name}{' '} + {capitalize(event.verbPastTense)} a{" "} + public link to the{" "} + {event.data.name}{" "} document ); - case 'users.create': + case "users.create": return ( {event.data.name} created an account ); - case 'users.invite': + case "users.invite": return ( {capitalize(event.verbPastTense)} {event.data.name} ( - {event.data.email || ''} + {event.data.email || ""} ) ); - case 'users.suspend': + case "users.suspend": return ( Suspended {event.data.name}’s account ); - case 'users.activate': + case "users.activate": return ( Unsuspended {event.data.name}’s account ); - case 'users.promote': + case "users.promote": return ( Made {event.data.name} an admin ); - case 'users.demote': + case "users.demote": return ( Made {event.data.name} a member ); - case 'users.delete': - return 'Deleted their account'; - case 'groups.create': + case "users.delete": + return "Deleted their account"; + case "groups.create": return ( Created the group {event.data.name} ); - case 'groups.update': + case "groups.update": return ( Update the group {event.data.name} ); - case 'groups.delete': + case "groups.delete": return ( Deleted the group {event.data.name} ); - case 'collections.add_user': - case 'collections.add_group': + case "collections.add_user": + case "collections.add_group": return ( - Granted {event.data.name} access to a{' '} - + Granted {event.data.name} access to a{" "} + collection ); - case 'collections.remove_user': - case 'collections.remove_group': + case "collections.remove_user": + case "collections.remove_group": return ( - Revoked {event.data.name} access to a{' '} - + Revoked {event.data.name} access to a{" "} + collection @@ -120,7 +120,7 @@ const description = event => { } if (event.documentId) { - if (event.name === 'documents.delete') { + if (event.name === "documents.delete") { return ( Deleted the {event.data.title} document @@ -129,13 +129,13 @@ const description = event => { } return ( - {capitalize(event.verbPastTense)} the{' '} + {capitalize(event.verbPastTense)} the{" "} {event.data.title} document ); } if (event.collectionId) { - if (event.name === 'collections.delete') { + if (event.name === "collections.delete") { return ( Deleted the {event.data.name} collection @@ -144,10 +144,10 @@ const description = event => { } return ( - {capitalize(event.verbPastTense)} the{' '} - + {capitalize(event.verbPastTense)} the{" "} + {event.data.name} - {' '} + {" "} collection ); @@ -159,7 +159,7 @@ const description = event => { ); } - return ''; + return ""; }; const EventListItem = ({ event }: Props) => { @@ -170,7 +170,7 @@ const EventListItem = ({ event }: Props) => { image={} subtitle={ - {description(event)} } @@ -193,7 +193,7 @@ const EventListItem = ({ event }: Props) => { ); }; -const IP = styled('span')` +const IP = styled("span")` color: ${props => props.theme.textTertiary}; font-size: 12px; `; diff --git a/app/scenes/Settings/components/ImageUpload.js b/app/scenes/Settings/components/ImageUpload.js index 47b1069c..ac64d195 100644 --- a/app/scenes/Settings/components/ImageUpload.js +++ b/app/scenes/Settings/components/ImageUpload.js @@ -1,16 +1,16 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { observer, inject } from 'mobx-react'; -import styled from 'styled-components'; -import Dropzone from 'react-dropzone'; -import LoadingIndicator from 'components/LoadingIndicator'; -import Flex from 'shared/components/Flex'; -import Modal from 'components/Modal'; -import Button from 'components/Button'; -import AvatarEditor from 'react-avatar-editor'; -import { uploadFile, dataUrlToBlob } from 'utils/uploadFile'; -import UiStore from 'stores/UiStore'; +import * as React from "react"; +import { observable } from "mobx"; +import { observer, inject } from "mobx-react"; +import styled from "styled-components"; +import Dropzone from "react-dropzone"; +import LoadingIndicator from "components/LoadingIndicator"; +import Flex from "shared/components/Flex"; +import Modal from "components/Modal"; +import Button from "components/Button"; +import AvatarEditor from "react-avatar-editor"; +import { uploadFile, dataUrlToBlob } from "utils/uploadFile"; +import UiStore from "stores/UiStore"; type Props = { children?: React.Node, @@ -30,7 +30,7 @@ class ImageUpload extends React.Component { avatarEditorRef: AvatarEditor; static defaultProps = { - submitText: 'Crop Picture', + submitText: "Crop Picture", borderRadius: 150, }; @@ -92,7 +92,7 @@ class ImageUpload extends React.Component { border={25} borderRadius={this.props.borderRadius} color={ - ui.theme === 'light' ? [255, 255, 255, 0.6] : [0, 0, 0, 0.6] + ui.theme === "light" ? [255, 255, 255, 0.6] : [0, 0, 0, 0.6] } // RGBA scale={this.zoom} rotate={0} @@ -107,7 +107,7 @@ class ImageUpload extends React.Component { onChange={this.handleZoom} /> - {this.isUploading ? 'Uploading…' : submitText} + {this.isUploading ? "Uploading…" : submitText} @@ -167,4 +167,4 @@ const CropButton = styled(Button)` width: 300px; `; -export default inject('ui')(ImageUpload); +export default inject("ui")(ImageUpload); diff --git a/app/scenes/Settings/components/NotificationListItem.js b/app/scenes/Settings/components/NotificationListItem.js index 60e41fac..bb5ee980 100644 --- a/app/scenes/Settings/components/NotificationListItem.js +++ b/app/scenes/Settings/components/NotificationListItem.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import Checkbox from 'components/Checkbox'; -import NotificationSetting from 'models/NotificationSetting'; +import * as React from "react"; +import Checkbox from "components/Checkbox"; +import NotificationSetting from "models/NotificationSetting"; type Props = { setting?: NotificationSetting, diff --git a/app/scenes/Settings/components/ShareListItem.js b/app/scenes/Settings/components/ShareListItem.js index d0b7e271..1f054aec 100644 --- a/app/scenes/Settings/components/ShareListItem.js +++ b/app/scenes/Settings/components/ShareListItem.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import ShareMenu from 'menus/ShareMenu'; -import ListItem from 'components/List/Item'; -import Time from 'shared/components/Time'; -import Share from 'models/Share'; +import * as React from "react"; +import ShareMenu from "menus/ShareMenu"; +import ListItem from "components/List/Item"; +import Time from "shared/components/Time"; +import Share from "models/Share"; type Props = { share: Share, @@ -16,7 +16,7 @@ const ShareListItem = ({ share }: Props) => { title={share.documentTitle} subtitle={ - Shared } diff --git a/app/scenes/Settings/components/SlackButton.js b/app/scenes/Settings/components/SlackButton.js index 13f72dcc..d471f2a9 100644 --- a/app/scenes/Settings/components/SlackButton.js +++ b/app/scenes/Settings/components/SlackButton.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { slackAuth } from 'shared/utils/routeHelpers'; -import SlackLogo from 'shared/components/SlackLogo'; -import Button from 'components/Button'; +import * as React from "react"; +import styled from "styled-components"; +import { slackAuth } from "shared/utils/routeHelpers"; +import SlackLogo from "shared/components/SlackLogo"; +import Button from "components/Button"; type Props = { scopes?: string[], diff --git a/app/scenes/Settings/components/TokenListItem.js b/app/scenes/Settings/components/TokenListItem.js index d89e58b1..2f7df3a8 100644 --- a/app/scenes/Settings/components/TokenListItem.js +++ b/app/scenes/Settings/components/TokenListItem.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import Button from 'components/Button'; -import ListItem from 'components/List/Item'; -import ApiKey from 'models/ApiKey'; +import * as React from "react"; +import Button from "components/Button"; +import ListItem from "components/List/Item"; +import ApiKey from "models/ApiKey"; type Props = { token: ApiKey, diff --git a/app/scenes/Settings/components/UserListItem.js b/app/scenes/Settings/components/UserListItem.js index 8069d331..a049e8c8 100644 --- a/app/scenes/Settings/components/UserListItem.js +++ b/app/scenes/Settings/components/UserListItem.js @@ -1,15 +1,15 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import { observable } from 'mobx'; -import { observer } from 'mobx-react'; -import UserMenu from 'menus/UserMenu'; -import Avatar from 'components/Avatar'; -import Badge from 'components/Badge'; -import UserProfile from 'scenes/UserProfile'; -import ListItem from 'components/List/Item'; -import Time from 'shared/components/Time'; -import User from 'models/User'; +import * as React from "react"; +import styled from "styled-components"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import UserMenu from "menus/UserMenu"; +import Avatar from "components/Avatar"; +import Badge from "components/Badge"; +import UserProfile from "scenes/UserProfile"; +import ListItem from "components/List/Item"; +import Time from "shared/components/Time"; +import User from "models/User"; type Props = { user: User, @@ -56,7 +56,7 @@ class UserListItem extends React.Component { Active ) : ( - 'Invited' + "Invited" )} {user.isAdmin && Admin} {user.isSuspended && Suspended} diff --git a/app/scenes/Settings/index.js b/app/scenes/Settings/index.js index c1e3920e..c8f34287 100644 --- a/app/scenes/Settings/index.js +++ b/app/scenes/Settings/index.js @@ -1,3 +1,3 @@ // @flow -import Profile from './Profile'; +import Profile from "./Profile"; export default Profile; diff --git a/app/scenes/Starred.js b/app/scenes/Starred.js index 4aaaa679..8e808353 100644 --- a/app/scenes/Starred.js +++ b/app/scenes/Starred.js @@ -1,18 +1,18 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; -import CenteredContent from 'components/CenteredContent'; -import Empty from 'components/Empty'; -import PageTitle from 'components/PageTitle'; -import Heading from 'components/Heading'; -import PaginatedDocumentList from 'components/PaginatedDocumentList'; -import InputSearch from 'components/InputSearch'; -import Tabs from 'components/Tabs'; -import Tab from 'components/Tab'; -import NewDocumentMenu from 'menus/NewDocumentMenu'; -import Actions, { Action } from 'components/Actions'; -import DocumentsStore from 'stores/DocumentsStore'; +import CenteredContent from "components/CenteredContent"; +import Empty from "components/Empty"; +import PageTitle from "components/PageTitle"; +import Heading from "components/Heading"; +import PaginatedDocumentList from "components/PaginatedDocumentList"; +import InputSearch from "components/InputSearch"; +import Tabs from "components/Tabs"; +import Tab from "components/Tab"; +import NewDocumentMenu from "menus/NewDocumentMenu"; +import Actions, { Action } from "components/Actions"; +import DocumentsStore from "stores/DocumentsStore"; type Props = { documents: DocumentsStore, @@ -42,7 +42,7 @@ class Starred extends React.Component { } empty={You’ve not starred any documents yet.} fetch={fetchStarred} - documents={sort === 'alphabetical' ? starredAlphabetical : starred} + documents={sort === "alphabetical" ? starredAlphabetical : starred} showCollection /> @@ -59,4 +59,4 @@ class Starred extends React.Component { } } -export default inject('documents')(Starred); +export default inject("documents")(Starred); diff --git a/app/scenes/Trash.js b/app/scenes/Trash.js index 931b3a5a..31ecd60d 100644 --- a/app/scenes/Trash.js +++ b/app/scenes/Trash.js @@ -1,14 +1,14 @@ // @flow -import * as React from 'react'; -import { observer, inject } from 'mobx-react'; +import * as React from "react"; +import { observer, inject } from "mobx-react"; -import CenteredContent from 'components/CenteredContent'; -import Empty from 'components/Empty'; -import PageTitle from 'components/PageTitle'; -import Heading from 'components/Heading'; -import PaginatedDocumentList from 'components/PaginatedDocumentList'; -import Subheading from 'components/Subheading'; -import DocumentsStore from 'stores/DocumentsStore'; +import CenteredContent from "components/CenteredContent"; +import Empty from "components/Empty"; +import PageTitle from "components/PageTitle"; +import Heading from "components/Heading"; +import PaginatedDocumentList from "components/PaginatedDocumentList"; +import Subheading from "components/Subheading"; +import DocumentsStore from "stores/DocumentsStore"; type Props = { documents: DocumentsStore, @@ -35,4 +35,4 @@ class Trash extends React.Component { } } -export default inject('documents')(Trash); +export default inject("documents")(Trash); diff --git a/app/scenes/UserDelete.js b/app/scenes/UserDelete.js index 41ac9e95..7309b81e 100644 --- a/app/scenes/UserDelete.js +++ b/app/scenes/UserDelete.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import { observable } from 'mobx'; -import { inject, observer } from 'mobx-react'; -import Button from 'components/Button'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import Modal from 'components/Modal'; -import AuthStore from 'stores/AuthStore'; +import * as React from "react"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import Button from "components/Button"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import Modal from "components/Modal"; +import AuthStore from "stores/AuthStore"; type Props = { auth: AuthStore, @@ -47,7 +47,7 @@ class UserDelete extends React.Component { be automatically reprovisioned. @@ -56,4 +56,4 @@ class UserDelete extends React.Component { } } -export default inject('auth')(UserDelete); +export default inject("auth")(UserDelete); diff --git a/app/scenes/UserProfile.js b/app/scenes/UserProfile.js index 010bed5c..7ce72313 100644 --- a/app/scenes/UserProfile.js +++ b/app/scenes/UserProfile.js @@ -1,22 +1,22 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; -import { inject, observer } from 'mobx-react'; -import { withRouter, type RouterHistory } from 'react-router-dom'; -import { EditIcon } from 'outline-icons'; -import Flex from 'shared/components/Flex'; -import HelpText from 'components/HelpText'; -import Modal from 'components/Modal'; -import Button from 'components/Button'; -import Avatar from 'components/Avatar'; -import Badge from 'components/Badge'; -import PaginatedDocumentList from 'components/PaginatedDocumentList'; -import Subheading from 'components/Subheading'; -import User from 'models/User'; -import DocumentsStore from 'stores/DocumentsStore'; -import AuthStore from 'stores/AuthStore'; -import { settings } from 'shared/utils/routeHelpers'; +import * as React from "react"; +import styled from "styled-components"; +import distanceInWordsToNow from "date-fns/distance_in_words_to_now"; +import { inject, observer } from "mobx-react"; +import { withRouter, type RouterHistory } from "react-router-dom"; +import { EditIcon } from "outline-icons"; +import Flex from "shared/components/Flex"; +import HelpText from "components/HelpText"; +import Modal from "components/Modal"; +import Button from "components/Button"; +import Avatar from "components/Avatar"; +import Badge from "components/Badge"; +import PaginatedDocumentList from "components/PaginatedDocumentList"; +import Subheading from "components/Subheading"; +import User from "models/User"; +import DocumentsStore from "stores/DocumentsStore"; +import AuthStore from "stores/AuthStore"; +import { settings } from "shared/utils/routeHelpers"; type Props = { user: User, @@ -46,8 +46,8 @@ class UserProfile extends React.Component { {isCurrentUser - ? 'You joined' - : user.lastActiveAt ? 'Joined' : 'Invited'}{' '} + ? "You joined" + : user.lastActiveAt ? "Joined" : "Invited"}{" "} {distanceInWordsToNow(new Date(user.createdAt))} ago. {user.isAdmin && ( Admin @@ -96,4 +96,4 @@ const Meta = styled(HelpText)` margin-top: -12px; `; -export default inject('documents', 'auth')(withRouter(UserProfile)); +export default inject("documents", "auth")(withRouter(UserProfile)); diff --git a/app/stores/ApiKeysStore.js b/app/stores/ApiKeysStore.js index 865eeb13..a9bc4f35 100644 --- a/app/stores/ApiKeysStore.js +++ b/app/stores/ApiKeysStore.js @@ -1,10 +1,10 @@ // @flow -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import ApiKey from 'models/ApiKey'; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import ApiKey from "models/ApiKey"; export default class ApiKeysStore extends BaseStore { - actions = ['list', 'create', 'delete']; + actions = ["list", "create", "delete"]; constructor(rootStore: RootStore) { super(rootStore, ApiKey); diff --git a/app/stores/AuthStore.js b/app/stores/AuthStore.js index 6586b290..a0e9afdb 100644 --- a/app/stores/AuthStore.js +++ b/app/stores/AuthStore.js @@ -1,14 +1,14 @@ // @flow -import { observable, action, computed, autorun, runInAction } from 'mobx'; -import invariant from 'invariant'; -import { getCookie, setCookie, removeCookie } from 'tiny-cookie'; -import { client } from 'utils/ApiClient'; -import { getCookieDomain } from 'shared/utils/domains'; -import RootStore from 'stores/RootStore'; -import User from 'models/User'; -import Team from 'models/Team'; +import { observable, action, computed, autorun, runInAction } from "mobx"; +import invariant from "invariant"; +import { getCookie, setCookie, removeCookie } from "tiny-cookie"; +import { client } from "utils/ApiClient"; +import { getCookieDomain } from "shared/utils/domains"; +import RootStore from "stores/RootStore"; +import User from "models/User"; +import Team from "models/Team"; -const AUTH_STORE = 'AUTH_STORE'; +const AUTH_STORE = "AUTH_STORE"; export default class AuthStore { @observable user: ?User; @@ -23,7 +23,7 @@ export default class AuthStore { // Rehydrate let data = {}; try { - data = JSON.parse(localStorage.getItem(AUTH_STORE) || '{}'); + data = JSON.parse(localStorage.getItem(AUTH_STORE) || "{}"); } catch (_) { // no-op Safari private mode } @@ -31,7 +31,7 @@ export default class AuthStore { this.rootStore = rootStore; this.user = new User(data.user); this.team = new Team(data.team); - this.token = getCookie('accessToken'); + this.token = getCookie("accessToken"); if (this.token) setImmediate(() => this.fetch()); @@ -66,10 +66,10 @@ export default class AuthStore { @action fetch = async () => { try { - const res = await client.post('/auth.info'); - invariant(res && res.data, 'Auth not available'); + const res = await client.post("/auth.info"); + invariant(res && res.data, "Auth not available"); - runInAction('AuthStore#fetch', () => { + runInAction("AuthStore#fetch", () => { this.addPolicies(res.policies); const { user, team } = res.data; this.user = new User(user); @@ -78,20 +78,20 @@ export default class AuthStore { if (window.Sentry) { Sentry.configureScope(function(scope) { scope.setUser({ id: user.id }); - scope.setExtra('team', team.name); - scope.setExtra('teamId', team.id); + scope.setExtra("team", team.name); + scope.setExtra("teamId", team.id); }); } // If we came from a redirect then send the user immediately there - const postLoginRedirectPath = getCookie('postLoginRedirectPath'); + const postLoginRedirectPath = getCookie("postLoginRedirectPath"); if (postLoginRedirectPath) { - removeCookie('postLoginRedirectPath'); + removeCookie("postLoginRedirectPath"); window.location.href = postLoginRedirectPath; } }); } catch (err) { - if (err.error === 'user_suspended') { + if (err.error === "user_suspended") { this.isSuspended = true; this.suspendedContactEmail = err.data.adminEmail; } @@ -102,7 +102,7 @@ export default class AuthStore { deleteUser = async () => { await client.post(`/users.delete`, { confirmation: true }); - runInAction('AuthStore#updateUser', () => { + runInAction("AuthStore#updateUser", () => { this.user = null; this.team = null; this.token = null; @@ -115,9 +115,9 @@ export default class AuthStore { try { const res = await client.post(`/users.update`, params); - invariant(res && res.data, 'User response not available'); + invariant(res && res.data, "User response not available"); - runInAction('AuthStore#updateUser', () => { + runInAction("AuthStore#updateUser", () => { this.addPolicies(res.policies); this.user = res.data; }); @@ -136,9 +136,9 @@ export default class AuthStore { try { const res = await client.post(`/team.update`, params); - invariant(res && res.data, 'Team response not available'); + invariant(res && res.data, "Team response not available"); - runInAction('AuthStore#updateTeam', () => { + runInAction("AuthStore#updateTeam", () => { this.addPolicies(res.policies); this.team = new Team(res.data); }); @@ -161,19 +161,19 @@ export default class AuthStore { // if this logout was forced from an authenticated route then // save the current path so we can go back there once signed in if (savePath) { - setCookie('postLoginRedirectPath', window.location.pathname); + setCookie("postLoginRedirectPath", window.location.pathname); } // remove authentication token itself - removeCookie('accessToken', { path: '/' }); + removeCookie("accessToken", { path: "/" }); // remove session record on apex cookie const team = this.team; if (team) { - const sessions = JSON.parse(getCookie('sessions') || '{}'); + const sessions = JSON.parse(getCookie("sessions") || "{}"); delete sessions[team.id]; - setCookie('sessions', JSON.stringify(sessions), { + setCookie("sessions", JSON.stringify(sessions), { domain: getCookieDomain(window.location.hostname), }); this.team = null; diff --git a/app/stores/BaseStore.js b/app/stores/BaseStore.js index 20345ffc..36dd6c9e 100644 --- a/app/stores/BaseStore.js +++ b/app/stores/BaseStore.js @@ -1,13 +1,13 @@ // @flow -import invariant from 'invariant'; -import { observable, set, action, computed, runInAction } from 'mobx'; -import { orderBy } from 'lodash'; -import { client } from 'utils/ApiClient'; -import RootStore from 'stores/RootStore'; -import BaseModel from '../models/BaseModel'; -import type { PaginationParams } from 'types'; +import invariant from "invariant"; +import { observable, set, action, computed, runInAction } from "mobx"; +import { orderBy } from "lodash"; +import { client } from "utils/ApiClient"; +import RootStore from "stores/RootStore"; +import BaseModel from "../models/BaseModel"; +import type { PaginationParams } from "types"; -type Action = 'list' | 'info' | 'create' | 'update' | 'delete'; +type Action = "list" | "info" | "create" | "update" | "delete"; function modelNameFromClassName(string) { return string.charAt(0).toLowerCase() + string.slice(1); @@ -24,7 +24,7 @@ export default class BaseStore { model: Class; modelName: string; rootStore: RootStore; - actions: Action[] = ['list', 'info', 'create', 'update', 'delete']; + actions: Action[] = ["list", "info", "create", "update", "delete"]; constructor(rootStore: RootStore, model: Class) { this.rootStore = rootStore; @@ -77,7 +77,7 @@ export default class BaseStore { @action async create(params: Object) { - if (!this.actions.includes('create')) { + if (!this.actions.includes("create")) { throw new Error(`Cannot create ${this.modelName}`); } this.isSaving = true; @@ -85,7 +85,7 @@ export default class BaseStore { try { const res = await client.post(`/${this.modelName}s.create`, params); - invariant(res && res.data, 'Data should be available'); + invariant(res && res.data, "Data should be available"); this.addPolicies(res.policies); return this.add(res.data); @@ -96,7 +96,7 @@ export default class BaseStore { @action async update(params: Object): * { - if (!this.actions.includes('update')) { + if (!this.actions.includes("update")) { throw new Error(`Cannot update ${this.modelName}`); } this.isSaving = true; @@ -104,7 +104,7 @@ export default class BaseStore { try { const res = await client.post(`/${this.modelName}s.update`, params); - invariant(res && res.data, 'Data should be available'); + invariant(res && res.data, "Data should be available"); this.addPolicies(res.policies); return this.add(res.data); @@ -115,7 +115,7 @@ export default class BaseStore { @action async delete(item: T, options?: Object = {}) { - if (!this.actions.includes('delete')) { + if (!this.actions.includes("delete")) { throw new Error(`Cannot delete ${this.modelName}`); } this.isSaving = true; @@ -133,7 +133,7 @@ export default class BaseStore { @action async fetch(id: string, options?: Object = {}): Promise<*> { - if (!this.actions.includes('info')) { + if (!this.actions.includes("info")) { throw new Error(`Cannot fetch ${this.modelName}`); } @@ -144,7 +144,7 @@ export default class BaseStore { try { const res = await client.post(`/${this.modelName}s.info`, { id }); - invariant(res && res.data, 'Data should be available'); + invariant(res && res.data, "Data should be available"); this.addPolicies(res.policies); return this.add(res.data); @@ -160,7 +160,7 @@ export default class BaseStore { @action fetchPage = async (params: ?PaginationParams): Promise<*> => { - if (!this.actions.includes('list')) { + if (!this.actions.includes("list")) { throw new Error(`Cannot list ${this.modelName}`); } this.isFetching = true; @@ -168,7 +168,7 @@ export default class BaseStore { try { const res = await client.post(`/${this.modelName}s.list`, params); - invariant(res && res.data, 'Data not available'); + invariant(res && res.data, "Data not available"); runInAction(`list#${this.modelName}`, () => { this.addPolicies(res.policies); @@ -183,6 +183,6 @@ export default class BaseStore { @computed get orderedData(): T[] { - return orderBy(Array.from(this.data.values()), 'createdAt', 'desc'); + return orderBy(Array.from(this.data.values()), "createdAt", "desc"); } } diff --git a/app/stores/CollectionGroupMembershipsStore.js b/app/stores/CollectionGroupMembershipsStore.js index 10b58de8..b55851ed 100644 --- a/app/stores/CollectionGroupMembershipsStore.js +++ b/app/stores/CollectionGroupMembershipsStore.js @@ -1,16 +1,16 @@ // @flow -import invariant from 'invariant'; -import { action, runInAction } from 'mobx'; -import { client } from 'utils/ApiClient'; -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import CollectionGroupMembership from 'models/CollectionGroupMembership'; -import type { PaginationParams } from 'types'; +import invariant from "invariant"; +import { action, runInAction } from "mobx"; +import { client } from "utils/ApiClient"; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import CollectionGroupMembership from "models/CollectionGroupMembership"; +import type { PaginationParams } from "types"; export default class CollectionGroupMembershipsStore extends BaseStore< CollectionGroupMembership > { - actions = ['create', 'delete']; + actions = ["create", "delete"]; constructor(rootStore: RootStore) { super(rootStore, CollectionGroupMembership); @@ -23,7 +23,7 @@ export default class CollectionGroupMembershipsStore extends BaseStore< try { const res = await client.post(`/collections.group_memberships`, params); - invariant(res && res.data, 'Data not available'); + invariant(res && res.data, "Data not available"); runInAction(`CollectionGroupMembershipsStore#fetchPage`, () => { res.data.groups.forEach(this.rootStore.groups.add); @@ -46,12 +46,12 @@ export default class CollectionGroupMembershipsStore extends BaseStore< groupId: string, permission: string, }) { - const res = await client.post('/collections.add_group', { + const res = await client.post("/collections.add_group", { id: collectionId, groupId, permission, }); - invariant(res && res.data, 'Membership data should be available'); + invariant(res && res.data, "Membership data should be available"); res.data.collectionGroupMemberships.forEach(this.add); } @@ -64,7 +64,7 @@ export default class CollectionGroupMembershipsStore extends BaseStore< collectionId: string, groupId: string, }) { - await client.post('/collections.remove_group', { + await client.post("/collections.remove_group", { id: collectionId, groupId, }); diff --git a/app/stores/CollectionsStore.js b/app/stores/CollectionsStore.js index d08ae3bb..9684047a 100644 --- a/app/stores/CollectionsStore.js +++ b/app/stores/CollectionsStore.js @@ -1,19 +1,19 @@ // @flow -import { computed } from 'mobx'; -import { concat, filter, last } from 'lodash'; -import { client } from 'utils/ApiClient'; +import { computed } from "mobx"; +import { concat, filter, last } from "lodash"; +import { client } from "utils/ApiClient"; -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import Collection from 'models/Collection'; -import naturalSort from 'shared/utils/naturalSort'; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import Collection from "models/Collection"; +import naturalSort from "shared/utils/naturalSort"; export type DocumentPathItem = { id: string, collectionId: string, title: string, url: string, - type: 'collection' | 'document', + type: "collection" | "document", }; export type DocumentPath = DocumentPathItem & { @@ -35,7 +35,7 @@ export default class CollectionsStore extends BaseStore { @computed get orderedData(): Collection[] { return filter( - naturalSort(Array.from(this.data.values()), 'name'), + naturalSort(Array.from(this.data.values()), "name"), d => !d.deletedAt ); } @@ -59,7 +59,7 @@ export default class CollectionsStore extends BaseStore { const travelDocuments = (documentList, collectionId, path) => documentList.forEach(document => { const { id, title, url } = document; - const node = { id, collectionId, title, url, type: 'document' }; + const node = { id, collectionId, title, url, type: "document" }; results.push(concat(path, node)); travelDocuments(document.children, collectionId, concat(path, [node])); }); @@ -72,7 +72,7 @@ export default class CollectionsStore extends BaseStore { collectionId: id, title: name, url, - type: 'collection', + type: "collection", }; results.push([node]); travelDocuments(collection.documents, id, [node]); @@ -105,6 +105,6 @@ export default class CollectionsStore extends BaseStore { } export = () => { - return client.post('/collections.export_all'); + return client.post("/collections.export_all"); }; } diff --git a/app/stores/DocumentPresenceStore.js b/app/stores/DocumentPresenceStore.js index e368a8c5..6877a909 100644 --- a/app/stores/DocumentPresenceStore.js +++ b/app/stores/DocumentPresenceStore.js @@ -1,6 +1,6 @@ // @flow -import { observable, action } from 'mobx'; -import { USER_PRESENCE_INTERVAL } from 'shared/constants'; +import { observable, action } from "mobx"; +import { USER_PRESENCE_INTERVAL } from "shared/constants"; type DocumentPresence = Map; diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index 614a840b..b0ea646c 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -1,5 +1,5 @@ // @flow -import { observable, action, computed, runInAction } from 'mobx'; +import { observable, action, computed, runInAction } from "mobx"; import { without, map, @@ -9,16 +9,16 @@ import { compact, omitBy, uniq, -} from 'lodash'; -import { client } from 'utils/ApiClient'; -import naturalSort from 'shared/utils/naturalSort'; -import invariant from 'invariant'; +} from "lodash"; +import { client } from "utils/ApiClient"; +import naturalSort from "shared/utils/naturalSort"; +import invariant from "invariant"; -import BaseStore from 'stores/BaseStore'; -import RootStore from 'stores/RootStore'; -import Document from 'models/Document'; -import Revision from 'models/Revision'; -import type { FetchOptions, PaginationParams, SearchResult } from 'types'; +import BaseStore from "stores/BaseStore"; +import RootStore from "stores/RootStore"; +import Document from "models/Document"; +import Revision from "models/Revision"; +import type { FetchOptions, PaginationParams, SearchResult } from "types"; export default class DocumentsStore extends BaseStore { @observable recentlyViewedIds: string[] = []; @@ -39,21 +39,21 @@ export default class DocumentsStore extends BaseStore { get recentlyViewed(): Document[] { return orderBy( compact(this.recentlyViewedIds.map(id => this.data.get(id))), - 'updatedAt', - 'desc' + "updatedAt", + "desc" ); } @computed get recentlyUpdated(): Document[] { - return orderBy(this.all, 'updatedAt', 'desc'); + return orderBy(this.all, "updatedAt", "desc"); } createdByUser(userId: string): Document[] { return orderBy( filter(this.all, d => d.createdBy.id === userId), - 'updatedAt', - 'desc' + "updatedAt", + "desc" ); } @@ -77,23 +77,23 @@ export default class DocumentsStore extends BaseStore { } leastRecentlyUpdatedInCollection(collectionId: string): Document[] { - return orderBy(this.inCollection(collectionId), 'updatedAt', 'asc'); + return orderBy(this.inCollection(collectionId), "updatedAt", "asc"); } recentlyUpdatedInCollection(collectionId: string): Document[] { - return orderBy(this.inCollection(collectionId), 'updatedAt', 'desc'); + return orderBy(this.inCollection(collectionId), "updatedAt", "desc"); } recentlyPublishedInCollection(collectionId: string): Document[] { return orderBy( this.publishedInCollection(collectionId), - 'publishedAt', - 'desc' + "publishedAt", + "desc" ); } alphabeticalInCollection(collectionId: string): Document[] { - return naturalSort(this.inCollection(collectionId), 'title'); + return naturalSort(this.inCollection(collectionId), "title"); } searchResults(query: string): SearchResult[] { @@ -108,7 +108,7 @@ export default class DocumentsStore extends BaseStore { @computed get archived(): Document[] { return filter( - orderBy(this.orderedData, 'archivedAt', 'desc'), + orderBy(this.orderedData, "archivedAt", "desc"), d => d.archivedAt && !d.deletedAt ); } @@ -116,20 +116,20 @@ export default class DocumentsStore extends BaseStore { @computed get deleted(): Document[] { return filter( - orderBy(this.orderedData, 'deletedAt', 'desc'), + orderBy(this.orderedData, "deletedAt", "desc"), d => d.deletedAt ); } @computed get starredAlphabetical(): Document[] { - return naturalSort(this.starred, 'title'); + return naturalSort(this.starred, "title"); } @computed get drafts(): Document[] { return filter( - orderBy(this.all, 'updatedAt', 'desc'), + orderBy(this.all, "updatedAt", "desc"), doc => !doc.publishedAt ); } @@ -146,9 +146,9 @@ export default class DocumentsStore extends BaseStore { const res = await client.post(`/documents.list`, { backlinkDocumentId: documentId, }); - invariant(res && res.data, 'Document list not available'); + invariant(res && res.data, "Document list not available"); const { data } = res; - runInAction('DocumentsStore#fetchBacklinks', () => { + runInAction("DocumentsStore#fetchBacklinks", () => { data.forEach(this.add); this.backlinks.set(documentId, data.map(doc => doc.id)); }); @@ -158,8 +158,8 @@ export default class DocumentsStore extends BaseStore { const documentIds = this.backlinks.get(documentId) || []; return orderBy( compact(documentIds.map(id => this.data.get(id))), - 'updatedAt', - 'desc' + "updatedAt", + "desc" ); } @@ -168,24 +168,24 @@ export default class DocumentsStore extends BaseStore { const res = await client.post(`/documents.list`, { parentDocumentId: documentId, }); - invariant(res && res.data, 'Document list not available'); + invariant(res && res.data, "Document list not available"); const { data } = res; - runInAction('DocumentsStore#fetchChildDocuments', () => { + runInAction("DocumentsStore#fetchChildDocuments", () => { data.forEach(this.add); }); }; @action fetchNamedPage = async ( - request: string = 'list', + request: string = "list", options: ?PaginationParams ): Promise => { this.isFetching = true; try { const res = await client.post(`/documents.${request}`, options); - invariant(res && res.data, 'Document list not available'); - runInAction('DocumentsStore#fetchNamedPage', () => { + invariant(res && res.data, "Document list not available"); + runInAction("DocumentsStore#fetchNamedPage", () => { res.data.forEach(this.add); this.addPolicies(res.policies); this.isLoaded = true; @@ -198,24 +198,24 @@ export default class DocumentsStore extends BaseStore { @action fetchArchived = async (options: ?PaginationParams): Promise<*> => { - return this.fetchNamedPage('archived', options); + return this.fetchNamedPage("archived", options); }; @action fetchDeleted = async (options: ?PaginationParams): Promise<*> => { - return this.fetchNamedPage('deleted', options); + return this.fetchNamedPage("deleted", options); }; @action fetchRecentlyUpdated = async (options: ?PaginationParams): Promise<*> => { - return this.fetchNamedPage('list', options); + return this.fetchNamedPage("list", options); }; @action fetchAlphabetical = async (options: ?PaginationParams): Promise<*> => { - return this.fetchNamedPage('list', { - sort: 'title', - direction: 'ASC', + return this.fetchNamedPage("list", { + sort: "title", + direction: "ASC", ...options, }); }; @@ -224,30 +224,30 @@ export default class DocumentsStore extends BaseStore { fetchLeastRecentlyUpdated = async ( options: ?PaginationParams ): Promise<*> => { - return this.fetchNamedPage('list', { - sort: 'updatedAt', - direction: 'ASC', + return this.fetchNamedPage("list", { + sort: "updatedAt", + direction: "ASC", ...options, }); }; @action fetchRecentlyPublished = async (options: ?PaginationParams): Promise<*> => { - return this.fetchNamedPage('list', { - sort: 'publishedAt', - direction: 'DESC', + return this.fetchNamedPage("list", { + sort: "publishedAt", + direction: "DESC", ...options, }); }; @action fetchRecentlyViewed = async (options: ?PaginationParams): Promise<*> => { - const data = await this.fetchNamedPage('viewed', options); + const data = await this.fetchNamedPage("viewed", options); - runInAction('DocumentsStore#fetchRecentlyViewed', () => { + runInAction("DocumentsStore#fetchRecentlyViewed", () => { // $FlowFixMe this.recentlyViewedIds.replace( - uniq(this.recentlyViewedIds.concat(map(data, 'id'))) + uniq(this.recentlyViewedIds.concat(map(data, "id"))) ); }); return data; @@ -255,22 +255,22 @@ export default class DocumentsStore extends BaseStore { @action fetchStarred = (options: ?PaginationParams): Promise<*> => { - return this.fetchNamedPage('starred', options); + return this.fetchNamedPage("starred", options); }; @action fetchDrafts = (options: ?PaginationParams): Promise<*> => { - return this.fetchNamedPage('drafts', options); + return this.fetchNamedPage("drafts", options); }; @action fetchPinned = (options: ?PaginationParams): Promise<*> => { - return this.fetchNamedPage('pinned', options); + return this.fetchNamedPage("pinned", options); }; @action fetchOwned = (options: ?PaginationParams): Promise<*> => { - return this.fetchNamedPage('list', options); + return this.fetchNamedPage("list", options); }; @action @@ -280,11 +280,11 @@ export default class DocumentsStore extends BaseStore { ): Promise => { // $FlowFixMe const compactedOptions = omitBy(options, o => !o); - const res = await client.get('/documents.search', { + const res = await client.get("/documents.search", { ...compactedOptions, query, }); - invariant(res && res.data, 'Search response should be available'); + invariant(res && res.data, "Search response should be available"); // add the documents and associated policies to the store res.data.forEach(result => this.add(result.document)); @@ -335,16 +335,16 @@ export default class DocumentsStore extends BaseStore { return doc; } - const res = await client.post('/documents.info', { + const res = await client.post("/documents.info", { id, shareId: options.shareId, }); - invariant(res && res.data, 'Document not available'); + invariant(res && res.data, "Document not available"); this.addPolicies(res.policies); this.add(res.data); - runInAction('DocumentsStore#fetch', () => { + runInAction("DocumentsStore#fetch", () => { this.isLoaded = true; }); @@ -360,12 +360,12 @@ export default class DocumentsStore extends BaseStore { collectionId: string, parentDocumentId: ?string ) => { - const res = await client.post('/documents.move', { + const res = await client.post("/documents.move", { id: document.id, collectionId, parentDocumentId, }); - invariant(res && res.data, 'Data not available'); + invariant(res && res.data, "Data not available"); res.data.documents.forEach(this.add); res.data.collections.forEach(this.rootStore.collections.add); @@ -373,14 +373,14 @@ export default class DocumentsStore extends BaseStore { @action duplicate = async (document: Document): * => { - const res = await client.post('/documents.create', { + const res = await client.post("/documents.create", { publish: !!document.publishedAt, parentDocumentId: document.parentDocumentId, collectionId: document.collectionId, title: `${document.title} (duplicate)`, text: document.text, }); - invariant(res && res.data, 'Data should be available'); + invariant(res && res.data, "Data should be available"); const collection = this.getCollectionForDocument(document); if (collection) collection.refresh(); @@ -439,11 +439,11 @@ export default class DocumentsStore extends BaseStore { @action archive = async (document: Document) => { - const res = await client.post('/documents.archive', { + const res = await client.post("/documents.archive", { id: document.id, }); - runInAction('Document#archive', () => { - invariant(res && res.data, 'Data should be available'); + runInAction("Document#archive", () => { + invariant(res && res.data, "Data should be available"); document.updateFromJson(res.data); this.addPolicies(res.policies); }); @@ -454,12 +454,12 @@ export default class DocumentsStore extends BaseStore { @action restore = async (document: Document, revision?: Revision) => { - const res = await client.post('/documents.restore', { + const res = await client.post("/documents.restore", { id: document.id, revisionId: revision ? revision.id : undefined, }); - runInAction('Document#restore', () => { - invariant(res && res.data, 'Data should be available'); + runInAction("Document#restore", () => { + invariant(res && res.data, "Data should be available"); document.updateFromJson(res.data); this.addPolicies(res.policies); }); @@ -469,18 +469,18 @@ export default class DocumentsStore extends BaseStore { }; pin = (document: Document) => { - return client.post('/documents.pin', { id: document.id }); + return client.post("/documents.pin", { id: document.id }); }; unpin = (document: Document) => { - return client.post('/documents.unpin', { id: document.id }); + return client.post("/documents.unpin", { id: document.id }); }; star = async (document: Document) => { this.starredIds.set(document.id, true); try { - return client.post('/documents.star', { id: document.id }); + return client.post("/documents.star", { id: document.id }); } catch (err) { this.starredIds.set(document.id, false); } @@ -490,13 +490,13 @@ export default class DocumentsStore extends BaseStore { this.starredIds.set(document.id, false); try { - return client.post('/documents.unstar', { id: document.id }); + return client.post("/documents.unstar", { id: document.id }); } catch (err) { this.starredIds.set(document.id, false); } }; - getByUrl = (url: string = ''): ?Document => { + getByUrl = (url: string = ""): ?Document => { return find(this.orderedData, doc => url.endsWith(doc.urlId)); }; diff --git a/app/stores/EventsStore.js b/app/stores/EventsStore.js index 0b967b23..a1ec5606 100644 --- a/app/stores/EventsStore.js +++ b/app/stores/EventsStore.js @@ -1,12 +1,12 @@ // @flow -import { sortBy } from 'lodash'; -import { computed } from 'mobx'; -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import Event from 'models/Event'; +import { sortBy } from "lodash"; +import { computed } from "mobx"; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import Event from "models/Event"; export default class EventsStore extends BaseStore { - actions = ['list']; + actions = ["list"]; constructor(rootStore: RootStore) { super(rootStore, Event); @@ -14,6 +14,6 @@ export default class EventsStore extends BaseStore { @computed get orderedData(): Event[] { - return sortBy(Array.from(this.data.values()), 'createdAt').reverse(); + return sortBy(Array.from(this.data.values()), "createdAt").reverse(); } } diff --git a/app/stores/GroupMembershipsStore.js b/app/stores/GroupMembershipsStore.js index faa3b2ed..ee3c794d 100644 --- a/app/stores/GroupMembershipsStore.js +++ b/app/stores/GroupMembershipsStore.js @@ -1,15 +1,15 @@ // @flow -import invariant from 'invariant'; -import { action, runInAction } from 'mobx'; -import { filter } from 'lodash'; -import { client } from 'utils/ApiClient'; -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import GroupMembership from 'models/GroupMembership'; -import type { PaginationParams } from 'types'; +import invariant from "invariant"; +import { action, runInAction } from "mobx"; +import { filter } from "lodash"; +import { client } from "utils/ApiClient"; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import GroupMembership from "models/GroupMembership"; +import type { PaginationParams } from "types"; export default class GroupMembershipsStore extends BaseStore { - actions = ['create', 'delete']; + actions = ["create", "delete"]; constructor(rootStore: RootStore) { super(rootStore, GroupMembership); @@ -22,7 +22,7 @@ export default class GroupMembershipsStore extends BaseStore { try { const res = await client.post(`/groups.memberships`, params); - invariant(res && res.data, 'Data not available'); + invariant(res && res.data, "Data not available"); runInAction(`GroupMembershipsStore#fetchPage`, () => { res.data.users.forEach(this.rootStore.users.add); @@ -37,11 +37,11 @@ export default class GroupMembershipsStore extends BaseStore { @action async create({ groupId, userId }: { groupId: string, userId: string }) { - const res = await client.post('/groups.add_user', { + const res = await client.post("/groups.add_user", { id: groupId, userId, }); - invariant(res && res.data, 'Group Membership data should be available'); + invariant(res && res.data, "Group Membership data should be available"); res.data.users.forEach(this.rootStore.users.add); res.data.groups.forEach(this.rootStore.groups.add); @@ -50,11 +50,11 @@ export default class GroupMembershipsStore extends BaseStore { @action async delete({ groupId, userId }: { groupId: string, userId: string }) { - const res = await client.post('/groups.remove_user', { + const res = await client.post("/groups.remove_user", { id: groupId, userId, }); - invariant(res && res.data, 'Group Membership data should be available'); + invariant(res && res.data, "Group Membership data should be available"); this.remove(`${userId}-${groupId}`); diff --git a/app/stores/GroupsStore.js b/app/stores/GroupsStore.js index 445b336b..7d97c7e6 100644 --- a/app/stores/GroupsStore.js +++ b/app/stores/GroupsStore.js @@ -1,13 +1,13 @@ // @flow -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import naturalSort from 'shared/utils/naturalSort'; -import Group from 'models/Group'; -import { client } from 'utils/ApiClient'; -import invariant from 'invariant'; -import { filter } from 'lodash'; -import { action, runInAction, computed } from 'mobx'; -import type { PaginationParams } from 'types'; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import naturalSort from "shared/utils/naturalSort"; +import Group from "models/Group"; +import { client } from "utils/ApiClient"; +import invariant from "invariant"; +import { filter } from "lodash"; +import { action, runInAction, computed } from "mobx"; +import type { PaginationParams } from "types"; export default class GroupsStore extends BaseStore { constructor(rootStore: RootStore) { @@ -16,7 +16,7 @@ export default class GroupsStore extends BaseStore { @computed get orderedData(): Group[] { - return naturalSort(Array.from(this.data.values()), 'name'); + return naturalSort(Array.from(this.data.values()), "name"); } @action @@ -26,7 +26,7 @@ export default class GroupsStore extends BaseStore { try { const res = await client.post(`/groups.list`, params); - invariant(res && res.data, 'Data not available'); + invariant(res && res.data, "Data not available"); runInAction(`GroupsStore#fetchPage`, () => { this.addPolicies(res.policies); @@ -54,7 +54,7 @@ export default class GroupsStore extends BaseStore { return queriedGroups(groups, query); }; - notInCollection = (collectionId: string, query: string = '') => { + notInCollection = (collectionId: string, query: string = "") => { const memberships = filter( this.rootStore.collectionGroupMemberships.orderedData, member => member.collectionId === collectionId diff --git a/app/stores/IntegrationsStore.js b/app/stores/IntegrationsStore.js index 5a2d4994..56d5068e 100644 --- a/app/stores/IntegrationsStore.js +++ b/app/stores/IntegrationsStore.js @@ -1,11 +1,11 @@ // @flow -import { computed } from 'mobx'; -import { filter } from 'lodash'; +import { computed } from "mobx"; +import { filter } from "lodash"; -import naturalSort from 'shared/utils/naturalSort'; -import BaseStore from 'stores/BaseStore'; -import RootStore from 'stores/RootStore'; -import Integration from 'models/Integration'; +import naturalSort from "shared/utils/naturalSort"; +import BaseStore from "stores/BaseStore"; +import RootStore from "stores/RootStore"; +import Integration from "models/Integration"; class IntegrationsStore extends BaseStore { constructor(rootStore: RootStore) { @@ -14,12 +14,12 @@ class IntegrationsStore extends BaseStore { @computed get orderedData(): Integration[] { - return naturalSort(Array.from(this.data.values()), 'name'); + return naturalSort(Array.from(this.data.values()), "name"); } @computed get slackIntegrations(): Integration[] { - return filter(this.orderedData, { service: 'slack' }); + return filter(this.orderedData, { service: "slack" }); } } diff --git a/app/stores/MembershipsStore.js b/app/stores/MembershipsStore.js index 3dfeaa82..2cb2b294 100644 --- a/app/stores/MembershipsStore.js +++ b/app/stores/MembershipsStore.js @@ -1,14 +1,14 @@ // @flow -import invariant from 'invariant'; -import { action, runInAction } from 'mobx'; -import { client } from 'utils/ApiClient'; -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import Membership from 'models/Membership'; -import type { PaginationParams } from 'types'; +import invariant from "invariant"; +import { action, runInAction } from "mobx"; +import { client } from "utils/ApiClient"; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import Membership from "models/Membership"; +import type { PaginationParams } from "types"; export default class MembershipsStore extends BaseStore { - actions = ['create', 'delete']; + actions = ["create", "delete"]; constructor(rootStore: RootStore) { super(rootStore, Membership); @@ -21,7 +21,7 @@ export default class MembershipsStore extends BaseStore { try { const res = await client.post(`/collections.memberships`, params); - invariant(res && res.data, 'Data not available'); + invariant(res && res.data, "Data not available"); runInAction(`/collections.memberships`, () => { res.data.users.forEach(this.rootStore.users.add); @@ -44,12 +44,12 @@ export default class MembershipsStore extends BaseStore { userId: string, permission: string, }) { - const res = await client.post('/collections.add_user', { + const res = await client.post("/collections.add_user", { id: collectionId, userId, permission, }); - invariant(res && res.data, 'Membership data should be available'); + invariant(res && res.data, "Membership data should be available"); res.data.users.forEach(this.rootStore.users.add); res.data.memberships.forEach(this.add); @@ -63,7 +63,7 @@ export default class MembershipsStore extends BaseStore { collectionId: string, userId: string, }) { - await client.post('/collections.remove_user', { + await client.post("/collections.remove_user", { id: collectionId, userId, }); diff --git a/app/stores/NotificationSettingsStore.js b/app/stores/NotificationSettingsStore.js index cf5fb35d..4640a658 100644 --- a/app/stores/NotificationSettingsStore.js +++ b/app/stores/NotificationSettingsStore.js @@ -1,13 +1,13 @@ // @flow -import { find } from 'lodash'; -import NotificationSetting from 'models/NotificationSetting'; -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; +import { find } from "lodash"; +import NotificationSetting from "models/NotificationSetting"; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; export default class NotificationSettingsStore extends BaseStore< NotificationSetting > { - actions = ['list', 'create', 'delete']; + actions = ["list", "create", "delete"]; constructor(rootStore: RootStore) { super(rootStore, NotificationSetting); diff --git a/app/stores/PoliciesStore.js b/app/stores/PoliciesStore.js index 3dc2eee6..787b387c 100644 --- a/app/stores/PoliciesStore.js +++ b/app/stores/PoliciesStore.js @@ -1,7 +1,7 @@ // @flow -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import Policy from 'models/Policy'; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import Policy from "models/Policy"; export default class PoliciesStore extends BaseStore { actions = []; diff --git a/app/stores/RevisionsStore.js b/app/stores/RevisionsStore.js index 527b93b8..d1c15501 100644 --- a/app/stores/RevisionsStore.js +++ b/app/stores/RevisionsStore.js @@ -1,15 +1,15 @@ // @flow -import { action, runInAction } from 'mobx'; -import { filter } from 'lodash'; -import invariant from 'invariant'; -import { client } from 'utils/ApiClient'; -import BaseStore from 'stores/BaseStore'; -import RootStore from 'stores/RootStore'; -import Revision from 'models/Revision'; -import type { FetchOptions, PaginationParams } from 'types'; +import { action, runInAction } from "mobx"; +import { filter } from "lodash"; +import invariant from "invariant"; +import { client } from "utils/ApiClient"; +import BaseStore from "stores/BaseStore"; +import RootStore from "stores/RootStore"; +import Revision from "models/Revision"; +import type { FetchOptions, PaginationParams } from "types"; export default class RevisionsStore extends BaseStore { - actions = ['list']; + actions = ["list"]; constructor(rootStore: RootStore) { super(rootStore, Revision); @@ -22,19 +22,19 @@ export default class RevisionsStore extends BaseStore { @action fetch = async (id: string, options?: FetchOptions): Promise => { this.isFetching = true; - invariant(id, 'Id is required'); + invariant(id, "Id is required"); try { const rev = this.data.get(id); if (rev) return rev; - const res = await client.post('/revisions.info', { + const res = await client.post("/revisions.info", { id, }); - invariant(res && res.data, 'Revision not available'); + invariant(res && res.data, "Revision not available"); this.add(res.data); - runInAction('RevisionsStore#fetch', () => { + runInAction("RevisionsStore#fetch", () => { this.isLoaded = true; }); @@ -49,9 +49,9 @@ export default class RevisionsStore extends BaseStore { this.isFetching = true; try { - const res = await client.post('/revisions.list', options); - invariant(res && res.data, 'Document revisions not available'); - runInAction('RevisionsStore#fetchPage', () => { + const res = await client.post("/revisions.list", options); + invariant(res && res.data, "Document revisions not available"); + runInAction("RevisionsStore#fetchPage", () => { res.data.forEach(revision => this.add(revision)); this.isLoaded = true; }); diff --git a/app/stores/RootStore.js b/app/stores/RootStore.js index be84e78a..b92052bd 100644 --- a/app/stores/RootStore.js +++ b/app/stores/RootStore.js @@ -1,22 +1,22 @@ // @flow -import ApiKeysStore from './ApiKeysStore'; -import AuthStore from './AuthStore'; -import CollectionsStore from './CollectionsStore'; -import DocumentsStore from './DocumentsStore'; -import EventsStore from './EventsStore'; -import GroupsStore from './GroupsStore'; -import GroupMembershipsStore from './GroupMembershipsStore'; -import IntegrationsStore from './IntegrationsStore'; -import MembershipsStore from './MembershipsStore'; -import NotificationSettingsStore from './NotificationSettingsStore'; -import DocumentPresenceStore from './DocumentPresenceStore'; -import PoliciesStore from './PoliciesStore'; -import RevisionsStore from './RevisionsStore'; -import SharesStore from './SharesStore'; -import UiStore from './UiStore'; -import UsersStore from './UsersStore'; -import ViewsStore from './ViewsStore'; -import CollectionGroupMembershipsStore from './CollectionGroupMembershipsStore'; +import ApiKeysStore from "./ApiKeysStore"; +import AuthStore from "./AuthStore"; +import CollectionsStore from "./CollectionsStore"; +import DocumentsStore from "./DocumentsStore"; +import EventsStore from "./EventsStore"; +import GroupsStore from "./GroupsStore"; +import GroupMembershipsStore from "./GroupMembershipsStore"; +import IntegrationsStore from "./IntegrationsStore"; +import MembershipsStore from "./MembershipsStore"; +import NotificationSettingsStore from "./NotificationSettingsStore"; +import DocumentPresenceStore from "./DocumentPresenceStore"; +import PoliciesStore from "./PoliciesStore"; +import RevisionsStore from "./RevisionsStore"; +import SharesStore from "./SharesStore"; +import UiStore from "./UiStore"; +import UsersStore from "./UsersStore"; +import ViewsStore from "./ViewsStore"; +import CollectionGroupMembershipsStore from "./CollectionGroupMembershipsStore"; export default class RootStore { apiKeys: ApiKeysStore; diff --git a/app/stores/SharesStore.js b/app/stores/SharesStore.js index d1b04056..133a392d 100644 --- a/app/stores/SharesStore.js +++ b/app/stores/SharesStore.js @@ -1,13 +1,13 @@ // @flow -import { sortBy } from 'lodash'; -import { action, computed } from 'mobx'; -import { client } from 'utils/ApiClient'; -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import Share from 'models/Share'; +import { sortBy } from "lodash"; +import { action, computed } from "mobx"; +import { client } from "utils/ApiClient"; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import Share from "models/Share"; export default class SharesStore extends BaseStore { - actions = ['list', 'create']; + actions = ["list", "create"]; constructor(rootStore: RootStore) { super(rootStore, Share); @@ -15,12 +15,12 @@ export default class SharesStore extends BaseStore { @computed get orderedData(): Share[] { - return sortBy(Array.from(this.data.values()), 'createdAt').reverse(); + return sortBy(Array.from(this.data.values()), "createdAt").reverse(); } @action revoke = async (share: Share) => { - await client.post('/shares.revoke', { id: share.id }); + await client.post("/shares.revoke", { id: share.id }); this.remove(share.id); }; } diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index f28bda62..07fb96e3 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -1,19 +1,19 @@ // @flow -import { v4 } from 'uuid'; -import { orderBy } from 'lodash'; -import { observable, action, autorun, computed } from 'mobx'; -import Document from 'models/Document'; -import Collection from 'models/Collection'; -import type { Toast } from '../types'; +import { v4 } from "uuid"; +import { orderBy } from "lodash"; +import { observable, action, autorun, computed } from "mobx"; +import Document from "models/Document"; +import Collection from "models/Collection"; +import type { Toast } from "../types"; -const UI_STORE = 'UI_STORE'; +const UI_STORE = "UI_STORE"; class UiStore { // theme represents the users UI preference (defaults to system) - @observable theme: 'light' | 'dark' | 'system'; + @observable theme: "light" | "dark" | "system"; // systemTheme represents the system UI theme (Settings -> General in macOS) - @observable systemTheme: 'light' | 'dark'; + @observable systemTheme: "light" | "dark"; @observable activeModalName: ?string; @observable activeModalProps: ?Object; @observable activeDocumentId: ?string; @@ -28,18 +28,18 @@ class UiStore { // Rehydrate let data = {}; try { - data = JSON.parse(localStorage.getItem(UI_STORE) || '{}'); + data = JSON.parse(localStorage.getItem(UI_STORE) || "{}"); } catch (_) { // no-op Safari private mode } // system theme listeners const colorSchemeQueryList = window.matchMedia( - '(prefers-color-scheme: dark)' + "(prefers-color-scheme: dark)" ); const setSystemTheme = event => { - this.systemTheme = event.matches ? 'dark' : 'light'; + this.systemTheme = event.matches ? "dark" : "light"; }; setSystemTheme(colorSchemeQueryList); if (colorSchemeQueryList.addListener) { @@ -48,7 +48,7 @@ class UiStore { // persisted keys this.tocVisible = data.tocVisible; - this.theme = data.theme || 'system'; + this.theme = data.theme || "system"; autorun(() => { try { @@ -60,11 +60,11 @@ class UiStore { } @action - setTheme = (theme: 'light' | 'dark' | 'system') => { + setTheme = (theme: "light" | "dark" | "system") => { this.theme = theme; if (window.localStorage) { - window.localStorage.setItem('theme', this.theme); + window.localStorage.setItem("theme", this.theme); } }; @@ -149,7 +149,7 @@ class UiStore { showToast = ( message: string, options?: { - type?: 'warning' | 'error' | 'info' | 'success', + type?: "warning" | "error" | "info" | "success", timeout?: number, action?: { text: string, @@ -171,8 +171,8 @@ class UiStore { }; @computed - get resolvedTheme(): 'dark' | 'light' { - if (this.theme === 'system') { + get resolvedTheme(): "dark" | "light" { + if (this.theme === "system") { return this.systemTheme; } @@ -181,7 +181,7 @@ class UiStore { @computed get orderedToasts(): Toast[] { - return orderBy(Array.from(this.toasts.values()), 'createdAt', 'desc'); + return orderBy(Array.from(this.toasts.values()), "createdAt", "desc"); } @computed diff --git a/app/stores/UsersStore.js b/app/stores/UsersStore.js index 0ba77f76..c9790088 100644 --- a/app/stores/UsersStore.js +++ b/app/stores/UsersStore.js @@ -1,11 +1,11 @@ // @flow -import { filter, orderBy } from 'lodash'; -import { computed, action, runInAction } from 'mobx'; -import invariant from 'invariant'; -import { client } from 'utils/ApiClient'; -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import User from 'models/User'; +import { filter, orderBy } from "lodash"; +import { computed, action, runInAction } from "mobx"; +import invariant from "invariant"; +import { client } from "utils/ApiClient"; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import User from "models/User"; export default class UsersStore extends BaseStore { constructor(rootStore: RootStore) { @@ -47,40 +47,40 @@ export default class UsersStore extends BaseStore { @computed get orderedData(): User[] { - return orderBy(Array.from(this.data.values()), 'name', 'asc'); + return orderBy(Array.from(this.data.values()), "name", "asc"); } @action promote = (user: User) => { - return this.actionOnUser('promote', user); + return this.actionOnUser("promote", user); }; @action demote = (user: User) => { - return this.actionOnUser('demote', user); + return this.actionOnUser("demote", user); }; @action suspend = (user: User) => { - return this.actionOnUser('suspend', user); + return this.actionOnUser("suspend", user); }; @action activate = (user: User) => { - return this.actionOnUser('activate', user); + return this.actionOnUser("activate", user); }; @action invite = async (invites: { email: string, name: string }[]) => { const res = await client.post(`/users.invite`, { invites }); - invariant(res && res.data, 'Data should be available'); + invariant(res && res.data, "Data should be available"); runInAction(`invite`, () => { res.data.users.forEach(this.add); }); return res.data; }; - notInCollection = (collectionId: string, query: string = '') => { + notInCollection = (collectionId: string, query: string = "") => { const memberships = filter( this.rootStore.memberships.orderedData, member => member.collectionId === collectionId @@ -109,7 +109,7 @@ export default class UsersStore extends BaseStore { return queriedUsers(users, query); }; - notInGroup = (groupId: string, query: string = '') => { + notInGroup = (groupId: string, query: string = "") => { const memberships = filter( this.rootStore.groupMemberships.orderedData, member => member.groupId === groupId @@ -142,7 +142,7 @@ export default class UsersStore extends BaseStore { const res = await client.post(`/users.${action}`, { id: user.id, }); - invariant(res && res.data, 'Data should be available'); + invariant(res && res.data, "Data should be available"); runInAction(`UsersStore#${action}`, () => { this.addPolicies(res.policies); diff --git a/app/stores/ViewsStore.js b/app/stores/ViewsStore.js index 7cd03e7d..990f864f 100644 --- a/app/stores/ViewsStore.js +++ b/app/stores/ViewsStore.js @@ -1,11 +1,11 @@ // @flow -import { reduce, filter, find, orderBy } from 'lodash'; -import BaseStore from './BaseStore'; -import RootStore from './RootStore'; -import View from 'models/View'; +import { reduce, filter, find, orderBy } from "lodash"; +import BaseStore from "./BaseStore"; +import RootStore from "./RootStore"; +import View from "models/View"; export default class ViewsStore extends BaseStore { - actions = ['list', 'create']; + actions = ["list", "create"]; constructor(rootStore: RootStore) { super(rootStore, View); @@ -14,8 +14,8 @@ export default class ViewsStore extends BaseStore { inDocument(documentId: string): View[] { return orderBy( filter(this.orderedData, view => view.documentId === documentId), - 'lastViewedAt', - 'desc' + "lastViewedAt", + "desc" ); } diff --git a/app/stores/index.js b/app/stores/index.js index 052b30ac..bd27266d 100644 --- a/app/stores/index.js +++ b/app/stores/index.js @@ -1,5 +1,5 @@ // @flow -import RootStore from 'stores/RootStore'; +import RootStore from "stores/RootStore"; const stores = new RootStore(); diff --git a/app/types/index.js b/app/types/index.js index e9b9d89d..4fa9c1af 100644 --- a/app/types/index.js +++ b/app/types/index.js @@ -1,11 +1,11 @@ // @flow -import Document from 'models/Document'; +import Document from "models/Document"; export type Toast = { id: string, createdAt: string, message: string, - type: 'warning' | 'error' | 'info' | 'success', + type: "warning" | "error" | "info" | "success", timeout?: number, action?: { text: string, @@ -39,7 +39,7 @@ export type PaginationParams = { limit?: number, offset?: number, sort?: string, - direction?: 'ASC' | 'DESC', + direction?: "ASC" | "DESC", }; export type SearchResult = { diff --git a/app/utils/ApiClient.js b/app/utils/ApiClient.js index 57bc59df..b156ec14 100644 --- a/app/utils/ApiClient.js +++ b/app/utils/ApiClient.js @@ -1,9 +1,9 @@ // @flow -import pkg from 'rich-markdown-editor/package.json'; -import { map, trim } from 'lodash'; -import invariant from 'invariant'; -import stores from 'stores'; -import download from './download'; +import pkg from "rich-markdown-editor/package.json"; +import { map, trim } from "lodash"; +import invariant from "invariant"; +import stores from "stores"; +import download from "./download"; import { AuthorizationError, NetworkError, @@ -11,7 +11,7 @@ import { OfflineError, RequestError, UpdateRequiredError, -} from './errors'; +} from "./errors"; type Options = { baseUrl?: string, @@ -22,8 +22,8 @@ class ApiClient { userAgent: string; constructor(options: Options = {}) { - this.baseUrl = options.baseUrl || '/api'; - this.userAgent = 'OutlineFrontend'; + this.baseUrl = options.baseUrl || "/api"; + this.userAgent = "OutlineFrontend"; } fetch = async ( @@ -35,27 +35,27 @@ class ApiClient { let body; let modifiedPath; - if (method === 'GET') { + if (method === "GET") { if (data) { modifiedPath = `${path}?${data && this.constructQueryString(data)}`; } else { modifiedPath = path; } - } else if (method === 'POST' || method === 'PUT') { + } else if (method === "POST" || method === "PUT") { body = data ? JSON.stringify(data) : undefined; } // Construct headers const headers = new Headers({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'cache-control': 'no-cache', - 'x-editor-version': pkg.version, - pragma: 'no-cache', + Accept: "application/json", + "Content-Type": "application/json", + "cache-control": "no-cache", + "x-editor-version": pkg.version, + pragma: "no-cache", }); if (stores.auth.authenticated) { - invariant(stores.auth.token, 'JWT token not set properly'); - headers.set('Authorization', `Bearer ${stores.auth.token}`); + invariant(stores.auth.token, "JWT token not set properly"); + headers.set("Authorization", `Bearer ${stores.auth.token}`); } let response; @@ -64,15 +64,15 @@ class ApiClient { method, body, headers, - redirect: 'follow', - credentials: 'omit', - cache: 'no-cache', + redirect: "follow", + credentials: "omit", + cache: "no-cache", }); } catch (err) { if (window.navigator.onLine) { - throw new NetworkError('A network error occurred, try again?'); + throw new NetworkError("A network error occurred, try again?"); } else { - throw new OfflineError('No internet connection available'); + throw new OfflineError("No internet connection available"); } } @@ -81,8 +81,8 @@ class ApiClient { if (options.download && success) { const blob = await response.blob(); const fileName = ( - response.headers.get('content-disposition') || '' - ).split('filename=')[1]; + response.headers.get("content-disposition") || "" + ).split("filename=")[1]; download(blob, trim(fileName, '"')); return; @@ -103,14 +103,14 @@ class ApiClient { try { const parsed = await response.json(); - error.message = parsed.message || ''; + error.message = parsed.message || ""; error.error = parsed.error; error.data = parsed.data; } catch (_err) { // we're trying to parse an error so JSON may not be valid } - if (response.status === 400 && error.error === 'editor_update_required') { + if (response.status === 400 && error.error === "editor_update_required") { window.location.reload(true); throw new UpdateRequiredError(error.message); } @@ -127,11 +127,11 @@ class ApiClient { }; get = (path: string, data: ?Object, options?: Object) => { - return this.fetch(path, 'GET', data, options); + return this.fetch(path, "GET", data, options); }; post = (path: string, data: ?Object, options?: Object) => { - return this.fetch(path, 'POST', data, options); + return this.fetch(path, "POST", data, options); }; // Helpers @@ -139,7 +139,7 @@ class ApiClient { return map( data, (v, k) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}` - ).join('&'); + ).join("&"); }; } diff --git a/app/utils/download.js b/app/utils/download.js index 46caf7e8..709c4a4d 100644 --- a/app/utils/download.js +++ b/app/utils/download.js @@ -14,22 +14,22 @@ export default function download( strMimeType?: string ) { var self = window, // this script is only for browsers anyway... - u = 'application/octet-stream', // this default mime also triggers iframe downloads + u = "application/octet-stream", // this default mime also triggers iframe downloads m = strMimeType || u, x = data, D = document, - a = D.createElement('a'), + a = D.createElement("a"), z = function(a, o) { return String(a); }, B = self.Blob || self.MozBlob || self.WebKitBlob || z, BB = self.MSBlobBuilder || self.WebKitBlobBuilder || self.BlobBuilder, - fn = strFileName || 'download', + fn = strFileName || "download", blob, b, fr; - if (String(this) === 'true') { + if (String(this) === "true") { //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback x = [x, m]; m = x[0]; @@ -56,12 +56,12 @@ export default function download( } function d2b(u) { - if (typeof u !== 'string') { - throw Error('Attempted to pass non-string to d2b'); + if (typeof u !== "string") { + throw Error("Attempted to pass non-string to d2b"); } var p = u.split(/[:;,]/), t = p[1], - dec = p[2] === 'base64' ? atob : decodeURIComponent, + dec = p[2] === "base64" ? atob : decodeURIComponent, bin = dec(p.pop()), mx = bin.length, i = 0, @@ -73,14 +73,14 @@ export default function download( } function saver(url, winMode) { - if (typeof url !== 'string') { - throw Error('Attempted to pass non-string url to saver'); + if (typeof url !== "string") { + throw Error("Attempted to pass non-string url to saver"); } - if ('download' in a) { + if ("download" in a) { a.href = url; - a.setAttribute('download', fn); - a.innerHTML = 'downloading…'; + a.setAttribute("download", fn); + a.innerHTML = "downloading…"; D.body && D.body.appendChild(a); setTimeout(function() { a.click(); @@ -95,11 +95,11 @@ export default function download( } //do iframe dataURL download (old ch+FF): - var f = D.createElement('iframe'); + var f = D.createElement("iframe"); D.body && D.body.appendChild(f); if (!winMode) { // force a mime that will download: - url = 'data:' + url.replace(/^data:([\w\/\-\+]+)/, u); + url = "data:" + url.replace(/^data:([\w\/\-\+]+)/, u); } f.src = url; @@ -121,14 +121,14 @@ export default function download( // handle non-Blob()+non-URL browsers: if ( blob && - (typeof blob === 'string' || blob.constructor === z) && - typeof m === 'string' + (typeof blob === "string" || blob.constructor === z) && + typeof m === "string" ) { try { - return saver('data:' + m + ';base64,' + self.btoa(blob)); + return saver("data:" + m + ";base64," + self.btoa(blob)); } catch (y) { // $FlowIssue - return saver('data:' + m + ',' + encodeURIComponent(blob)); + return saver("data:" + m + "," + encodeURIComponent(blob)); } } diff --git a/app/utils/emoji.js b/app/utils/emoji.js index 342e8af9..f9c9528d 100644 --- a/app/utils/emoji.js +++ b/app/utils/emoji.js @@ -15,7 +15,7 @@ export function toCodePoint(unicodeSurrogates: string, sep: ?string) { r.push(c.toString(16)); } } - return r.join(sep || '-'); + return r.join(sep || "-"); } export function emojiToUrl(text: string) { diff --git a/app/utils/errors.js b/app/utils/errors.js index da15cf3c..662a57c7 100644 --- a/app/utils/errors.js +++ b/app/utils/errors.js @@ -1,5 +1,5 @@ // @flow -import ExtendableError from 'es6-error'; +import ExtendableError from "es6-error"; export class AuthorizationError extends ExtendableError {} export class NetworkError extends ExtendableError {} diff --git a/app/utils/importFile.js b/app/utils/importFile.js index 9afff572..49f4e62f 100644 --- a/app/utils/importFile.js +++ b/app/utils/importFile.js @@ -1,7 +1,7 @@ // @flow -import Document from 'models/Document'; -import DocumentsStore from 'stores/DocumentsStore'; -import parseTitle from 'shared/utils/parseTitle'; +import Document from "models/Document"; +import DocumentsStore from "stores/DocumentsStore"; +import parseTitle from "shared/utils/parseTitle"; type Options = { file: File, @@ -25,14 +25,14 @@ const importFile = async ({ // If the first line of the imported file looks like a markdown heading // then we can use this as the document title - if (text.trim().startsWith('# ')) { + if (text.trim().startsWith("# ")) { const result = parseTitle(text); title = result.title; - text = text.replace(`# ${title}\n`, ''); + text = text.replace(`# ${title}\n`, ""); // otherwise, just use the filename without the extension as our best guess } else { - title = file.name.replace(/\.[^/.]+$/, ''); + title = file.name.replace(/\.[^/.]+$/, ""); } let document = new Document( diff --git a/app/utils/isInternalUrl.js b/app/utils/isInternalUrl.js index 2177d09c..8f0f59e4 100644 --- a/app/utils/isInternalUrl.js +++ b/app/utils/isInternalUrl.js @@ -1,8 +1,8 @@ // @flow -import { parseDomain } from '../../shared/utils/domains'; +import { parseDomain } from "../../shared/utils/domains"; export default function isInternalUrl(href: string) { - if (href[0] === '/') return true; + if (href[0] === "/") return true; const outline = parseDomain(window.location.href); const parsed = parseDomain(href); diff --git a/app/utils/keyboard.js b/app/utils/keyboard.js index 3b09fe0d..833738a9 100644 --- a/app/utils/keyboard.js +++ b/app/utils/keyboard.js @@ -1,3 +1,3 @@ // @flow -export const meta = window.navigator.platform === 'MacIntel' ? '⌘' : 'Ctrl'; +export const meta = window.navigator.platform === "MacIntel" ? "⌘" : "Ctrl"; diff --git a/app/utils/routeHelpers.js b/app/utils/routeHelpers.js index 0cbdf8ae..fe9d2b68 100644 --- a/app/utils/routeHelpers.js +++ b/app/utils/routeHelpers.js @@ -1,16 +1,16 @@ // @flow -import Document from 'models/Document'; +import Document from "models/Document"; export function homeUrl(): string { - return '/home'; + return "/home"; } export function starredUrl(): string { - return '/starred'; + return "/starred"; } export function newCollectionUrl(): string { - return '/collections/new'; + return "/collections/new"; } export function collectionUrl(collectionId: string, section: ?string): string { @@ -43,10 +43,10 @@ export function documentHistoryUrl(doc: Document, revisionId?: string): string { */ export function updateDocumentUrl(oldUrl: string, newUrl: string): string { // Update url to match the current one - const urlParts = oldUrl.trim().split('/'); + const urlParts = oldUrl.trim().split("/"); const actions = urlParts.slice(3); if (actions[0]) { - return [newUrl, actions].join('/'); + return [newUrl, actions].join("/"); } return newUrl; } @@ -65,7 +65,7 @@ export function newDocumentUrl( } export function searchUrl(query?: string, collectionId?: string): string { - let route = '/search'; + let route = "/search"; if (query) route += `/${encodeURIComponent(query)}`; if (collectionId) { @@ -75,10 +75,10 @@ export function searchUrl(query?: string, collectionId?: string): string { } export function notFoundUrl(): string { - return '/404'; + return "/404"; } export const matchDocumentSlug = - ':documentSlug([0-9a-zA-Z-_~]*-[a-zA-z0-9]{10,15})'; + ":documentSlug([0-9a-zA-Z-_~]*-[a-zA-z0-9]{10,15})"; export const matchDocumentEdit = `/doc/${matchDocumentSlug}/edit`; diff --git a/app/utils/uploadFile.js b/app/utils/uploadFile.js index 46bbb288..f8cffc10 100644 --- a/app/utils/uploadFile.js +++ b/app/utils/uploadFile.js @@ -1,6 +1,6 @@ // @flow -import { client } from './ApiClient'; -import invariant from 'invariant'; +import { client } from "./ApiClient"; +import invariant from "invariant"; type Options = { name?: string, @@ -10,10 +10,10 @@ type Options = { export const uploadFile = async ( file: File | Blob, - options?: Options = { name: '' } + options?: Options = { name: "" } ) => { const name = file instanceof File ? file.name : options.name; - const response = await client.post('/attachments.create', { + const response = await client.post("/attachments.create", { public: options.public, documentId: options.documentId, contentType: file.type, @@ -21,7 +21,7 @@ export const uploadFile = async ( name, }); - invariant(response, 'Response should be available'); + invariant(response, "Response should be available"); const data = response.data; const attachment = data.attachment; @@ -34,13 +34,13 @@ export const uploadFile = async ( // $FlowFixMe if (file.blob) { // $FlowFixMe - formData.append('file', file.file); + formData.append("file", file.file); } else { - formData.append('file', file); + formData.append("file", file); } await fetch(data.uploadUrl, { - method: 'post', + method: "post", body: formData, }); @@ -48,11 +48,11 @@ export const uploadFile = async ( }; export const dataUrlToBlob = (dataURL: string) => { - var blobBin = atob(dataURL.split(',')[1]); + var blobBin = atob(dataURL.split(",")[1]); var array = []; for (var i = 0; i < blobBin.length; i++) { array.push(blobBin.charCodeAt(i)); } - const file = new Blob([new Uint8Array(array)], { type: 'image/png' }); + const file = new Blob([new Uint8Array(array)], { type: "image/png" }); return file; }; diff --git a/server/api/apiKeys.js b/server/api/apiKeys.js index bf5ab8cb..bbff4c8e 100644 --- a/server/api/apiKeys.js +++ b/server/api/apiKeys.js @@ -1,21 +1,21 @@ // @flow -import Router from 'koa-router'; +import Router from "koa-router"; -import auth from '../middlewares/authentication'; -import pagination from './middlewares/pagination'; -import { presentApiKey } from '../presenters'; -import { ApiKey, Event } from '../models'; -import policy from '../policies'; +import auth from "../middlewares/authentication"; +import pagination from "./middlewares/pagination"; +import { presentApiKey } from "../presenters"; +import { ApiKey, Event } from "../models"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -router.post('apiKeys.create', auth(), async ctx => { +router.post("apiKeys.create", auth(), async ctx => { const { name } = ctx.body; - ctx.assertPresent(name, 'name is required'); + ctx.assertPresent(name, "name is required"); const user = ctx.state.user; - authorize(user, 'create', ApiKey); + authorize(user, "create", ApiKey); const key = await ApiKey.create({ name, @@ -23,7 +23,7 @@ router.post('apiKeys.create', auth(), async ctx => { }); await Event.create({ - name: 'api_keys.create', + name: "api_keys.create", modelId: key.id, teamId: user.teamId, actorId: user.id, @@ -36,13 +36,13 @@ router.post('apiKeys.create', auth(), async ctx => { }; }); -router.post('apiKeys.list', auth(), pagination(), async ctx => { +router.post("apiKeys.list", auth(), pagination(), async ctx => { const user = ctx.state.user; const keys = await ApiKey.findAll({ where: { userId: user.id, }, - order: [['createdAt', 'DESC']], + order: [["createdAt", "DESC"]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); @@ -53,18 +53,18 @@ router.post('apiKeys.list', auth(), pagination(), async ctx => { }; }); -router.post('apiKeys.delete', auth(), async ctx => { +router.post("apiKeys.delete", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const key = await ApiKey.findByPk(id); - authorize(user, 'delete', key); + authorize(user, "delete", key); await key.destroy(); await Event.create({ - name: 'api_keys.delete', + name: "api_keys.delete", modelId: key.id, teamId: user.teamId, actorId: user.id, diff --git a/server/api/attachments.js b/server/api/attachments.js index 8134effa..98722bc3 100644 --- a/server/api/attachments.js +++ b/server/api/attachments.js @@ -1,29 +1,29 @@ // @flow -import Router from 'koa-router'; -import uuid from 'uuid'; -import format from 'date-fns/format'; -import { Attachment, Document, Event } from '../models'; +import Router from "koa-router"; +import uuid from "uuid"; +import format from "date-fns/format"; +import { Attachment, Document, Event } from "../models"; import { makePolicy, getSignature, publicS3Endpoint, makeCredential, getSignedImageUrl, -} from '../utils/s3'; -import auth from '../middlewares/authentication'; -import { NotFoundError } from '../errors'; -import policy from '../policies'; +} from "../utils/s3"; +import auth from "../middlewares/authentication"; +import { NotFoundError } from "../errors"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -const AWS_S3_ACL = process.env.AWS_S3_ACL || 'private'; +const AWS_S3_ACL = process.env.AWS_S3_ACL || "private"; -router.post('attachments.create', auth(), async ctx => { +router.post("attachments.create", auth(), async ctx => { let { name, documentId, contentType, size } = ctx.body; - ctx.assertPresent(name, 'name is required'); - ctx.assertPresent(contentType, 'contentType is required'); - ctx.assertPresent(size, 'size is required'); + ctx.assertPresent(name, "name is required"); + ctx.assertPresent(contentType, "contentType is required"); + ctx.assertPresent(size, "size is required"); const { user } = ctx.state; const s3Key = uuid.v4(); @@ -31,16 +31,16 @@ router.post('attachments.create', auth(), async ctx => { const acl = ctx.body.public === undefined ? AWS_S3_ACL - : ctx.body.public ? 'public-read' : 'private'; + : ctx.body.public ? "public-read" : "private"; const credential = makeCredential(); - const longDate = format(new Date(), 'YYYYMMDDTHHmmss\\Z'); + const longDate = format(new Date(), "YYYYMMDDTHHmmss\\Z"); const policy = makePolicy(credential, longDate, acl); const endpoint = publicS3Endpoint(); const url = `${endpoint}/${key}`; if (documentId) { const document = await Document.findByPk(documentId, { userId: user.id }); - authorize(user, 'update', document); + authorize(user, "update", document); } const attachment = await Attachment.create({ @@ -55,7 +55,7 @@ router.post('attachments.create', auth(), async ctx => { }); await Event.create({ - name: 'attachments.create', + name: "attachments.create", data: { name }, teamId: user.teamId, userId: user.id, @@ -67,15 +67,15 @@ router.post('attachments.create', auth(), async ctx => { maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE, uploadUrl: endpoint, form: { - 'Cache-Control': 'max-age=31557600', - 'Content-Type': contentType, + "Cache-Control": "max-age=31557600", + "Content-Type": contentType, acl, key, policy, - 'x-amz-algorithm': 'AWS4-HMAC-SHA256', - 'x-amz-credential': credential, - 'x-amz-date': longDate, - 'x-amz-signature': getSignature(policy), + "x-amz-algorithm": "AWS4-HMAC-SHA256", + "x-amz-credential": credential, + "x-amz-date": longDate, + "x-amz-signature": getSignature(policy), }, attachment: { documentId, @@ -88,9 +88,9 @@ router.post('attachments.create', auth(), async ctx => { }; }); -router.post('attachments.redirect', auth(), async ctx => { +router.post("attachments.redirect", auth(), async ctx => { const { id } = ctx.body; - ctx.assertPresent(id, 'id is required'); + ctx.assertPresent(id, "id is required"); const user = ctx.state.user; const attachment = await Attachment.findByPk(id); @@ -103,7 +103,7 @@ router.post('attachments.redirect', auth(), async ctx => { const document = await Document.findByPk(attachment.documentId, { userId: user.id, }); - authorize(user, 'read', document); + authorize(user, "read", document); } const accessUrl = await getSignedImageUrl(attachment.key); diff --git a/server/api/attachments.test.js b/server/api/attachments.test.js index 7ed1919c..b885013e 100644 --- a/server/api/attachments.test.js +++ b/server/api/attachments.test.js @@ -1,40 +1,40 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; -import { flushdb } from '../test/support'; +import TestServer from "fetch-test-server"; +import app from "../app"; +import { flushdb } from "../test/support"; import { buildUser, buildCollection, buildAttachment, buildDocument, -} from '../test/factories'; +} from "../test/factories"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#attachments.redirect', async () => { - it('should require authentication', async () => { - const res = await server.post('/api/attachments.redirect'); +describe("#attachments.redirect", async () => { + it("should require authentication", async () => { + const res = await server.post("/api/attachments.redirect"); expect(res.status).toEqual(401); }); - it('should return a redirect for an attachment belonging to a document user has access to', async () => { + it("should return a redirect for an attachment belonging to a document user has access to", async () => { const user = await buildUser(); const attachment = await buildAttachment({ teamId: user.teamId, userId: user.id, }); - const res = await server.post('/api/attachments.redirect', { + const res = await server.post("/api/attachments.redirect", { body: { token: user.getJwtToken(), id: attachment.id }, - redirect: 'manual', + redirect: "manual", }); expect(res.status).toEqual(302); }); - it('should always return a redirect for a public attachment', async () => { + it("should always return a redirect for a public attachment", async () => { const user = await buildUser(); const collection = await buildCollection({ teamId: user.teamId, @@ -52,15 +52,15 @@ describe('#attachments.redirect', async () => { documentId: document.id, }); - const res = await server.post('/api/attachments.redirect', { + const res = await server.post("/api/attachments.redirect", { body: { token: user.getJwtToken(), id: attachment.id }, - redirect: 'manual', + redirect: "manual", }); expect(res.status).toEqual(302); }); - it('should not return a redirect for a private attachment belonging to a document user does not have access to', async () => { + it("should not return a redirect for a private attachment belonging to a document user does not have access to", async () => { const user = await buildUser(); const collection = await buildCollection({ private: true, @@ -74,10 +74,10 @@ describe('#attachments.redirect', async () => { teamId: document.teamId, userId: document.userId, documentId: document.id, - acl: 'private', + acl: "private", }); - const res = await server.post('/api/attachments.redirect', { + const res = await server.post("/api/attachments.redirect", { body: { token: user.getJwtToken(), id: attachment.id }, }); diff --git a/server/api/auth.js b/server/api/auth.js index 2b886dc1..f168bc03 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -1,12 +1,12 @@ // @flow -import Router from 'koa-router'; -import auth from '../middlewares/authentication'; -import { presentUser, presentTeam, presentPolicies } from '../presenters'; -import { Team } from '../models'; +import Router from "koa-router"; +import auth from "../middlewares/authentication"; +import { presentUser, presentTeam, presentPolicies } from "../presenters"; +import { Team } from "../models"; const router = new Router(); -router.post('auth.info', auth(), async ctx => { +router.post("auth.info", auth(), async ctx => { const user = ctx.state.user; const team = await Team.findByPk(user.teamId); diff --git a/server/api/collections.js b/server/api/collections.js index 07555ce5..5da4c65f 100644 --- a/server/api/collections.js +++ b/server/api/collections.js @@ -1,9 +1,9 @@ // @flow -import fs from 'fs'; -import Router from 'koa-router'; -import { Op } from '../sequelize'; -import auth from '../middlewares/authentication'; -import pagination from './middlewares/pagination'; +import fs from "fs"; +import Router from "koa-router"; +import { Op } from "../sequelize"; +import auth from "../middlewares/authentication"; +import pagination from "./middlewares/pagination"; import { presentCollection, presentUser, @@ -11,7 +11,7 @@ import { presentMembership, presentGroup, presentCollectionGroupMembership, -} from '../presenters'; +} from "../presenters"; import { Collection, CollectionUser, @@ -20,40 +20,40 @@ import { Event, User, Group, -} from '../models'; -import { ValidationError } from '../errors'; -import { exportCollections } from '../logistics'; -import { archiveCollection, archiveCollections } from '../utils/zip'; -import policy from '../policies'; +} from "../models"; +import { ValidationError } from "../errors"; +import { exportCollections } from "../logistics"; +import { archiveCollection, archiveCollections } from "../utils/zip"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -router.post('collections.create', auth(), async ctx => { +router.post("collections.create", auth(), async ctx => { const { name, color, description, icon, type } = ctx.body; const isPrivate = ctx.body.private; - ctx.assertPresent(name, 'name is required'); + ctx.assertPresent(name, "name is required"); if (color) { - ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)'); + ctx.assertHexColor(color, "Invalid hex value (please use format #FFFFFF)"); } const user = ctx.state.user; - authorize(user, 'create', Collection); + authorize(user, "create", Collection); let collection = await Collection.create({ name, description, icon, color, - type: type || 'atlas', + type: type || "atlas", teamId: user.teamId, creatorId: user.id, private: isPrivate, }); await Event.create({ - name: 'collections.create', + name: "collections.create", collectionId: collection.id, teamId: collection.teamId, actorId: user.id, @@ -64,7 +64,7 @@ router.post('collections.create', auth(), async ctx => { // we must reload the collection to get memberships for policy presenter if (isPrivate) { collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(collection.id); } @@ -74,15 +74,15 @@ router.post('collections.create', auth(), async ctx => { }; }); -router.post('collections.info', auth(), async ctx => { +router.post("collections.info", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(id); - authorize(user, 'read', collection); + authorize(user, "read", collection); ctx.body = { data: presentCollection(collection), @@ -90,18 +90,18 @@ router.post('collections.info', auth(), async ctx => { }; }); -router.post('collections.add_group', auth(), async ctx => { - const { id, groupId, permission = 'read_write' } = ctx.body; - ctx.assertUuid(id, 'id is required'); - ctx.assertUuid(groupId, 'groupId is required'); +router.post("collections.add_group", auth(), async ctx => { + const { id, groupId, permission = "read_write" } = ctx.body; + ctx.assertUuid(id, "id is required"); + ctx.assertUuid(groupId, "groupId is required"); const collection = await Collection.scope({ - method: ['withMembership', ctx.state.user.id], + method: ["withMembership", ctx.state.user.id], }).findByPk(id); - authorize(ctx.state.user, 'update', collection); + authorize(ctx.state.user, "update", collection); const group = await Group.findByPk(groupId); - authorize(ctx.state.user, 'read', group); + authorize(ctx.state.user, "read", group); let membership = await CollectionGroup.findOne({ where: { @@ -123,7 +123,7 @@ router.post('collections.add_group', auth(), async ctx => { } await Event.create({ - name: 'collections.add_group', + name: "collections.add_group", collectionId: collection.id, teamId: collection.teamId, actorId: ctx.state.user.id, @@ -140,23 +140,23 @@ router.post('collections.add_group', auth(), async ctx => { }; }); -router.post('collections.remove_group', auth(), async ctx => { +router.post("collections.remove_group", auth(), async ctx => { const { id, groupId } = ctx.body; - ctx.assertUuid(id, 'id is required'); - ctx.assertUuid(groupId, 'groupId is required'); + ctx.assertUuid(id, "id is required"); + ctx.assertUuid(groupId, "groupId is required"); const collection = await Collection.scope({ - method: ['withMembership', ctx.state.user.id], + method: ["withMembership", ctx.state.user.id], }).findByPk(id); - authorize(ctx.state.user, 'update', collection); + authorize(ctx.state.user, "update", collection); const group = await Group.findByPk(groupId); - authorize(ctx.state.user, 'read', group); + authorize(ctx.state.user, "read", group); await collection.removeGroup(group); await Event.create({ - name: 'collections.remove_group', + name: "collections.remove_group", collectionId: collection.id, teamId: collection.teamId, actorId: ctx.state.user.id, @@ -170,19 +170,19 @@ router.post('collections.remove_group', auth(), async ctx => { }); router.post( - 'collections.group_memberships', + "collections.group_memberships", auth(), pagination(), async ctx => { const { id, query, permission } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(id); - authorize(user, 'read', collection); + authorize(user, "read", collection); let where = { collectionId: id, @@ -207,13 +207,13 @@ router.post( const memberships = await CollectionGroup.findAll({ where, - order: [['createdAt', 'DESC']], + order: [["createdAt", "DESC"]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, include: [ { model: Group, - as: 'group', + as: "group", where: groupWhere, required: true, }, @@ -232,18 +232,18 @@ router.post( } ); -router.post('collections.add_user', auth(), async ctx => { - const { id, userId, permission = 'read_write' } = ctx.body; - ctx.assertUuid(id, 'id is required'); - ctx.assertUuid(userId, 'userId is required'); +router.post("collections.add_user", auth(), async ctx => { + const { id, userId, permission = "read_write" } = ctx.body; + ctx.assertUuid(id, "id is required"); + ctx.assertUuid(userId, "userId is required"); const collection = await Collection.scope({ - method: ['withMembership', ctx.state.user.id], + method: ["withMembership", ctx.state.user.id], }).findByPk(id); - authorize(ctx.state.user, 'update', collection); + authorize(ctx.state.user, "update", collection); const user = await User.findByPk(userId); - authorize(ctx.state.user, 'read', user); + authorize(ctx.state.user, "read", user); let membership = await CollectionUser.findOne({ where: { @@ -265,7 +265,7 @@ router.post('collections.add_user', auth(), async ctx => { } await Event.create({ - name: 'collections.add_user', + name: "collections.add_user", userId, collectionId: collection.id, teamId: collection.teamId, @@ -282,23 +282,23 @@ router.post('collections.add_user', auth(), async ctx => { }; }); -router.post('collections.remove_user', auth(), async ctx => { +router.post("collections.remove_user", auth(), async ctx => { const { id, userId } = ctx.body; - ctx.assertUuid(id, 'id is required'); - ctx.assertUuid(userId, 'userId is required'); + ctx.assertUuid(id, "id is required"); + ctx.assertUuid(userId, "userId is required"); const collection = await Collection.scope({ - method: ['withMembership', ctx.state.user.id], + method: ["withMembership", ctx.state.user.id], }).findByPk(id); - authorize(ctx.state.user, 'update', collection); + authorize(ctx.state.user, "update", collection); const user = await User.findByPk(userId); - authorize(ctx.state.user, 'read', user); + authorize(ctx.state.user, "read", user); await collection.removeUser(user); await Event.create({ - name: 'collections.remove_user', + name: "collections.remove_user", userId, collectionId: collection.id, teamId: collection.teamId, @@ -313,15 +313,15 @@ router.post('collections.remove_user', auth(), async ctx => { }); // DEPRECATED: Use collection.memberships which has pagination, filtering and permissions -router.post('collections.users', auth(), async ctx => { +router.post("collections.users", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(id); - authorize(user, 'read', collection); + authorize(user, "read", collection); const users = await collection.getUsers(); @@ -330,15 +330,15 @@ router.post('collections.users', auth(), async ctx => { }; }); -router.post('collections.memberships', auth(), pagination(), async ctx => { +router.post("collections.memberships", auth(), pagination(), async ctx => { const { id, query, permission } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(id); - authorize(user, 'read', collection); + authorize(user, "read", collection); let where = { collectionId: id, @@ -363,13 +363,13 @@ router.post('collections.memberships', auth(), pagination(), async ctx => { const memberships = await CollectionUser.findAll({ where, - order: [['createdAt', 'DESC']], + order: [["createdAt", "DESC"]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, include: [ { model: User, - as: 'user', + as: "user", where: userWhere, required: true, }, @@ -385,20 +385,20 @@ router.post('collections.memberships', auth(), pagination(), async ctx => { }; }); -router.post('collections.export', auth(), async ctx => { +router.post("collections.export", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(id); - authorize(user, 'export', collection); + authorize(user, "export", collection); const filePath = await archiveCollection(collection); await Event.create({ - name: 'collections.export', + name: "collections.export", collectionId: collection.id, teamId: user.teamId, actorId: user.id, @@ -407,19 +407,19 @@ router.post('collections.export', auth(), async ctx => { }); ctx.attachment(`${collection.name}.zip`); - ctx.set('Content-Type', 'application/force-download'); + ctx.set("Content-Type", "application/force-download"); ctx.body = fs.createReadStream(filePath); }); -router.post('collections.export_all', auth(), async ctx => { +router.post("collections.export_all", auth(), async ctx => { const { download = false } = ctx.body; const user = ctx.state.user; const team = await Team.findByPk(user.teamId); - authorize(user, 'export', team); + authorize(user, "export", team); await Event.create({ - name: 'collections.export', + name: "collections.export", teamId: user.teamId, actorId: user.id, ip: ctx.request.ip, @@ -428,12 +428,12 @@ router.post('collections.export_all', auth(), async ctx => { if (download) { const collections = await Collection.findAll({ where: { teamId: team.id }, - order: [['name', 'ASC']], + order: [["name", "ASC"]], }); const filePath = await archiveCollections(collections); ctx.attachment(`${team.name}.zip`); - ctx.set('Content-Type', 'application/force-download'); + ctx.set("Content-Type", "application/force-download"); ctx.body = fs.createReadStream(filePath); } else { // async operation to create zip archive and email user @@ -445,22 +445,22 @@ router.post('collections.export_all', auth(), async ctx => { } }); -router.post('collections.update', auth(), async ctx => { +router.post("collections.update", auth(), async ctx => { const { id, name, description, icon, color } = ctx.body; const isPrivate = ctx.body.private; - ctx.assertPresent(name, 'name is required'); + ctx.assertPresent(name, "name is required"); if (color) { - ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)'); + ctx.assertHexColor(color, "Invalid hex value (please use format #FFFFFF)"); } const user = ctx.state.user; const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(id); - authorize(user, 'update', collection); + authorize(user, "update", collection); // we're making this collection private right now, ensure that the current // user has a read-write membership so that at least they can edit it @@ -471,7 +471,7 @@ router.post('collections.update', auth(), async ctx => { userId: user.id, }, defaults: { - permission: 'read_write', + permission: "read_write", createdById: user.id, }, }); @@ -488,7 +488,7 @@ router.post('collections.update', auth(), async ctx => { await collection.save(); await Event.create({ - name: 'collections.update', + name: "collections.update", collectionId: collection.id, teamId: collection.teamId, actorId: user.id, @@ -508,17 +508,17 @@ router.post('collections.update', auth(), async ctx => { }; }); -router.post('collections.list', auth(), pagination(), async ctx => { +router.post("collections.list", auth(), pagination(), async ctx => { const user = ctx.state.user; const collectionIds = await user.collectionIds(); let collections = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findAll({ where: { teamId: user.teamId, id: collectionIds, }, - order: [['updatedAt', 'DESC']], + order: [["updatedAt", "DESC"]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); @@ -530,24 +530,24 @@ router.post('collections.list', auth(), pagination(), async ctx => { }; }); -router.post('collections.delete', auth(), async ctx => { +router.post("collections.delete", auth(), async ctx => { const { id } = ctx.body; const user = ctx.state.user; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(id); - authorize(user, 'delete', collection); + authorize(user, "delete", collection); const total = await Collection.count(); - if (total === 1) throw new ValidationError('Cannot delete last collection'); + if (total === 1) throw new ValidationError("Cannot delete last collection"); await collection.destroy(); await Event.create({ - name: 'collections.delete', + name: "collections.delete", collectionId: collection.id, teamId: collection.teamId, actorId: user.id, diff --git a/server/api/collections.test.js b/server/api/collections.test.js index fdc959ab..a7b31763 100644 --- a/server/api/collections.test.js +++ b/server/api/collections.test.js @@ -1,26 +1,26 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; -import { flushdb, seed } from '../test/support'; -import { buildUser, buildGroup, buildCollection } from '../test/factories'; -import { Collection, CollectionUser, CollectionGroup } from '../models'; +import TestServer from "fetch-test-server"; +import app from "../app"; +import { flushdb, seed } from "../test/support"; +import { buildUser, buildGroup, buildCollection } from "../test/factories"; +import { Collection, CollectionUser, CollectionGroup } from "../models"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#collections.list', async () => { - it('should require authentication', async () => { - const res = await server.post('/api/collections.list'); +describe("#collections.list", async () => { + it("should require authentication", async () => { + const res = await server.post("/api/collections.list"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should return collections', async () => { + it("should return collections", async () => { const { user, collection } = await seed(); - const res = await server.post('/api/collections.list', { + const res = await server.post("/api/collections.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -32,13 +32,13 @@ describe('#collections.list', async () => { expect(body.policies[0].abilities.read).toEqual(true); }); - it('should not return private collections actor is not a member of', async () => { + it("should not return private collections actor is not a member of", async () => { const { user, collection } = await seed(); await buildCollection({ private: true, teamId: user.teamId, }); - const res = await server.post('/api/collections.list', { + const res = await server.post("/api/collections.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -48,7 +48,7 @@ describe('#collections.list', async () => { expect(body.data[0].id).toEqual(collection.id); }); - it('should return private collections actor is a member of', async () => { + it("should return private collections actor is a member of", async () => { const user = await buildUser(); await buildCollection({ private: true, @@ -61,7 +61,7 @@ describe('#collections.list', async () => { userId: user.id, }); - const res = await server.post('/api/collections.list', { + const res = await server.post("/api/collections.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -72,7 +72,7 @@ describe('#collections.list', async () => { expect(body.policies[0].abilities.read).toEqual(true); }); - it('should return private collections actor is a group-member of', async () => { + it("should return private collections actor is a group-member of", async () => { const user = await buildUser(); await buildCollection({ private: true, @@ -89,10 +89,10 @@ describe('#collections.list', async () => { await group.addUser(user, { through: { createdById: user.id } }); await collection.addGroup(group, { - through: { permission: 'read', createdById: user.id }, + through: { permission: "read", createdById: user.id }, }); - const res = await server.post('/api/collections.list', { + const res = await server.post("/api/collections.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -104,21 +104,21 @@ describe('#collections.list', async () => { }); }); -describe('#collections.export', async () => { - it('should now allow export of private collection not a member', async () => { +describe("#collections.export", async () => { + it("should now allow export of private collection not a member", async () => { const { user } = await seed(); const collection = await buildCollection({ private: true, teamId: user.teamId, }); - const res = await server.post('/api/collections.export', { + const res = await server.post("/api/collections.export", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(403); }); - it('should allow export of private collection when the actor is a member', async () => { + it("should allow export of private collection when the actor is a member", async () => { const { user, collection } = await seed(); collection.private = true; await collection.save(); @@ -127,17 +127,17 @@ describe('#collections.export', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read', + permission: "read", }); - const res = await server.post('/api/collections.export', { + const res = await server.post("/api/collections.export", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(200); }); - it('should allow export of private collection when the actor is a group member', async () => { + it("should allow export of private collection when the actor is a group member", async () => { const user = await buildUser(); const collection = await buildCollection({ private: true, @@ -148,27 +148,27 @@ describe('#collections.export', async () => { await group.addUser(user, { through: { createdById: user.id } }); await collection.addGroup(group, { - through: { permission: 'read', createdById: user.id }, + through: { permission: "read", createdById: user.id }, }); - const res = await server.post('/api/collections.export', { + const res = await server.post("/api/collections.export", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(200); }); - it('should require authentication', async () => { - const res = await server.post('/api/collections.export'); + it("should require authentication", async () => { + const res = await server.post("/api/collections.export"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should return success', async () => { + it("should return success", async () => { const { user, collection } = await seed(); - const res = await server.post('/api/collections.export', { + const res = await server.post("/api/collections.export", { body: { token: user.getJwtToken(), id: collection.id }, }); @@ -176,47 +176,47 @@ describe('#collections.export', async () => { }); }); -describe('#collections.export_all', async () => { - it('should require authentication', async () => { - const res = await server.post('/api/collections.export_all'); +describe("#collections.export_all", async () => { + it("should require authentication", async () => { + const res = await server.post("/api/collections.export_all"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const user = await buildUser(); - const res = await server.post('/api/collections.export_all', { + const res = await server.post("/api/collections.export_all", { body: { token: user.getJwtToken() }, }); expect(res.status).toEqual(403); }); - it('should return success', async () => { + it("should return success", async () => { const { admin } = await seed(); - const res = await server.post('/api/collections.export_all', { + const res = await server.post("/api/collections.export_all", { body: { token: admin.getJwtToken() }, }); expect(res.status).toEqual(200); }); - it('should allow downloading directly', async () => { + it("should allow downloading directly", async () => { const { admin } = await seed(); - const res = await server.post('/api/collections.export_all', { + const res = await server.post("/api/collections.export_all", { body: { token: admin.getJwtToken(), download: true }, }); expect(res.status).toEqual(200); - expect(res.headers.get('content-type')).toEqual( - 'application/force-download' + expect(res.headers.get("content-type")).toEqual( + "application/force-download" ); }); }); -describe('#collections.add_user', async () => { - it('should add user to collection', async () => { +describe("#collections.add_user", async () => { + it("should add user to collection", async () => { const user = await buildUser(); const collection = await buildCollection({ teamId: user.teamId, @@ -224,7 +224,7 @@ describe('#collections.add_user', async () => { private: true, }); const anotherUser = await buildUser({ teamId: user.teamId }); - const res = await server.post('/api/collections.add_user', { + const res = await server.post("/api/collections.add_user", { body: { token: user.getJwtToken(), id: collection.id, @@ -237,14 +237,14 @@ describe('#collections.add_user', async () => { expect(users.length).toEqual(2); }); - it('should require user in team', async () => { + it("should require user in team", async () => { const user = await buildUser(); const collection = await buildCollection({ teamId: user.teamId, private: true, }); const anotherUser = await buildUser(); - const res = await server.post('/api/collections.add_user', { + const res = await server.post("/api/collections.add_user", { body: { token: user.getJwtToken(), id: collection.id, @@ -257,18 +257,18 @@ describe('#collections.add_user', async () => { expect(body).toMatchSnapshot(); }); - it('should require authentication', async () => { - const res = await server.post('/api/collections.add_user'); + it("should require authentication", async () => { + const res = await server.post("/api/collections.add_user"); expect(res.status).toEqual(401); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { collection } = await seed(); const user = await buildUser(); const anotherUser = await buildUser({ teamId: user.teamId }); - const res = await server.post('/api/collections.add_user', { + const res = await server.post("/api/collections.add_user", { body: { token: user.getJwtToken(), id: collection.id, @@ -279,8 +279,8 @@ describe('#collections.add_user', async () => { }); }); -describe('#collections.add_group', async () => { - it('should add group to collection', async () => { +describe("#collections.add_group", async () => { + it("should add group to collection", async () => { const user = await buildUser({ isAdmin: true }); const collection = await buildCollection({ teamId: user.teamId, @@ -288,7 +288,7 @@ describe('#collections.add_group', async () => { private: true, }); const group = await buildGroup({ teamId: user.teamId }); - const res = await server.post('/api/collections.add_group', { + const res = await server.post("/api/collections.add_group", { body: { token: user.getJwtToken(), id: collection.id, @@ -301,7 +301,7 @@ describe('#collections.add_group', async () => { expect(res.status).toEqual(200); }); - it('should require group in team', async () => { + it("should require group in team", async () => { const user = await buildUser(); const collection = await buildCollection({ teamId: user.teamId, @@ -309,7 +309,7 @@ describe('#collections.add_group', async () => { private: true, }); const group = await buildGroup(); - const res = await server.post('/api/collections.add_group', { + const res = await server.post("/api/collections.add_group", { body: { token: user.getJwtToken(), id: collection.id, @@ -322,16 +322,16 @@ describe('#collections.add_group', async () => { expect(body).toMatchSnapshot(); }); - it('should require authentication', async () => { - const res = await server.post('/api/collections.add_group'); + it("should require authentication", async () => { + const res = await server.post("/api/collections.add_group"); expect(res.status).toEqual(401); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const collection = await buildCollection(); const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId }); - const res = await server.post('/api/collections.add_group', { + const res = await server.post("/api/collections.add_group", { body: { token: user.getJwtToken(), id: collection.id, @@ -342,8 +342,8 @@ describe('#collections.add_group', async () => { }); }); -describe('#collections.remove_group', async () => { - it('should remove group from collection', async () => { +describe("#collections.remove_group", async () => { + it("should remove group from collection", async () => { const user = await buildUser({ isAdmin: true }); const collection = await buildCollection({ teamId: user.teamId, @@ -352,7 +352,7 @@ describe('#collections.remove_group', async () => { }); const group = await buildGroup({ teamId: user.teamId }); - await server.post('/api/collections.add_group', { + await server.post("/api/collections.add_group", { body: { token: user.getJwtToken(), id: collection.id, @@ -363,7 +363,7 @@ describe('#collections.remove_group', async () => { let users = await collection.getGroups(); expect(users.length).toEqual(1); - const res = await server.post('/api/collections.remove_group', { + const res = await server.post("/api/collections.remove_group", { body: { token: user.getJwtToken(), id: collection.id, @@ -376,14 +376,14 @@ describe('#collections.remove_group', async () => { expect(users.length).toEqual(0); }); - it('should require group in team', async () => { + it("should require group in team", async () => { const user = await buildUser(); const collection = await buildCollection({ teamId: user.teamId, private: true, }); const group = await buildGroup(); - const res = await server.post('/api/collections.remove_group', { + const res = await server.post("/api/collections.remove_group", { body: { token: user.getJwtToken(), id: collection.id, @@ -396,18 +396,18 @@ describe('#collections.remove_group', async () => { expect(body).toMatchSnapshot(); }); - it('should require authentication', async () => { - const res = await server.post('/api/collections.remove_group'); + it("should require authentication", async () => { + const res = await server.post("/api/collections.remove_group"); expect(res.status).toEqual(401); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { collection } = await seed(); const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId }); - const res = await server.post('/api/collections.remove_group', { + const res = await server.post("/api/collections.remove_group", { body: { token: user.getJwtToken(), id: collection.id, @@ -418,8 +418,8 @@ describe('#collections.remove_group', async () => { }); }); -describe('#collections.remove_user', async () => { - it('should remove user from collection', async () => { +describe("#collections.remove_user", async () => { + it("should remove user from collection", async () => { const user = await buildUser(); const collection = await buildCollection({ teamId: user.teamId, @@ -428,7 +428,7 @@ describe('#collections.remove_user', async () => { }); const anotherUser = await buildUser({ teamId: user.teamId }); - await server.post('/api/collections.add_user', { + await server.post("/api/collections.add_user", { body: { token: user.getJwtToken(), id: collection.id, @@ -436,7 +436,7 @@ describe('#collections.remove_user', async () => { }, }); - const res = await server.post('/api/collections.remove_user', { + const res = await server.post("/api/collections.remove_user", { body: { token: user.getJwtToken(), id: collection.id, @@ -449,14 +449,14 @@ describe('#collections.remove_user', async () => { expect(users.length).toEqual(1); }); - it('should require user in team', async () => { + it("should require user in team", async () => { const user = await buildUser(); const collection = await buildCollection({ teamId: user.teamId, private: true, }); const anotherUser = await buildUser(); - const res = await server.post('/api/collections.remove_user', { + const res = await server.post("/api/collections.remove_user", { body: { token: user.getJwtToken(), id: collection.id, @@ -469,18 +469,18 @@ describe('#collections.remove_user', async () => { expect(body).toMatchSnapshot(); }); - it('should require authentication', async () => { - const res = await server.post('/api/collections.remove_user'); + it("should require authentication", async () => { + const res = await server.post("/api/collections.remove_user"); expect(res.status).toEqual(401); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { collection } = await seed(); const user = await buildUser(); const anotherUser = await buildUser({ teamId: user.teamId }); - const res = await server.post('/api/collections.remove_user', { + const res = await server.post("/api/collections.remove_user", { body: { token: user.getJwtToken(), id: collection.id, @@ -491,8 +491,8 @@ describe('#collections.remove_user', async () => { }); }); -describe('#collections.users', async () => { - it('should return users in private collection', async () => { +describe("#collections.users", async () => { + it("should return users in private collection", async () => { const { collection, user } = await seed(); collection.private = true; await collection.save(); @@ -501,10 +501,10 @@ describe('#collections.users', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read', + permission: "read", }); - const res = await server.post('/api/collections.users', { + const res = await server.post("/api/collections.users", { body: { token: user.getJwtToken(), id: collection.id }, }); const body = await res.json(); @@ -513,26 +513,26 @@ describe('#collections.users', async () => { expect(body.data.length).toEqual(1); }); - it('should require authentication', async () => { - const res = await server.post('/api/collections.users'); + it("should require authentication", async () => { + const res = await server.post("/api/collections.users"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { collection } = await seed(); const user = await buildUser(); - const res = await server.post('/api/collections.users', { + const res = await server.post("/api/collections.users", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(403); }); }); -describe('#collections.group_memberships', async () => { - it('should return groups in private collection', async () => { +describe("#collections.group_memberships", async () => { + it("should return groups in private collection", async () => { const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId }); const collection = await buildCollection({ @@ -544,17 +544,17 @@ describe('#collections.group_memberships', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read_write', + permission: "read_write", }); await CollectionGroup.create({ createdById: user.id, collectionId: collection.id, groupId: group.id, - permission: 'read_write', + permission: "read_write", }); - const res = await server.post('/api/collections.group_memberships', { + const res = await server.post("/api/collections.group_memberships", { body: { token: user.getJwtToken(), id: collection.id }, }); const body = await res.json(); @@ -564,14 +564,14 @@ describe('#collections.group_memberships', async () => { expect(body.data.groups[0].id).toEqual(group.id); expect(body.data.collectionGroupMemberships.length).toEqual(1); expect(body.data.collectionGroupMemberships[0].permission).toEqual( - 'read_write' + "read_write" ); }); - it('should allow filtering groups in collection by name', async () => { + it("should allow filtering groups in collection by name", async () => { const user = await buildUser(); - const group = await buildGroup({ name: 'will find', teamId: user.teamId }); - const group2 = await buildGroup({ name: 'wont find', teamId: user.teamId }); + const group = await buildGroup({ name: "will find", teamId: user.teamId }); + const group2 = await buildGroup({ name: "wont find", teamId: user.teamId }); const collection = await buildCollection({ private: true, teamId: user.teamId, @@ -581,28 +581,28 @@ describe('#collections.group_memberships', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read_write', + permission: "read_write", }); await CollectionGroup.create({ createdById: user.id, collectionId: collection.id, groupId: group.id, - permission: 'read_write', + permission: "read_write", }); await CollectionGroup.create({ createdById: user.id, collectionId: collection.id, groupId: group2.id, - permission: 'read_write', + permission: "read_write", }); - const res = await server.post('/api/collections.group_memberships', { + const res = await server.post("/api/collections.group_memberships", { body: { token: user.getJwtToken(), id: collection.id, - query: 'will', + query: "will", }, }); @@ -613,7 +613,7 @@ describe('#collections.group_memberships', async () => { expect(body.data.groups[0].id).toEqual(group.id); }); - it('should allow filtering groups in collection by permission', async () => { + it("should allow filtering groups in collection by permission", async () => { const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId }); const group2 = await buildGroup({ teamId: user.teamId }); @@ -626,28 +626,28 @@ describe('#collections.group_memberships', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read_write', + permission: "read_write", }); await CollectionGroup.create({ createdById: user.id, collectionId: collection.id, groupId: group.id, - permission: 'read_write', + permission: "read_write", }); await CollectionGroup.create({ createdById: user.id, collectionId: collection.id, groupId: group2.id, - permission: 'maintainer', + permission: "maintainer", }); - const res = await server.post('/api/collections.group_memberships', { + const res = await server.post("/api/collections.group_memberships", { body: { token: user.getJwtToken(), id: collection.id, - permission: 'maintainer', + permission: "maintainer", }, }); @@ -658,30 +658,30 @@ describe('#collections.group_memberships', async () => { expect(body.data.groups[0].id).toEqual(group2.id); }); - it('should require authentication', async () => { - const res = await server.post('/api/collections.group_memberships'); + it("should require authentication", async () => { + const res = await server.post("/api/collections.group_memberships"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const user = await buildUser(); const collection = await buildCollection({ private: true, teamId: user.teamId, }); - const res = await server.post('/api/collections.group_memberships', { + const res = await server.post("/api/collections.group_memberships", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(403); }); }); -describe('#collections.memberships', async () => { - it('should return members in private collection', async () => { +describe("#collections.memberships", async () => { + it("should return members in private collection", async () => { const { collection, user } = await seed(); collection.private = true; await collection.save(); @@ -690,10 +690,10 @@ describe('#collections.memberships', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read_write', + permission: "read_write", }); - const res = await server.post('/api/collections.memberships', { + const res = await server.post("/api/collections.memberships", { body: { token: user.getJwtToken(), id: collection.id }, }); const body = await res.json(); @@ -702,26 +702,26 @@ describe('#collections.memberships', async () => { expect(body.data.users.length).toEqual(1); expect(body.data.users[0].id).toEqual(user.id); expect(body.data.memberships.length).toEqual(1); - expect(body.data.memberships[0].permission).toEqual('read_write'); + expect(body.data.memberships[0].permission).toEqual("read_write"); }); - it('should allow filtering members in collection by name', async () => { + it("should allow filtering members in collection by name", async () => { const { collection, user } = await seed(); const user2 = await buildUser({ name: "Won't find" }); await CollectionUser.create({ createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read_write', + permission: "read_write", }); await CollectionUser.create({ createdById: user2.id, collectionId: collection.id, userId: user2.id, - permission: 'read_write', + permission: "read_write", }); - const res = await server.post('/api/collections.memberships', { + const res = await server.post("/api/collections.memberships", { body: { token: user.getJwtToken(), id: collection.id, @@ -735,27 +735,27 @@ describe('#collections.memberships', async () => { expect(body.data.users[0].id).toEqual(user.id); }); - it('should allow filtering members in collection by permission', async () => { + it("should allow filtering members in collection by permission", async () => { const { collection, user } = await seed(); const user2 = await buildUser(); await CollectionUser.create({ createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read_write', + permission: "read_write", }); await CollectionUser.create({ createdById: user2.id, collectionId: collection.id, userId: user2.id, - permission: 'maintainer', + permission: "maintainer", }); - const res = await server.post('/api/collections.memberships', { + const res = await server.post("/api/collections.memberships", { body: { token: user.getJwtToken(), id: collection.id, - permission: 'maintainer', + permission: "maintainer", }, }); const body = await res.json(); @@ -765,28 +765,28 @@ describe('#collections.memberships', async () => { expect(body.data.users[0].id).toEqual(user2.id); }); - it('should require authentication', async () => { - const res = await server.post('/api/collections.memberships'); + it("should require authentication", async () => { + const res = await server.post("/api/collections.memberships"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { collection } = await seed(); const user = await buildUser(); - const res = await server.post('/api/collections.memberships', { + const res = await server.post("/api/collections.memberships", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(403); }); }); -describe('#collections.info', async () => { - it('should return collection', async () => { +describe("#collections.info", async () => { + it("should return collection", async () => { const { user, collection } = await seed(); - const res = await server.post('/api/collections.info', { + const res = await server.post("/api/collections.info", { body: { token: user.getJwtToken(), id: collection.id }, }); const body = await res.json(); @@ -795,18 +795,18 @@ describe('#collections.info', async () => { expect(body.data.id).toEqual(collection.id); }); - it('should require user member of collection', async () => { + it("should require user member of collection", async () => { const { user, collection } = await seed(); collection.private = true; await collection.save(); - const res = await server.post('/api/collections.info', { + const res = await server.post("/api/collections.info", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(403); }); - it('should allow user member of collection', async () => { + it("should allow user member of collection", async () => { const { user, collection } = await seed(); collection.private = true; await collection.save(); @@ -815,10 +815,10 @@ describe('#collections.info', async () => { collectionId: collection.id, userId: user.id, createdById: user.id, - permission: 'read', + permission: "read", }); - const res = await server.post('/api/collections.info', { + const res = await server.post("/api/collections.info", { body: { token: user.getJwtToken(), id: collection.id }, }); const body = await res.json(); @@ -827,52 +827,52 @@ describe('#collections.info', async () => { expect(body.data.id).toEqual(collection.id); }); - it('should require authentication', async () => { - const res = await server.post('/api/collections.info'); + it("should require authentication", async () => { + const res = await server.post("/api/collections.info"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { collection } = await seed(); const user = await buildUser(); - const res = await server.post('/api/collections.info', { + const res = await server.post("/api/collections.info", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(403); }); }); -describe('#collections.create', async () => { - it('should require authentication', async () => { - const res = await server.post('/api/collections.create'); +describe("#collections.create", async () => { + it("should require authentication", async () => { + const res = await server.post("/api/collections.create"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should create collection', async () => { + it("should create collection", async () => { const { user } = await seed(); - const res = await server.post('/api/collections.create', { - body: { token: user.getJwtToken(), name: 'Test' }, + const res = await server.post("/api/collections.create", { + body: { token: user.getJwtToken(), name: "Test" }, }); const body = await res.json(); expect(res.status).toEqual(200); expect(body.data.id).toBeTruthy(); - expect(body.data.name).toBe('Test'); + expect(body.data.name).toBe("Test"); expect(body.policies.length).toBe(1); expect(body.policies[0].abilities.read).toBeTruthy(); expect(body.policies[0].abilities.export).toBeTruthy(); }); - it('should return correct policies with private collection', async () => { + it("should return correct policies with private collection", async () => { const { user } = await seed(); - const res = await server.post('/api/collections.create', { - body: { token: user.getJwtToken(), name: 'Test', private: true }, + const res = await server.post("/api/collections.create", { + body: { token: user.getJwtToken(), name: "Test", private: true }, }); const body = await res.json(); @@ -884,11 +884,11 @@ describe('#collections.create', async () => { }); }); -describe('#collections.update', async () => { - it('should require authentication', async () => { +describe("#collections.update", async () => { + it("should require authentication", async () => { const collection = await buildCollection(); - const res = await server.post('/api/collections.update', { - body: { id: collection.id, name: 'Test' }, + const res = await server.post("/api/collections.update", { + body: { id: collection.id, name: "Test" }, }); const body = await res.json(); @@ -896,39 +896,39 @@ describe('#collections.update', async () => { expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { collection } = await seed(); const user = await buildUser(); - const res = await server.post('/api/collections.update', { - body: { token: user.getJwtToken(), id: collection.id, name: 'Test' }, + const res = await server.post("/api/collections.update", { + body: { token: user.getJwtToken(), id: collection.id, name: "Test" }, }); expect(res.status).toEqual(403); }); - it('allows editing non-private collection', async () => { + it("allows editing non-private collection", async () => { const { user, collection } = await seed(); - const res = await server.post('/api/collections.update', { - body: { token: user.getJwtToken(), id: collection.id, name: 'Test' }, + const res = await server.post("/api/collections.update", { + body: { token: user.getJwtToken(), id: collection.id, name: "Test" }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.name).toBe('Test'); + expect(body.data.name).toBe("Test"); expect(body.policies.length).toBe(1); }); - it('allows editing from non-private to private collection', async () => { + it("allows editing from non-private to private collection", async () => { const { user, collection } = await seed(); - const res = await server.post('/api/collections.update', { + const res = await server.post("/api/collections.update", { body: { token: user.getJwtToken(), id: collection.id, private: true, - name: 'Test', + name: "Test", }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.name).toBe('Test'); + expect(body.data.name).toBe("Test"); expect(body.data.private).toBe(true); // ensure we return with a write level policy @@ -936,7 +936,7 @@ describe('#collections.update', async () => { expect(body.policies[0].abilities.update).toBe(true); }); - it('allows editing from private to non-private collection', async () => { + it("allows editing from private to non-private collection", async () => { const { user, collection } = await seed(); collection.private = true; await collection.save(); @@ -945,20 +945,20 @@ describe('#collections.update', async () => { collectionId: collection.id, userId: user.id, createdById: user.id, - permission: 'read_write', + permission: "read_write", }); - const res = await server.post('/api/collections.update', { + const res = await server.post("/api/collections.update", { body: { token: user.getJwtToken(), id: collection.id, private: false, - name: 'Test', + name: "Test", }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.name).toBe('Test'); + expect(body.data.name).toBe("Test"); expect(body.data.private).toBe(false); // ensure we return with a write level policy @@ -966,7 +966,7 @@ describe('#collections.update', async () => { expect(body.policies[0].abilities.update).toBe(true); }); - it('allows editing by read-write collection user', async () => { + it("allows editing by read-write collection user", async () => { const { user, collection } = await seed(); collection.private = true; await collection.save(); @@ -975,19 +975,19 @@ describe('#collections.update', async () => { collectionId: collection.id, userId: user.id, createdById: user.id, - permission: 'read_write', + permission: "read_write", }); - const res = await server.post('/api/collections.update', { - body: { token: user.getJwtToken(), id: collection.id, name: 'Test' }, + const res = await server.post("/api/collections.update", { + body: { token: user.getJwtToken(), id: collection.id, name: "Test" }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.name).toBe('Test'); + expect(body.data.name).toBe("Test"); expect(body.policies.length).toBe(1); }); - it('allows editing by read-write collection group user', async () => { + it("allows editing by read-write collection group user", async () => { const user = await buildUser(); const collection = await buildCollection({ private: true, @@ -998,19 +998,19 @@ describe('#collections.update', async () => { await group.addUser(user, { through: { createdById: user.id } }); await collection.addGroup(group, { - through: { permission: 'read_write', createdById: user.id }, + through: { permission: "read_write", createdById: user.id }, }); - const res = await server.post('/api/collections.update', { - body: { token: user.getJwtToken(), id: collection.id, name: 'Test' }, + const res = await server.post("/api/collections.update", { + body: { token: user.getJwtToken(), id: collection.id, name: "Test" }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.name).toBe('Test'); + expect(body.data.name).toBe("Test"); expect(body.policies.length).toBe(1); }); - it('does not allow editing by read-only collection user', async () => { + it("does not allow editing by read-only collection user", async () => { const { user, collection } = await seed(); collection.private = true; await collection.save(); @@ -1019,53 +1019,53 @@ describe('#collections.update', async () => { collectionId: collection.id, userId: user.id, createdById: user.id, - permission: 'read', + permission: "read", }); - const res = await server.post('/api/collections.update', { - body: { token: user.getJwtToken(), id: collection.id, name: 'Test' }, + const res = await server.post("/api/collections.update", { + body: { token: user.getJwtToken(), id: collection.id, name: "Test" }, }); expect(res.status).toEqual(403); }); }); -describe('#collections.delete', async () => { - it('should require authentication', async () => { - const res = await server.post('/api/collections.delete'); +describe("#collections.delete", async () => { + it("should require authentication", async () => { + const res = await server.post("/api/collections.delete"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { collection } = await seed(); const user = await buildUser(); - const res = await server.post('/api/collections.delete', { + const res = await server.post("/api/collections.delete", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(403); }); - it('should not allow deleting last collection', async () => { + it("should not allow deleting last collection", async () => { const { user, collection } = await seed(); - const res = await server.post('/api/collections.delete', { + const res = await server.post("/api/collections.delete", { body: { token: user.getJwtToken(), id: collection.id }, }); expect(res.status).toEqual(400); }); - it('should delete collection', async () => { + it("should delete collection", async () => { const { user, collection } = await seed(); await Collection.create({ - name: 'Blah', - urlId: 'blah', + name: "Blah", + urlId: "blah", teamId: user.teamId, creatorId: user.id, - type: 'atlas', + type: "atlas", }); - const res = await server.post('/api/collections.delete', { + const res = await server.post("/api/collections.delete", { body: { token: user.getJwtToken(), id: collection.id }, }); const body = await res.json(); @@ -1074,7 +1074,7 @@ describe('#collections.delete', async () => { expect(body.success).toBe(true); }); - it('allows deleting by read-write collection group user', async () => { + it("allows deleting by read-write collection group user", async () => { const user = await buildUser(); const collection = await buildCollection({ private: true, @@ -1088,10 +1088,10 @@ describe('#collections.delete', async () => { await group.addUser(user, { through: { createdById: user.id } }); await collection.addGroup(group, { - through: { permission: 'read_write', createdById: user.id }, + through: { permission: "read_write", createdById: user.id }, }); - const res = await server.post('/api/collections.delete', { + const res = await server.post("/api/collections.delete", { body: { token: user.getJwtToken(), id: collection.id }, }); const body = await res.json(); diff --git a/server/api/documents.js b/server/api/documents.js index 22937254..9458ca41 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -1,14 +1,14 @@ // @flow -import Router from 'koa-router'; -import Sequelize from 'sequelize'; -import auth from '../middlewares/authentication'; -import pagination from './middlewares/pagination'; -import documentMover from '../commands/documentMover'; +import Router from "koa-router"; +import Sequelize from "sequelize"; +import auth from "../middlewares/authentication"; +import pagination from "./middlewares/pagination"; +import documentMover from "../commands/documentMover"; import { presentDocument, presentCollection, presentPolicies, -} from '../presenters'; +} from "../presenters"; import { Collection, Document, @@ -19,23 +19,23 @@ import { Revision, Backlink, User, -} from '../models'; -import { InvalidRequestError } from '../errors'; -import policy from '../policies'; -import { sequelize } from '../sequelize'; +} from "../models"; +import { InvalidRequestError } from "../errors"; +import policy from "../policies"; +import { sequelize } from "../sequelize"; const Op = Sequelize.Op; const { authorize, cannot } = policy; const router = new Router(); -router.post('documents.list', auth(), pagination(), async ctx => { - const { sort = 'updatedAt', backlinkDocumentId, parentDocumentId } = ctx.body; +router.post("documents.list", auth(), pagination(), async ctx => { + const { sort = "updatedAt", backlinkDocumentId, parentDocumentId } = ctx.body; // collection and user are here for backwards compatablity const collectionId = ctx.body.collectionId || ctx.body.collection; const createdById = ctx.body.userId || ctx.body.user; let direction = ctx.body.direction; - if (direction !== 'ASC') direction = 'DESC'; + if (direction !== "ASC") direction = "DESC"; // always filter by the current team const user = ctx.state.user; @@ -44,19 +44,19 @@ router.post('documents.list', auth(), pagination(), async ctx => { // if a specific user is passed then add to filters. If the user doesn't // exist in the team then nothing will be returned, so no need to check auth if (createdById) { - ctx.assertUuid(createdById, 'user must be a UUID'); + ctx.assertUuid(createdById, "user must be a UUID"); where = { ...where, createdById }; } // if a specific collection is passed then we need to check auth to view it if (collectionId) { - ctx.assertUuid(collectionId, 'collection must be a UUID'); + ctx.assertUuid(collectionId, "collection must be a UUID"); where = { ...where, collectionId }; const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(collectionId); - authorize(user, 'read', collection); + authorize(user, "read", collection); // otherwise, filter by all collections the user has access to } else { @@ -65,15 +65,15 @@ router.post('documents.list', auth(), pagination(), async ctx => { } if (parentDocumentId) { - ctx.assertUuid(parentDocumentId, 'parentDocumentId must be a UUID'); + ctx.assertUuid(parentDocumentId, "parentDocumentId must be a UUID"); where = { ...where, parentDocumentId }; } if (backlinkDocumentId) { - ctx.assertUuid(backlinkDocumentId, 'backlinkDocumentId must be a UUID'); + ctx.assertUuid(backlinkDocumentId, "backlinkDocumentId must be a UUID"); const backlinks = await Backlink.findAll({ - attributes: ['reverseDocumentId'], + attributes: ["reverseDocumentId"], where: { documentId: backlinkDocumentId, }, @@ -86,10 +86,10 @@ router.post('documents.list', auth(), pagination(), async ctx => { } // add the users starred state to the response by default - const starredScope = { method: ['withStarred', user.id] }; - const collectionScope = { method: ['withCollection', user.id] }; + const starredScope = { method: ["withStarred", user.id] }; + const collectionScope = { method: ["withCollection", user.id] }; const documents = await Document.scope( - 'defaultScope', + "defaultScope", starredScope, collectionScope ).findAll({ @@ -112,23 +112,23 @@ router.post('documents.list', auth(), pagination(), async ctx => { }; }); -router.post('documents.pinned', auth(), pagination(), async ctx => { - const { collectionId, sort = 'updatedAt' } = ctx.body; +router.post("documents.pinned", auth(), pagination(), async ctx => { + const { collectionId, sort = "updatedAt" } = ctx.body; let direction = ctx.body.direction; - if (direction !== 'ASC') direction = 'DESC'; - ctx.assertUuid(collectionId, 'collectionId is required'); + if (direction !== "ASC") direction = "DESC"; + ctx.assertUuid(collectionId, "collectionId is required"); const user = ctx.state.user; const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(collectionId); - authorize(user, 'read', collection); + authorize(user, "read", collection); - const starredScope = { method: ['withStarred', user.id] }; - const collectionScope = { method: ['withCollection', user.id] }; + const starredScope = { method: ["withStarred", user.id] }; + const collectionScope = { method: ["withCollection", user.id] }; const documents = await Document.scope( - 'defaultScope', + "defaultScope", starredScope, collectionScope ).findAll({ @@ -157,17 +157,17 @@ router.post('documents.pinned', auth(), pagination(), async ctx => { }; }); -router.post('documents.archived', auth(), pagination(), async ctx => { - const { sort = 'updatedAt' } = ctx.body; +router.post("documents.archived", auth(), pagination(), async ctx => { + const { sort = "updatedAt" } = ctx.body; let direction = ctx.body.direction; - if (direction !== 'ASC') direction = 'DESC'; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; const collectionIds = await user.collectionIds(); - const collectionScope = { method: ['withCollection', user.id] }; + const collectionScope = { method: ["withCollection", user.id] }; const documents = await Document.scope( - 'defaultScope', + "defaultScope", collectionScope ).findAll({ where: { @@ -195,15 +195,15 @@ router.post('documents.archived', auth(), pagination(), async ctx => { }; }); -router.post('documents.deleted', auth(), pagination(), async ctx => { - const { sort = 'deletedAt' } = ctx.body; +router.post("documents.deleted", auth(), pagination(), async ctx => { + const { sort = "deletedAt" } = ctx.body; let direction = ctx.body.direction; - if (direction !== 'ASC') direction = 'DESC'; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; const collectionIds = await user.collectionIds(); - const collectionScope = { method: ['withCollection', user.id] }; + const collectionScope = { method: ["withCollection", user.id] }; const documents = await Document.scope(collectionScope).findAll({ where: { teamId: user.teamId, @@ -213,8 +213,8 @@ router.post('documents.deleted', auth(), pagination(), async ctx => { }, }, include: [ - { model: User, as: 'createdBy', paranoid: false }, - { model: User, as: 'updatedBy', paranoid: false }, + { model: User, as: "createdBy", paranoid: false }, + { model: User, as: "updatedBy", paranoid: false }, ], paranoid: false, order: [[sort, direction]], @@ -235,9 +235,9 @@ router.post('documents.deleted', auth(), pagination(), async ctx => { }; }); -router.post('documents.viewed', auth(), pagination(), async ctx => { - let { sort = 'updatedAt', direction } = ctx.body; - if (direction !== 'ASC') direction = 'DESC'; +router.post("documents.viewed", auth(), pagination(), async ctx => { + let { sort = "updatedAt", direction } = ctx.body; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; const collectionIds = await user.collectionIds(); @@ -255,7 +255,7 @@ router.post('documents.viewed', auth(), pagination(), async ctx => { include: [ { model: Star, - as: 'starred', + as: "starred", where: { userId: user.id }, required: false, }, @@ -280,9 +280,9 @@ router.post('documents.viewed', auth(), pagination(), async ctx => { }; }); -router.post('documents.starred', auth(), pagination(), async ctx => { - let { sort = 'updatedAt', direction } = ctx.body; - if (direction !== 'ASC') direction = 'DESC'; +router.post("documents.starred", auth(), pagination(), async ctx => { + let { sort = "updatedAt", direction } = ctx.body; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; const collectionIds = await user.collectionIds(); @@ -301,11 +301,11 @@ router.post('documents.starred', auth(), pagination(), async ctx => { include: [ { model: Collection, - as: 'collection', + as: "collection", }, { model: Star, - as: 'starred', + as: "starred", where: { userId: user.id, }, @@ -331,16 +331,16 @@ router.post('documents.starred', auth(), pagination(), async ctx => { }; }); -router.post('documents.drafts', auth(), pagination(), async ctx => { - let { sort = 'updatedAt', direction } = ctx.body; - if (direction !== 'ASC') direction = 'DESC'; +router.post("documents.drafts", auth(), pagination(), async ctx => { + let { sort = "updatedAt", direction } = ctx.body; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; const collectionIds = await user.collectionIds(); - const collectionScope = { method: ['withCollection', user.id] }; + const collectionScope = { method: ["withCollection", user.id] }; const documents = await Document.scope( - 'defaultScope', + "defaultScope", collectionScope ).findAll({ where: { @@ -366,9 +366,9 @@ router.post('documents.drafts', auth(), pagination(), async ctx => { }; }); -router.post('documents.info', auth({ required: false }), async ctx => { +router.post("documents.info", auth({ required: false }), async ctx => { const { id, shareId } = ctx.body; - ctx.assertPresent(id || shareId, 'id or shareId is required'); + ctx.assertPresent(id || shareId, "id or shareId is required"); const user = ctx.state.user; let document; @@ -384,16 +384,16 @@ router.post('documents.info', auth({ required: false }), async ctx => { // unscoping here allows us to return unpublished documents model: Document.unscoped(), include: [ - { model: User, as: 'createdBy', paranoid: false }, - { model: User, as: 'updatedBy', paranoid: false }, + { model: User, as: "createdBy", paranoid: false }, + { model: User, as: "updatedBy", paranoid: false }, ], required: true, - as: 'document', + as: "document", }, ], }); if (!share || share.document.archivedAt) { - throw new InvalidRequestError('Document could not be found for shareId'); + throw new InvalidRequestError("Document could not be found for shareId"); } document = share.document; } else { @@ -401,10 +401,10 @@ router.post('documents.info', auth({ required: false }), async ctx => { id, user ? { userId: user.id } : undefined ); - authorize(user, 'read', document); + authorize(user, "read", document); } - const isPublic = cannot(user, 'read', document); + const isPublic = cannot(user, "read", document); ctx.body = { data: await presentDocument(document, { isPublic }), @@ -412,9 +412,9 @@ router.post('documents.info', auth({ required: false }), async ctx => { }; }); -router.post('documents.restore', auth(), async ctx => { +router.post("documents.restore", auth(), async ctx => { const { id, revisionId } = ctx.body; - ctx.assertPresent(id, 'id is required'); + ctx.assertPresent(id, "id is required"); const user = ctx.state.user; const document = await Document.findByPk(id, { @@ -423,13 +423,13 @@ router.post('documents.restore', auth(), async ctx => { }); if (document.deletedAt) { - authorize(user, 'restore', document); + authorize(user, "restore", document); // restore a previously deleted document await document.unarchive(user.id); await Event.create({ - name: 'documents.restore', + name: "documents.restore", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -438,13 +438,13 @@ router.post('documents.restore', auth(), async ctx => { ip: ctx.request.ip, }); } else if (document.archivedAt) { - authorize(user, 'unarchive', document); + authorize(user, "unarchive", document); // restore a previously archived document await document.unarchive(user.id); await Event.create({ - name: 'documents.unarchive', + name: "documents.unarchive", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -454,17 +454,17 @@ router.post('documents.restore', auth(), async ctx => { }); } else if (revisionId) { // restore a document to a specific revision - authorize(user, 'update', document); + authorize(user, "update", document); const revision = await Revision.findByPk(revisionId); - authorize(document, 'restore', revision); + authorize(document, "restore", revision); document.text = revision.text; document.title = revision.title; await document.save(); await Event.create({ - name: 'documents.restore', + name: "documents.restore", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -473,7 +473,7 @@ router.post('documents.restore', auth(), async ctx => { ip: ctx.request.ip, }); } else { - ctx.assertPresent(revisionId, 'revisionId is required'); + ctx.assertPresent(revisionId, "revisionId is required"); } ctx.body = { @@ -482,7 +482,7 @@ router.post('documents.restore', auth(), async ctx => { }; }); -router.post('documents.search', auth(), pagination(), async ctx => { +router.post("documents.search", auth(), pagination(), async ctx => { const { query, includeArchived, @@ -493,34 +493,34 @@ router.post('documents.search', auth(), pagination(), async ctx => { } = ctx.body; const { offset, limit } = ctx.state.pagination; const user = ctx.state.user; - ctx.assertPresent(query, 'query is required'); + ctx.assertPresent(query, "query is required"); if (collectionId) { - ctx.assertUuid(collectionId, 'collectionId must be a UUID'); + ctx.assertUuid(collectionId, "collectionId must be a UUID"); const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(collectionId); - authorize(user, 'read', collection); + authorize(user, "read", collection); } let collaboratorIds = undefined; if (userId) { - ctx.assertUuid(userId, 'userId must be a UUID'); + ctx.assertUuid(userId, "userId must be a UUID"); collaboratorIds = [userId]; } if (dateFilter) { ctx.assertIn( dateFilter, - ['day', 'week', 'month', 'year'], - 'dateFilter must be one of day,week,month,year' + ["day", "week", "month", "year"], + "dateFilter must be one of day,week,month,year" ); } const results = await Document.searchForUser(user, query, { - includeArchived: includeArchived === 'true', - includeDrafts: includeDrafts === 'true', + includeArchived: includeArchived === "true", + includeDrafts: includeDrafts === "true", collaboratorIds, collectionId, dateFilter, @@ -545,19 +545,19 @@ router.post('documents.search', auth(), pagination(), async ctx => { }; }); -router.post('documents.pin', auth(), async ctx => { +router.post("documents.pin", auth(), async ctx => { 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.findByPk(id, { userId: user.id }); - authorize(user, 'pin', document); + authorize(user, "pin", document); document.pinnedById = user.id; await document.save(); await Event.create({ - name: 'documents.pin', + name: "documents.pin", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -572,19 +572,19 @@ router.post('documents.pin', auth(), async ctx => { }; }); -router.post('documents.unpin', auth(), async ctx => { +router.post("documents.unpin", auth(), async ctx => { 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.findByPk(id, { userId: user.id }); - authorize(user, 'unpin', document); + authorize(user, "unpin", document); document.pinnedById = null; await document.save(); await Event.create({ - name: 'documents.unpin', + name: "documents.unpin", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -599,20 +599,20 @@ router.post('documents.unpin', auth(), async ctx => { }; }); -router.post('documents.star', auth(), async ctx => { +router.post("documents.star", auth(), async ctx => { 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.findByPk(id, { userId: user.id }); - authorize(user, 'read', document); + authorize(user, "read", document); await Star.findOrCreate({ where: { documentId: document.id, userId: user.id }, }); await Event.create({ - name: 'documents.star', + name: "documents.star", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -626,20 +626,20 @@ router.post('documents.star', auth(), async ctx => { }; }); -router.post('documents.unstar', auth(), async ctx => { +router.post("documents.unstar", auth(), async ctx => { 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.findByPk(id, { userId: user.id }); - authorize(user, 'read', document); + authorize(user, "read", document); await Star.destroy({ where: { documentId: document.id, userId: user.id }, }); await Event.create({ - name: 'documents.unstar', + name: "documents.unstar", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -653,46 +653,46 @@ router.post('documents.unstar', auth(), async ctx => { }; }); -router.post('documents.create', auth(), async ctx => { +router.post("documents.create", auth(), async ctx => { const { - title = '', - text = '', + title = "", + text = "", publish, collectionId, parentDocumentId, index, } = ctx.body; - const editorVersion = ctx.headers['x-editor-version']; + const editorVersion = ctx.headers["x-editor-version"]; - ctx.assertUuid(collectionId, 'collectionId must be an uuid'); + ctx.assertUuid(collectionId, "collectionId must be an uuid"); if (parentDocumentId) { - ctx.assertUuid(parentDocumentId, 'parentDocumentId must be an uuid'); + ctx.assertUuid(parentDocumentId, "parentDocumentId 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; - authorize(user, 'create', Document); + authorize(user, "create", Document); const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findOne({ where: { id: collectionId, teamId: user.teamId, }, }); - authorize(user, 'publish', collection); + authorize(user, "publish", collection); let parentDocument; - if (parentDocumentId && collection.type === 'atlas') { + if (parentDocumentId && collection.type === "atlas") { parentDocument = await Document.findOne({ where: { id: parentDocumentId, collectionId: collection.id, }, }); - authorize(user, 'read', parentDocument, { collection }); + authorize(user, "read", parentDocument, { collection }); } let document = await Document.create({ @@ -708,7 +708,7 @@ router.post('documents.create', auth(), async ctx => { }); await Event.create({ - name: 'documents.create', + name: "documents.create", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -721,7 +721,7 @@ router.post('documents.create', auth(), async ctx => { await document.publish(); await Event.create({ - name: 'documents.publish', + name: "documents.publish", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -745,7 +745,7 @@ router.post('documents.create', auth(), async ctx => { }; }); -router.post('documents.update', auth(), async ctx => { +router.post("documents.update", auth(), async ctx => { const { id, title, @@ -756,18 +756,18 @@ router.post('documents.update', auth(), async ctx => { lastRevision, append, } = ctx.body; - const editorVersion = ctx.headers['x-editor-version']; + const editorVersion = ctx.headers["x-editor-version"]; - ctx.assertPresent(id, 'id is required'); - ctx.assertPresent(title || text, 'title or text is required'); - if (append) ctx.assertPresent(text, 'Text is required while appending'); + ctx.assertPresent(id, "id is required"); + ctx.assertPresent(title || text, "title or text is required"); + if (append) ctx.assertPresent(text, "Text is required while appending"); const user = ctx.state.user; const document = await Document.findByPk(id, { userId: user.id }); - authorize(user, 'update', document); + authorize(user, "update", document); if (lastRevision && lastRevision !== document.revisionCount) { - throw new InvalidRequestError('Document has changed since last revision'); + throw new InvalidRequestError("Document has changed since last revision"); } // Update document @@ -801,7 +801,7 @@ router.post('documents.update', auth(), async ctx => { if (publish) { await Event.create({ - name: 'documents.publish', + name: "documents.publish", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -811,7 +811,7 @@ router.post('documents.update', auth(), async ctx => { }); } else { await Event.create({ - name: 'documents.update', + name: "documents.update", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -834,31 +834,31 @@ router.post('documents.update', auth(), async ctx => { }; }); -router.post('documents.move', auth(), async ctx => { +router.post("documents.move", auth(), async ctx => { const { id, collectionId, parentDocumentId, index } = ctx.body; - ctx.assertUuid(id, 'id must be a uuid'); - ctx.assertUuid(collectionId, 'collectionId must be a uuid'); + ctx.assertUuid(id, "id must be a uuid"); + ctx.assertUuid(collectionId, "collectionId must be a uuid"); if (parentDocumentId) { - ctx.assertUuid(parentDocumentId, 'parentDocumentId must be a uuid'); + ctx.assertUuid(parentDocumentId, "parentDocumentId must be a uuid"); } if (index) { - ctx.assertPositiveInteger(index, 'index must be a positive integer'); + ctx.assertPositiveInteger(index, "index must be a positive integer"); } if (parentDocumentId === id) { throw new InvalidRequestError( - 'Infinite loop detected, cannot nest a document inside itself' + "Infinite loop detected, cannot nest a document inside itself" ); } const user = ctx.state.user; const document = await Document.findByPk(id, { userId: user.id }); - authorize(user, 'move', document); + authorize(user, "move", document); const { collection } = document; - if (collection.type !== 'atlas' && parentDocumentId) { + if (collection.type !== "atlas" && parentDocumentId) { throw new InvalidRequestError( - 'Document cannot be nested in this collection type' + "Document cannot be nested in this collection type" ); } @@ -866,7 +866,7 @@ router.post('documents.move', auth(), async ctx => { const parent = await Document.findByPk(parentDocumentId, { userId: user.id, }); - authorize(user, 'update', parent); + authorize(user, "update", parent); } const { documents, collections } = await documentMover({ @@ -891,18 +891,18 @@ router.post('documents.move', auth(), async ctx => { }; }); -router.post('documents.archive', auth(), async ctx => { +router.post("documents.archive", auth(), async ctx => { 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.findByPk(id, { userId: user.id }); - authorize(user, 'archive', document); + authorize(user, "archive", document); await document.archive(user.id); await Event.create({ - name: 'documents.archive', + name: "documents.archive", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -917,18 +917,18 @@ router.post('documents.archive', auth(), async ctx => { }; }); -router.post('documents.delete', auth(), async ctx => { +router.post("documents.delete", auth(), async ctx => { 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.findByPk(id, { userId: user.id }); - authorize(user, 'delete', document); + authorize(user, "delete", document); await document.delete(); await Event.create({ - name: 'documents.delete', + name: "documents.delete", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 364e90ca..f71c99a6 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -1,6 +1,6 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; +import TestServer from "fetch-test-server"; +import app from "../app"; import { Document, View, @@ -8,24 +8,24 @@ import { Revision, Backlink, CollectionUser, -} from '../models'; -import { flushdb, seed } from '../test/support'; +} from "../models"; +import { flushdb, seed } from "../test/support"; import { buildShare, buildCollection, buildUser, buildDocument, -} from '../test/factories'; +} from "../test/factories"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#documents.info', async () => { - it('should return published document', async () => { +describe("#documents.info", async () => { + it("should return published document", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { token: user.getJwtToken(), id: document.id }, }); const body = await res.json(); @@ -34,10 +34,10 @@ describe('#documents.info', async () => { expect(body.data.id).toEqual(document.id); }); - it('should return archived document', async () => { + it("should return archived document", async () => { const { user, document } = await seed(); await document.archive(user.id); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { token: user.getJwtToken(), id: document.id }, }); const body = await res.json(); @@ -46,7 +46,7 @@ describe('#documents.info', async () => { expect(body.data.id).toEqual(document.id); }); - it('should not return published document in collection not a member of', async () => { + it("should not return published document in collection not a member of", async () => { const user = await buildUser(); const collection = await buildCollection({ private: true, @@ -54,19 +54,19 @@ describe('#documents.info', async () => { }); const document = await buildDocument({ collectionId: collection.id }); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { token: user.getJwtToken(), id: document.id }, }); expect(res.status).toEqual(403); }); - it('should return drafts', async () => { + it("should return drafts", async () => { const { user, document } = await seed(); document.publishedAt = null; await document.save(); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { token: user.getJwtToken(), id: document.id }, }); const body = await res.json(); @@ -75,7 +75,7 @@ describe('#documents.info', async () => { expect(body.data.id).toEqual(document.id); }); - it('should return document from shareId without token', async () => { + it("should return document from shareId without token", async () => { const { document, user } = await seed(); const share = await buildShare({ documentId: document.id, @@ -83,7 +83,7 @@ describe('#documents.info', async () => { userId: user.id, }); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { shareId: share.id }, }); const body = await res.json(); @@ -94,7 +94,7 @@ describe('#documents.info', async () => { expect(body.data.updatedBy).toEqual(undefined); }); - it('should not return document from revoked shareId', async () => { + it("should not return document from revoked shareId", async () => { const { document, user } = await seed(); const share = await buildShare({ documentId: document.id, @@ -103,13 +103,13 @@ describe('#documents.info', async () => { }); await share.revoke(user.id); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { shareId: share.id }, }); expect(res.status).toEqual(400); }); - it('should not return document from archived shareId', async () => { + it("should not return document from archived shareId", async () => { const { document, user } = await seed(); const share = await buildShare({ documentId: document.id, @@ -118,13 +118,13 @@ describe('#documents.info', async () => { }); await document.archive(user.id); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { shareId: share.id }, }); expect(res.status).toEqual(400); }); - it('should return document from shareId with token', async () => { + it("should return document from shareId with token", async () => { const { user, document } = await seed(); const share = await buildShare({ documentId: document.id, @@ -132,7 +132,7 @@ describe('#documents.info', async () => { userId: user.id, }); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { token: user.getJwtToken(), shareId: share.id }, }); const body = await res.json(); @@ -143,7 +143,7 @@ describe('#documents.info', async () => { expect(body.data.updatedBy.id).toEqual(user.id); }); - it('should return draft document from shareId with token', async () => { + it("should return draft document from shareId with token", async () => { const { user, document } = await seed(); document.publishedAt = null; await document.save(); @@ -154,7 +154,7 @@ describe('#documents.info', async () => { userId: user.id, }); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { token: user.getJwtToken(), shareId: share.id }, }); const body = await res.json(); @@ -165,7 +165,7 @@ describe('#documents.info', async () => { expect(body.data.updatedBy.id).toEqual(user.id); }); - it('should return document from shareId in collection not a member of', async () => { + it("should return document from shareId in collection not a member of", async () => { const { user, document, collection } = await seed(); const share = await buildShare({ documentId: document.id, @@ -176,7 +176,7 @@ describe('#documents.info', async () => { collection.private = true; await collection.save(); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { token: user.getJwtToken(), shareId: share.id }, }); const body = await res.json(); @@ -185,36 +185,36 @@ describe('#documents.info', async () => { expect(body.data.id).toEqual(document.id); }); - it('should require authorization without token', async () => { + it("should require authorization without token", async () => { const { document } = await seed(); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { id: document.id }, }); expect(res.status).toEqual(403); }); - it('should require authorization with incorrect token', async () => { + it("should require authorization with incorrect token", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/documents.info', { + const res = await server.post("/api/documents.info", { body: { token: user.getJwtToken(), id: document.id }, }); expect(res.status).toEqual(403); }); - it('should require a valid shareId', async () => { - const res = await server.post('/api/documents.info', { + it("should require a valid shareId", async () => { + const res = await server.post("/api/documents.info", { body: { shareId: 123 }, }); expect(res.status).toEqual(400); }); }); -describe('#documents.list', async () => { - it('should return documents', async () => { +describe("#documents.list", async () => { + it("should return documents", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.list', { + const res = await server.post("/api/documents.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -224,12 +224,12 @@ describe('#documents.list', async () => { expect(body.data[0].id).toEqual(document.id); }); - it('should not return unpublished documents', async () => { + it("should not return unpublished documents", async () => { const { user, document } = await seed(); document.publishedAt = null; await document.save(); - const res = await server.post('/api/documents.list', { + const res = await server.post("/api/documents.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -238,12 +238,12 @@ describe('#documents.list', async () => { expect(body.data.length).toEqual(0); }); - it('should not return documents in private collections not a member of', async () => { + it("should not return documents in private collections not a member of", async () => { const { user, collection } = await seed(); collection.private = true; await collection.save(); - const res = await server.post('/api/documents.list', { + const res = await server.post("/api/documents.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -252,16 +252,16 @@ describe('#documents.list', async () => { expect(body.data.length).toEqual(0); }); - it('should allow changing sort direction', async () => { + it("should allow changing sort direction", async () => { const { user, document } = await seed(); const anotherDoc = await buildDocument({ - title: 'another document', - text: 'random text', + title: "another document", + text: "random text", userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/documents.list', { - body: { token: user.getJwtToken(), direction: 'ASC' }, + const res = await server.post("/api/documents.list", { + body: { token: user.getJwtToken(), direction: "ASC" }, }); const body = await res.json(); @@ -270,9 +270,9 @@ describe('#documents.list', async () => { expect(body.data[1].id).toEqual(anotherDoc.id); }); - it('should allow filtering by collection', async () => { + it("should allow filtering by collection", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.list', { + const res = await server.post("/api/documents.list", { body: { token: user.getJwtToken(), collection: document.collectionId, @@ -284,7 +284,7 @@ describe('#documents.list', async () => { expect(body.data.length).toEqual(1); }); - it('should allow filtering to private collection', async () => { + it("should allow filtering to private collection", async () => { const { user, collection } = await seed(); collection.private = true; await collection.save(); @@ -293,10 +293,10 @@ describe('#documents.list', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read', + permission: "read", }); - const res = await server.post('/api/documents.list', { + const res = await server.post("/api/documents.list", { body: { token: user.getJwtToken(), collection: collection.id, @@ -308,11 +308,11 @@ describe('#documents.list', async () => { expect(body.data.length).toEqual(1); }); - it('should return backlinks', async () => { + it("should return backlinks", async () => { const { user, document } = await seed(); const anotherDoc = await buildDocument({ - title: 'another document', - text: 'random text', + title: "another document", + text: "random text", userId: user.id, teamId: user.teamId, }); @@ -323,7 +323,7 @@ describe('#documents.list', async () => { userId: user.id, }); - const res = await server.post('/api/documents.list', { + const res = await server.post("/api/documents.list", { body: { token: user.getJwtToken(), backlinkDocumentId: document.id }, }); const body = await res.json(); @@ -333,8 +333,8 @@ describe('#documents.list', async () => { expect(body.data[0].id).toEqual(anotherDoc.id); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.list'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.list"); const body = await res.json(); expect(res.status).toEqual(401); @@ -342,13 +342,13 @@ describe('#documents.list', async () => { }); }); -describe('#documents.pinned', async () => { - it('should return pinned documents', async () => { +describe("#documents.pinned", async () => { + it("should return pinned documents", async () => { const { user, document } = await seed(); document.pinnedById = user.id; await document.save(); - const res = await server.post('/api/documents.pinned', { + const res = await server.post("/api/documents.pinned", { body: { token: user.getJwtToken(), collectionId: document.collectionId }, }); const body = await res.json(); @@ -358,7 +358,7 @@ describe('#documents.pinned', async () => { expect(body.data[0].id).toEqual(document.id); }); - it('should return pinned documents in private collections member of', async () => { + it("should return pinned documents in private collections member of", async () => { const { user, collection, document } = await seed(); collection.private = true; await collection.save(); @@ -370,10 +370,10 @@ describe('#documents.pinned', async () => { collectionId: collection.id, userId: user.id, createdById: user.id, - permission: 'read_write', + permission: "read_write", }); - const res = await server.post('/api/documents.pinned', { + const res = await server.post("/api/documents.pinned", { body: { token: user.getJwtToken(), collectionId: document.collectionId }, }); const body = await res.json(); @@ -383,33 +383,33 @@ describe('#documents.pinned', async () => { expect(body.data[0].id).toEqual(document.id); }); - it('should not return pinned documents in private collections not a member of', async () => { + it("should not return pinned documents in private collections not a member of", async () => { const collection = await buildCollection({ private: true, }); const user = await buildUser({ teamId: collection.teamId }); - const res = await server.post('/api/documents.pinned', { + const res = await server.post("/api/documents.pinned", { body: { token: user.getJwtToken(), collectionId: collection.id }, }); expect(res.status).toEqual(403); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.pinned'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.pinned"); expect(res.status).toEqual(401); }); }); -describe('#documents.drafts', async () => { - it('should return unpublished documents', async () => { +describe("#documents.drafts", async () => { + it("should return unpublished documents", async () => { const { user, document } = await seed(); document.publishedAt = null; await document.save(); - const res = await server.post('/api/documents.drafts', { + const res = await server.post("/api/documents.drafts", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -418,7 +418,7 @@ describe('#documents.drafts', async () => { expect(body.data.length).toEqual(1); }); - it('should not return documents in private collections not a member of', async () => { + it("should not return documents in private collections not a member of", async () => { const { user, document, collection } = await seed(); document.publishedAt = null; await document.save(); @@ -426,7 +426,7 @@ describe('#documents.drafts', async () => { collection.private = true; await collection.save(); - const res = await server.post('/api/documents.drafts', { + const res = await server.post("/api/documents.drafts", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -436,36 +436,36 @@ describe('#documents.drafts', async () => { }); }); -describe('#documents.search', async () => { - it('should return results', async () => { +describe("#documents.search", async () => { + it("should return results", async () => { const { user } = await seed(); - const res = await server.post('/api/documents.search', { - body: { token: user.getJwtToken(), query: 'much' }, + const res = await server.post("/api/documents.search", { + body: { token: user.getJwtToken(), query: "much" }, }); const body = await res.json(); expect(res.status).toEqual(200); expect(body.data.length).toEqual(1); - expect(body.data[0].document.text).toEqual('# Much test support'); + expect(body.data[0].document.text).toEqual("# Much test support"); }); - it('should return results in ranked order', async () => { + it("should return results in ranked order", async () => { const { user } = await seed(); const firstResult = await buildDocument({ - title: 'search term', - text: 'random text', + title: "search term", + text: "random text", userId: user.id, teamId: user.teamId, }); const secondResult = await buildDocument({ - title: 'random text', - text: 'search term', + title: "random text", + text: "search term", userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/documents.search', { - body: { token: user.getJwtToken(), query: 'search term' }, + const res = await server.post("/api/documents.search", { + body: { token: user.getJwtToken(), query: "search term" }, }); const body = await res.json(); @@ -475,23 +475,23 @@ describe('#documents.search', async () => { expect(body.data[1].document.id).toEqual(secondResult.id); }); - it('should return partial results in ranked order', async () => { + it("should return partial results in ranked order", async () => { const { user } = await seed(); const firstResult = await buildDocument({ - title: 'search term', - text: 'random text', + title: "search term", + text: "random text", userId: user.id, teamId: user.teamId, }); const secondResult = await buildDocument({ - title: 'random text', - text: 'search term', + title: "random text", + text: "search term", userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/documents.search', { - body: { token: user.getJwtToken(), query: 'sear &' }, + const res = await server.post("/api/documents.search", { + body: { token: user.getJwtToken(), query: "sear &" }, }); const body = await res.json(); @@ -501,17 +501,17 @@ describe('#documents.search', async () => { expect(body.data[1].document.id).toEqual(secondResult.id); }); - it('should strip junk from search term', async () => { + it("should strip junk from search term", async () => { const { user } = await seed(); const firstResult = await buildDocument({ - title: 'search term', - text: 'this is some random text of the document body', + title: "search term", + text: "this is some random text of the document body", userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/documents.search', { - body: { token: user.getJwtToken(), query: 'rando &\\;:()' }, + const res = await server.post("/api/documents.search", { + body: { token: user.getJwtToken(), query: "rando &\\;:()" }, }); const body = await res.json(); @@ -520,19 +520,19 @@ describe('#documents.search', async () => { expect(body.data[0].document.id).toEqual(firstResult.id); }); - it('should not return draft documents', async () => { + it("should not return draft documents", async () => { const { user } = await seed(); await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", publishedAt: null, userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/documents.search', { + const res = await server.post("/api/documents.search", { body: { token: user.getJwtToken(), - query: 'search term', + query: "search term", }, }); const body = await res.json(); @@ -541,20 +541,20 @@ describe('#documents.search', async () => { expect(body.data.length).toEqual(0); }); - it('should return draft documents created by user if chosen', async () => { + it("should return draft documents created by user if chosen", async () => { const { user } = await seed(); const document = await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", publishedAt: null, userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/documents.search', { + const res = await server.post("/api/documents.search", { body: { token: user.getJwtToken(), - query: 'search term', - includeDrafts: 'true', + query: "search term", + includeDrafts: "true", }, }); const body = await res.json(); @@ -564,19 +564,19 @@ describe('#documents.search', async () => { expect(body.data[0].document.id).toEqual(document.id); }); - it('should not return draft documents created by other users', async () => { + it("should not return draft documents created by other users", async () => { const { user } = await seed(); await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", publishedAt: null, teamId: user.teamId, }); - const res = await server.post('/api/documents.search', { + const res = await server.post("/api/documents.search", { body: { token: user.getJwtToken(), - query: 'search term', - includeDrafts: 'true', + query: "search term", + includeDrafts: "true", }, }); const body = await res.json(); @@ -585,17 +585,17 @@ describe('#documents.search', async () => { expect(body.data.length).toEqual(0); }); - it('should not return archived documents', async () => { + it("should not return archived documents", async () => { const { user } = await seed(); const document = await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", teamId: user.teamId, }); await document.archive(user.id); - const res = await server.post('/api/documents.search', { - body: { token: user.getJwtToken(), query: 'search term' }, + const res = await server.post("/api/documents.search", { + body: { token: user.getJwtToken(), query: "search term" }, }); const body = await res.json(); @@ -603,20 +603,20 @@ describe('#documents.search', async () => { expect(body.data.length).toEqual(0); }); - it('should return archived documents if chosen', async () => { + it("should return archived documents if chosen", async () => { const { user } = await seed(); const document = await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", teamId: user.teamId, }); await document.archive(user.id); - const res = await server.post('/api/documents.search', { + const res = await server.post("/api/documents.search", { body: { token: user.getJwtToken(), - query: 'search term', - includeArchived: 'true', + query: "search term", + includeArchived: "true", }, }); const body = await res.json(); @@ -626,27 +626,27 @@ describe('#documents.search', async () => { expect(body.data[0].document.id).toEqual(document.id); }); - it('should return documents for a specific user', async () => { + it("should return documents for a specific user", async () => { const { user } = await seed(); const document = await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", teamId: user.teamId, userId: user.id, }); // This one will be filtered out await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", teamId: user.teamId, }); - const res = await server.post('/api/documents.search', { + const res = await server.post("/api/documents.search", { body: { token: user.getJwtToken(), - query: 'search term', + query: "search term", userId: user.id, }, }); @@ -657,7 +657,7 @@ describe('#documents.search', async () => { expect(body.data[0].document.id).toEqual(document.id); }); - it('should return documents for a specific private collection', async () => { + it("should return documents for a specific private collection", async () => { const { user, collection } = await seed(); collection.private = true; await collection.save(); @@ -666,20 +666,20 @@ describe('#documents.search', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read', + permission: "read", }); const document = await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", teamId: user.teamId, collectionId: collection.id, }); - const res = await server.post('/api/documents.search', { + const res = await server.post("/api/documents.search", { body: { token: user.getJwtToken(), - query: 'search term', + query: "search term", collectionId: collection.id, }, }); @@ -690,28 +690,28 @@ describe('#documents.search', async () => { expect(body.data[0].document.id).toEqual(document.id); }); - it('should return documents for a specific collection', async () => { + it("should return documents for a specific collection", async () => { const { user } = await seed(); const collection = await buildCollection(); const document = await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", teamId: user.teamId, }); // This one will be filtered out await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", teamId: user.teamId, collectionId: collection.id, }); - const res = await server.post('/api/documents.search', { + const res = await server.post("/api/documents.search", { body: { token: user.getJwtToken(), - query: 'search term', + query: "search term", collectionId: document.collectionId, }, }); @@ -722,19 +722,19 @@ describe('#documents.search', async () => { expect(body.data[0].document.id).toEqual(document.id); }); - it('should not return documents in private collections not a member of', async () => { + it("should not return documents in private collections not a member of", async () => { const { user } = await seed(); const collection = await buildCollection({ private: true }); await buildDocument({ - title: 'search term', - text: 'search term', + title: "search term", + text: "search term", publishedAt: null, teamId: user.teamId, collectionId: collection.id, }); - const res = await server.post('/api/documents.search', { - body: { token: user.getJwtToken(), query: 'search term' }, + const res = await server.post("/api/documents.search", { + body: { token: user.getJwtToken(), query: "search term" }, }); const body = await res.json(); @@ -742,22 +742,22 @@ describe('#documents.search', async () => { expect(body.data.length).toEqual(0); }); - it('should not allow unknown dateFilter values', async () => { + it("should not allow unknown dateFilter values", async () => { const { user } = await seed(); - const res = await server.post('/api/documents.search', { + const res = await server.post("/api/documents.search", { body: { token: user.getJwtToken(), - query: 'search term', - dateFilter: 'DROP TABLE students;', + query: "search term", + dateFilter: "DROP TABLE students;", }, }); expect(res.status).toEqual(400); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.search'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.search"); const body = await res.json(); expect(res.status).toEqual(401); @@ -765,8 +765,8 @@ describe('#documents.search', async () => { }); }); -describe('#documents.archived', async () => { - it('should return archived documents', async () => { +describe("#documents.archived", async () => { + it("should return archived documents", async () => { const { user } = await seed(); const document = await buildDocument({ userId: user.id, @@ -774,7 +774,7 @@ describe('#documents.archived', async () => { }); await document.archive(user.id); - const res = await server.post('/api/documents.archived', { + const res = await server.post("/api/documents.archived", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -783,7 +783,7 @@ describe('#documents.archived', async () => { expect(body.data.length).toEqual(1); }); - it('should not return deleted documents', async () => { + it("should not return deleted documents", async () => { const { user } = await seed(); const document = await buildDocument({ userId: user.id, @@ -791,7 +791,7 @@ describe('#documents.archived', async () => { }); await document.delete(); - const res = await server.post('/api/documents.archived', { + const res = await server.post("/api/documents.archived", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -800,7 +800,7 @@ describe('#documents.archived', async () => { expect(body.data.length).toEqual(0); }); - it('should not return documents in private collections not a member of', async () => { + it("should not return documents in private collections not a member of", async () => { const { user } = await seed(); const collection = await buildCollection({ private: true }); @@ -810,7 +810,7 @@ describe('#documents.archived', async () => { }); await document.archive(user.id); - const res = await server.post('/api/documents.archived', { + const res = await server.post("/api/documents.archived", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -819,16 +819,16 @@ describe('#documents.archived', async () => { expect(body.data.length).toEqual(0); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.archived'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.archived"); expect(res.status).toEqual(401); }); }); -describe('#documents.viewed', async () => { - it('should return empty result if no views', async () => { +describe("#documents.viewed", async () => { + it("should return empty result if no views", async () => { const { user } = await seed(); - const res = await server.post('/api/documents.viewed', { + const res = await server.post("/api/documents.viewed", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -837,11 +837,11 @@ describe('#documents.viewed', async () => { expect(body.data.length).toEqual(0); }); - it('should return recently viewed documents', async () => { + it("should return recently viewed documents", async () => { const { user, document } = await seed(); await View.increment({ documentId: document.id, userId: user.id }); - const res = await server.post('/api/documents.viewed', { + const res = await server.post("/api/documents.viewed", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -851,12 +851,12 @@ describe('#documents.viewed', async () => { expect(body.data[0].id).toEqual(document.id); }); - it('should not return recently viewed but deleted documents', async () => { + it("should not return recently viewed but deleted documents", async () => { const { user, document } = await seed(); await View.increment({ documentId: document.id, userId: user.id }); await document.destroy(); - const res = await server.post('/api/documents.viewed', { + const res = await server.post("/api/documents.viewed", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -865,13 +865,13 @@ describe('#documents.viewed', async () => { expect(body.data.length).toEqual(0); }); - it('should not return recently viewed documents in collection not a member of', async () => { + it("should not return recently viewed documents in collection not a member of", async () => { const { user, document, collection } = await seed(); await View.increment({ documentId: document.id, userId: user.id }); collection.private = true; await collection.save(); - const res = await server.post('/api/documents.viewed', { + const res = await server.post("/api/documents.viewed", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -880,8 +880,8 @@ describe('#documents.viewed', async () => { expect(body.data.length).toEqual(0); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.viewed'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.viewed"); const body = await res.json(); expect(res.status).toEqual(401); @@ -889,10 +889,10 @@ describe('#documents.viewed', async () => { }); }); -describe('#documents.starred', async () => { - it('should return empty result if no stars', async () => { +describe("#documents.starred", async () => { + it("should return empty result if no stars", async () => { const { user } = await seed(); - const res = await server.post('/api/documents.starred', { + const res = await server.post("/api/documents.starred", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -901,11 +901,11 @@ describe('#documents.starred', async () => { expect(body.data.length).toEqual(0); }); - it('should return starred documents', async () => { + it("should return starred documents", async () => { const { user, document } = await seed(); await Star.create({ documentId: document.id, userId: user.id }); - const res = await server.post('/api/documents.starred', { + const res = await server.post("/api/documents.starred", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -916,8 +916,8 @@ describe('#documents.starred', async () => { expect(body.policies[0].abilities.update).toEqual(true); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.starred'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.starred"); const body = await res.json(); expect(res.status).toEqual(401); @@ -925,11 +925,11 @@ describe('#documents.starred', async () => { }); }); -describe('#documents.pin', async () => { - it('should pin the document', async () => { +describe("#documents.pin", async () => { + it("should pin the document", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.pin', { + const res = await server.post("/api/documents.pin", { body: { token: user.getJwtToken(), id: document.id }, }); const body = await res.json(); @@ -937,37 +937,37 @@ describe('#documents.pin', async () => { expect(body.data.pinned).toEqual(true); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.pin'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.pin"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/documents.pin', { + const res = await server.post("/api/documents.pin", { body: { token: user.getJwtToken(), id: document.id }, }); expect(res.status).toEqual(403); }); }); -describe('#documents.restore', () => { - it('should allow restore of archived documents', async () => { +describe("#documents.restore", () => { + it("should allow restore of archived documents", async () => { const { user, document } = await seed(); await document.archive(user.id); - const res = await server.post('/api/documents.restore', { + const res = await server.post("/api/documents.restore", { body: { token: user.getJwtToken(), id: document.id }, }); const body = await res.json(); expect(body.data.archivedAt).toEqual(null); }); - it('should restore archived when previous parent is archived', async () => { + it("should restore archived when previous parent is archived", async () => { const { user, document } = await seed(); const childDocument = await buildDocument({ userId: user.id, @@ -978,7 +978,7 @@ describe('#documents.restore', () => { await childDocument.archive(user.id); await document.archive(user.id); - const res = await server.post('/api/documents.restore', { + const res = await server.post("/api/documents.restore", { body: { token: user.getJwtToken(), id: childDocument.id }, }); const body = await res.json(); @@ -986,7 +986,7 @@ describe('#documents.restore', () => { expect(body.data.archivedAt).toEqual(null); }); - it('should restore the document to a previous version', async () => { + it("should restore the document to a previous version", async () => { const { user, document } = await seed(); const revision = await Revision.findOne({ where: { documentId: document.id }, @@ -995,17 +995,17 @@ describe('#documents.restore', () => { const revisionId = revision.id; // update the document contents - document.text = 'UPDATED'; + document.text = "UPDATED"; await document.save(); - const res = await server.post('/api/documents.restore', { + const res = await server.post("/api/documents.restore", { body: { token: user.getJwtToken(), id: document.id, revisionId }, }); const body = await res.json(); expect(body.data.text).toEqual(previousText); }); - it('should not allow restoring a revision in another document', async () => { + it("should not allow restoring a revision in another document", async () => { const { user, document } = await seed(); const anotherDoc = await buildDocument(); const revision = await Revision.findOne({ @@ -1013,21 +1013,21 @@ describe('#documents.restore', () => { }); const revisionId = revision.id; - const res = await server.post('/api/documents.restore', { + const res = await server.post("/api/documents.restore", { body: { token: user.getJwtToken(), id: document.id, revisionId }, }); expect(res.status).toEqual(403); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.restore'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.restore"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const revision = await Revision.findOne({ where: { documentId: document.id }, @@ -1035,20 +1035,20 @@ describe('#documents.restore', () => { const revisionId = revision.id; const user = await buildUser(); - const res = await server.post('/api/documents.restore', { + const res = await server.post("/api/documents.restore", { body: { token: user.getJwtToken(), id: document.id, revisionId }, }); expect(res.status).toEqual(403); }); }); -describe('#documents.unpin', async () => { - it('should unpin the document', async () => { +describe("#documents.unpin", async () => { + it("should unpin the document", async () => { const { user, document } = await seed(); document.pinnedBy = user; await document.save(); - const res = await server.post('/api/documents.unpin', { + const res = await server.post("/api/documents.unpin", { body: { token: user.getJwtToken(), id: document.id }, }); const body = await res.json(); @@ -1056,29 +1056,29 @@ describe('#documents.unpin', async () => { expect(body.data.pinned).toEqual(false); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.unpin'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.unpin"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/documents.unpin', { + const res = await server.post("/api/documents.unpin", { body: { token: user.getJwtToken(), id: document.id }, }); expect(res.status).toEqual(403); }); }); -describe('#documents.star', async () => { - it('should star the document', async () => { +describe("#documents.star", async () => { + it("should star the document", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.star', { + const res = await server.post("/api/documents.star", { body: { token: user.getJwtToken(), id: document.id }, }); @@ -1088,30 +1088,30 @@ describe('#documents.star', async () => { expect(stars[0].documentId).toEqual(document.id); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.star'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.star"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/documents.star', { + const res = await server.post("/api/documents.star", { body: { token: user.getJwtToken(), id: document.id }, }); expect(res.status).toEqual(403); }); }); -describe('#documents.unstar', async () => { - it('should unstar the document', async () => { +describe("#documents.unstar", async () => { + it("should unstar the document", async () => { const { user, document } = await seed(); await Star.create({ documentId: document.id, userId: user.id }); - const res = await server.post('/api/documents.unstar', { + const res = await server.post("/api/documents.unstar", { body: { token: user.getJwtToken(), id: document.id }, }); @@ -1120,33 +1120,33 @@ describe('#documents.unstar', async () => { expect(stars.length).toEqual(0); }); - it('should require authentication', async () => { - const res = await server.post('/api/documents.star'); + it("should require authentication", async () => { + const res = await server.post("/api/documents.star"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/documents.unstar', { + const res = await server.post("/api/documents.unstar", { body: { token: user.getJwtToken(), id: document.id }, }); expect(res.status).toEqual(403); }); }); -describe('#documents.create', async () => { - it('should create as a new document', async () => { +describe("#documents.create", async () => { + it("should create as a new document", async () => { const { user, collection } = await seed(); - const res = await server.post('/api/documents.create', { + const res = await server.post("/api/documents.create", { body: { token: user.getJwtToken(), collectionId: collection.id, - title: 'new document', - text: 'hello', + title: "new document", + text: "hello", publish: true, }, }); @@ -1157,47 +1157,47 @@ describe('#documents.create', async () => { expect(newDocument.collectionId).toBe(collection.id); }); - it('should not allow very long titles', async () => { + it("should not allow very long titles", async () => { const { user, collection } = await seed(); - const res = await server.post('/api/documents.create', { + const res = await server.post("/api/documents.create", { body: { token: user.getJwtToken(), collectionId: collection.id, title: - 'This is a really long title that is not acceptable to Outline because it is so ridiculously long that we need to have a limit somewhere', - text: ' ', + "This is a really long title that is not acceptable to Outline because it is so ridiculously long that we need to have a limit somewhere", + text: " ", }, }); expect(res.status).toEqual(400); }); - it('should create as a child and add to collection if published', async () => { + it("should create as a child and add to collection if published", async () => { const { user, document, collection } = await seed(); - const res = await server.post('/api/documents.create', { + const res = await server.post("/api/documents.create", { body: { token: user.getJwtToken(), collectionId: collection.id, parentDocumentId: document.id, - title: 'new document', - text: 'hello', + title: "new document", + text: "hello", publish: true, }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.title).toBe('new document'); + expect(body.data.title).toBe("new document"); }); - it('should error with invalid parentDocument', async () => { + it("should error with invalid parentDocument", async () => { const { user, collection } = await seed(); - const res = await server.post('/api/documents.create', { + const res = await server.post("/api/documents.create", { body: { token: user.getJwtToken(), collectionId: collection.id, - parentDocumentId: 'd7a4eb73-fac1-4028-af45-d7e34d54db8e', - title: 'new document', - text: 'hello', + parentDocumentId: "d7a4eb73-fac1-4028-af45-d7e34d54db8e", + title: "new document", + text: "hello", }, }); const body = await res.json(); @@ -1206,45 +1206,45 @@ describe('#documents.create', async () => { expect(body).toMatchSnapshot(); }); - it('should create as a child and not add to collection', async () => { + it("should create as a child and not add to collection", async () => { const { user, document, collection } = await seed(); - const res = await server.post('/api/documents.create', { + const res = await server.post("/api/documents.create", { body: { token: user.getJwtToken(), collectionId: collection.id, parentDocumentId: document.id, - title: 'new document', - text: 'hello', + title: "new document", + text: "hello", }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.title).toBe('new document'); + expect(body.data.title).toBe("new document"); }); }); -describe('#documents.update', async () => { - it('should update document details in the root', async () => { +describe("#documents.update", async () => { + it("should update document details in the root", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, - title: 'Updated title', - text: 'Updated text', + title: "Updated title", + text: "Updated text", lastRevision: document.revision, }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.title).toBe('Updated title'); - expect(body.data.text).toBe('Updated text'); + expect(body.data.title).toBe("Updated title"); + expect(body.data.text).toBe("Updated text"); }); - it('should allow publishing document in private collection', async () => { + it("should allow publishing document in private collection", async () => { const { user, collection, document } = await seed(); document.publishedAt = null; await document.save(); @@ -1256,15 +1256,15 @@ describe('#documents.update', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read_write', + permission: "read_write", }); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, - title: 'Updated title', - text: 'Updated text', + title: "Updated title", + text: "Updated text", lastRevision: document.revision, publish: true, }, @@ -1276,31 +1276,31 @@ describe('#documents.update', async () => { expect(body.policies[0].abilities.update).toEqual(true); }); - it('should not edit archived document', async () => { + it("should not edit archived document", async () => { const { user, document } = await seed(); await document.archive(); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, - title: 'Updated title', - text: 'Updated text', + title: "Updated title", + text: "Updated text", lastRevision: document.revision, }, }); expect(res.status).toEqual(403); }); - it('should not create new version when autosave=true', async () => { + it("should not create new version when autosave=true", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, - title: 'Updated title', - text: 'Updated text', + title: "Updated title", + text: "Updated text", lastRevision: document.revision, autosave: true, }, @@ -1310,21 +1310,21 @@ describe('#documents.update', async () => { const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.title).toBe('Updated title'); - expect(body.data.text).toBe('Updated text'); + expect(body.data.title).toBe("Updated title"); + expect(body.data.text).toBe("Updated text"); const revisionRecords = await Revision.count(); expect(revisionRecords).toBe(prevRevisionRecords); }); - it('should fail if document lastRevision does not match', async () => { + it("should fail if document lastRevision does not match", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, - text: 'Updated text', + text: "Updated text", lastRevision: 123, }, }); @@ -1334,18 +1334,18 @@ describe('#documents.update', async () => { expect(body).toMatchSnapshot(); }); - it('should update document details for children', async () => { + it("should update document details for children", async () => { const { user, document, collection } = await seed(); collection.documentStructure = [ { - id: 'af1da94b-9591-4bab-897c-11774b804b77', - url: '/d/some-beef-RSZwQDsfpc', - title: 'some beef', + id: "af1da94b-9591-4bab-897c-11774b804b77", + url: "/d/some-beef-RSZwQDsfpc", + title: "some beef", children: [ { - id: 'ab1da94b-9591-4bab-897c-11774b804b66', - url: '/d/another-doc-RSZwQDsfpc', - title: 'Another doc', + id: "ab1da94b-9591-4bab-897c-11774b804b66", + url: "/d/another-doc-RSZwQDsfpc", + title: "Another doc", children: [], }, { ...document.toJSON(), children: [] }, @@ -1354,20 +1354,20 @@ describe('#documents.update', async () => { ]; await collection.save(); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, - title: 'Updated title', + title: "Updated title", }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.title).toBe('Updated title'); + expect(body.data.title).toBe("Updated title"); }); - it('allows editing by read-write collection user', async () => { + it("allows editing by read-write collection user", async () => { const { admin, document, collection } = await seed(); collection.private = true; await collection.save(); @@ -1376,14 +1376,14 @@ describe('#documents.update', async () => { collectionId: collection.id, userId: admin.id, createdById: admin.id, - permission: 'read_write', + permission: "read_write", }); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: admin.getJwtToken(), id: document.id, - text: 'Changed text', + text: "Changed text", lastRevision: document.revision, }, }); @@ -1391,11 +1391,11 @@ describe('#documents.update', async () => { const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.text).toBe('Changed text'); + expect(body.data.text).toBe("Changed text"); expect(body.data.updatedBy.id).toBe(admin.id); }); - it('does not allow editing by read-only collection user', async () => { + it("does not allow editing by read-only collection user", async () => { const { user, document, collection } = await seed(); collection.private = true; await collection.save(); @@ -1404,14 +1404,14 @@ describe('#documents.update', async () => { collectionId: collection.id, userId: user.id, createdById: user.id, - permission: 'read', + permission: "read", }); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, - text: 'Changed text', + text: "Changed text", lastRevision: document.revision, }, }); @@ -1419,14 +1419,14 @@ describe('#documents.update', async () => { expect(res.status).toEqual(403); }); - it('should append document with text', async () => { + it("should append document with text", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, - text: 'Additional text', + text: "Additional text", lastRevision: document.revision, append: true, }, @@ -1434,19 +1434,19 @@ describe('#documents.update', async () => { const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.text).toBe(document.text + 'Additional text'); + expect(body.data.text).toBe(document.text + "Additional text"); expect(body.data.updatedBy.id).toBe(user.id); }); - it('should require text while appending', async () => { + it("should require text while appending", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, lastRevision: document.revision, - title: 'Updated Title', + title: "Updated Title", append: true, }, }); @@ -1456,28 +1456,28 @@ describe('#documents.update', async () => { expect(body).toMatchSnapshot(); }); - it('should allow setting empty text', async () => { + it("should allow setting empty text", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.update', { + const res = await server.post("/api/documents.update", { body: { token: user.getJwtToken(), id: document.id, lastRevision: document.revision, - title: 'Updated Title', - text: '', + title: "Updated Title", + text: "", }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.text).toBe(''); + expect(body.data.text).toBe(""); }); - it('should require authentication', async () => { + it("should require authentication", async () => { const { document } = await seed(); - const res = await server.post('/api/documents.update', { - body: { id: document.id, text: 'Updated' }, + const res = await server.post("/api/documents.update", { + body: { id: document.id, text: "Updated" }, }); const body = await res.json(); @@ -1485,20 +1485,20 @@ describe('#documents.update', async () => { expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/documents.update', { - body: { token: user.getJwtToken(), id: document.id, text: 'Updated' }, + const res = await server.post("/api/documents.update", { + body: { token: user.getJwtToken(), id: document.id, text: "Updated" }, }); expect(res.status).toEqual(403); }); }); -describe('#documents.archive', async () => { - it('should allow archiving document', async () => { +describe("#documents.archive", async () => { + it("should allow archiving document", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.archive', { + const res = await server.post("/api/documents.archive", { body: { token: user.getJwtToken(), id: document.id }, }); const body = await res.json(); @@ -1508,19 +1508,19 @@ describe('#documents.archive', async () => { expect(body.data.archivedAt).toBeTruthy(); }); - it('should require authentication', async () => { + it("should require authentication", async () => { const { document } = await seed(); - const res = await server.post('/api/documents.archive', { + const res = await server.post("/api/documents.archive", { body: { id: document.id }, }); expect(res.status).toEqual(401); }); }); -describe('#documents.delete', async () => { - it('should allow deleting document', async () => { +describe("#documents.delete", async () => { + it("should allow deleting document", async () => { const { user, document } = await seed(); - const res = await server.post('/api/documents.delete', { + const res = await server.post("/api/documents.delete", { body: { token: user.getJwtToken(), id: document.id }, }); const body = await res.json(); @@ -1529,12 +1529,12 @@ describe('#documents.delete', async () => { expect(body.success).toEqual(true); }); - it('should allow deleting document without collection', async () => { + it("should allow deleting document without collection", async () => { const { user, document, collection } = await seed(); // delete collection without hooks to trigger document deletion await collection.destroy({ hooks: false }); - const res = await server.post('/api/documents.delete', { + const res = await server.post("/api/documents.delete", { body: { token: user.getJwtToken(), id: document.id }, }); const body = await res.json(); @@ -1543,9 +1543,9 @@ describe('#documents.delete', async () => { expect(body.success).toEqual(true); }); - it('should require authentication', async () => { + it("should require authentication", async () => { const { document } = await seed(); - const res = await server.post('/api/documents.delete', { + const res = await server.post("/api/documents.delete", { body: { id: document.id }, }); const body = await res.json(); diff --git a/server/api/events.js b/server/api/events.js index 84e4fc7c..3b0b3dd8 100644 --- a/server/api/events.js +++ b/server/api/events.js @@ -1,19 +1,19 @@ // @flow -import Sequelize from 'sequelize'; -import Router from 'koa-router'; -import auth from '../middlewares/authentication'; -import pagination from './middlewares/pagination'; -import { presentEvent } from '../presenters'; -import { Event, Team, User } from '../models'; -import policy from '../policies'; +import Sequelize from "sequelize"; +import Router from "koa-router"; +import auth from "../middlewares/authentication"; +import pagination from "./middlewares/pagination"; +import { presentEvent } from "../presenters"; +import { Event, Team, User } from "../models"; +import policy from "../policies"; const Op = Sequelize.Op; const { authorize } = policy; const router = new Router(); -router.post('events.list', auth(), pagination(), async ctx => { - let { sort = 'createdAt', direction, auditLog = false } = ctx.body; - if (direction !== 'ASC') direction = 'DESC'; +router.post("events.list", auth(), pagination(), async ctx => { + let { sort = "createdAt", direction, auditLog = false } = ctx.body; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; const paranoid = false; @@ -33,7 +33,7 @@ router.post('events.list', auth(), pagination(), async ctx => { }; if (auditLog) { - authorize(user, 'auditLog', Team); + authorize(user, "auditLog", Team); where.name = Event.AUDIT_EVENTS; } @@ -43,7 +43,7 @@ router.post('events.list', auth(), pagination(), async ctx => { include: [ { model: User, - as: 'actor', + as: "actor", paranoid: false, }, ], diff --git a/server/api/events.test.js b/server/api/events.test.js index 6e6e037d..373871bb 100644 --- a/server/api/events.test.js +++ b/server/api/events.test.js @@ -1,21 +1,21 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; -import { flushdb, seed } from '../test/support'; -import { buildEvent } from '../test/factories'; +import TestServer from "fetch-test-server"; +import app from "../app"; +import { flushdb, seed } from "../test/support"; +import { buildEvent } from "../test/factories"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#events.list', async () => { - it('should only return activity events', async () => { +describe("#events.list", async () => { + it("should only return activity events", async () => { const { user, admin, document, collection } = await seed(); // private event await buildEvent({ - name: 'users.promote', + name: "users.promote", teamId: user.teamId, actorId: admin.id, userId: user.id, @@ -23,13 +23,13 @@ describe('#events.list', async () => { // event viewable in activity stream const event = await buildEvent({ - name: 'documents.publish', + name: "documents.publish", collectionId: collection.id, documentId: document.id, teamId: user.teamId, actorId: admin.id, }); - const res = await server.post('/api/events.list', { + const res = await server.post("/api/events.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -39,12 +39,12 @@ describe('#events.list', async () => { expect(body.data[0].id).toEqual(event.id); }); - it('should return events with deleted actors', async () => { + it("should return events with deleted actors", async () => { const { user, admin, document, collection } = await seed(); // event viewable in activity stream const event = await buildEvent({ - name: 'documents.publish', + name: "documents.publish", collectionId: collection.id, documentId: document.id, teamId: user.teamId, @@ -53,7 +53,7 @@ describe('#events.list', async () => { await user.destroy(); - const res = await server.post('/api/events.list', { + const res = await server.post("/api/events.list", { body: { token: admin.getJwtToken() }, }); @@ -64,8 +64,8 @@ describe('#events.list', async () => { expect(body.data[0].id).toEqual(event.id); }); - it('should require authentication', async () => { - const res = await server.post('/api/events.list'); + it("should require authentication", async () => { + const res = await server.post("/api/events.list"); const body = await res.json(); expect(res.status).toEqual(401); diff --git a/server/api/groups.js b/server/api/groups.js index 4aceb55a..5dbbefe3 100644 --- a/server/api/groups.js +++ b/server/api/groups.js @@ -1,26 +1,26 @@ // @flow -import Router from 'koa-router'; -import auth from '../middlewares/authentication'; -import pagination from './middlewares/pagination'; -import { Op } from '../sequelize'; -import { MAX_AVATAR_DISPLAY } from '../../shared/constants'; +import Router from "koa-router"; +import auth from "../middlewares/authentication"; +import pagination from "./middlewares/pagination"; +import { Op } from "../sequelize"; +import { MAX_AVATAR_DISPLAY } from "../../shared/constants"; import { presentGroup, presentPolicies, presentUser, presentGroupMembership, -} from '../presenters'; -import { User, Event, Group, GroupUser } from '../models'; -import policy from '../policies'; +} from "../presenters"; +import { User, Event, Group, GroupUser } from "../models"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -router.post('groups.list', auth(), pagination(), async ctx => { - const { sort = 'updatedAt' } = ctx.body; +router.post("groups.list", auth(), pagination(), async ctx => { + const { sort = "updatedAt" } = ctx.body; let direction = ctx.body.direction; - if (direction !== 'ASC') direction = 'DESC'; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; let groups = await Group.findAll({ @@ -55,13 +55,13 @@ router.post('groups.list', auth(), pagination(), async ctx => { }; }); -router.post('groups.info', auth(), async ctx => { +router.post("groups.info", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const group = await Group.findByPk(id); - authorize(user, 'read', group); + authorize(user, "read", group); ctx.body = { data: presentGroup(group), @@ -69,13 +69,13 @@ router.post('groups.info', auth(), async ctx => { }; }); -router.post('groups.create', auth(), async ctx => { +router.post("groups.create", auth(), async ctx => { const { name } = ctx.body; - ctx.assertPresent(name, 'name is required'); + ctx.assertPresent(name, "name is required"); const user = ctx.state.user; - authorize(user, 'create', Group); + authorize(user, "create", Group); let group = await Group.create({ name, teamId: user.teamId, @@ -86,7 +86,7 @@ router.post('groups.create', auth(), async ctx => { group = await Group.findByPk(group.id); await Event.create({ - name: 'groups.create', + name: "groups.create", actorId: user.id, teamId: user.teamId, modelId: group.id, @@ -100,22 +100,22 @@ router.post('groups.create', auth(), async ctx => { }; }); -router.post('groups.update', auth(), async ctx => { +router.post("groups.update", auth(), async ctx => { const { id, name } = ctx.body; - ctx.assertPresent(name, 'name is required'); - ctx.assertUuid(id, 'id is required'); + ctx.assertPresent(name, "name is required"); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const group = await Group.findByPk(id); - authorize(user, 'update', group); + authorize(user, "update", group); group.name = name; if (group.changed()) { await group.save(); await Event.create({ - name: 'groups.update', + name: "groups.update", teamId: user.teamId, actorId: user.id, modelId: group.id, @@ -130,18 +130,18 @@ router.post('groups.update', auth(), async ctx => { }; }); -router.post('groups.delete', auth(), async ctx => { +router.post("groups.delete", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const { user } = ctx.state; const group = await Group.findByPk(id); - authorize(user, 'delete', group); + authorize(user, "delete", group); await group.destroy(); await Event.create({ - name: 'groups.delete', + name: "groups.delete", actorId: user.id, modelId: group.id, teamId: group.teamId, @@ -154,13 +154,13 @@ router.post('groups.delete', auth(), async ctx => { }; }); -router.post('groups.memberships', auth(), pagination(), async ctx => { +router.post("groups.memberships", auth(), pagination(), async ctx => { const { id, query } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const group = await Group.findByPk(id); - authorize(user, 'read', group); + authorize(user, "read", group); let userWhere; if (query) { @@ -173,13 +173,13 @@ router.post('groups.memberships', auth(), pagination(), async ctx => { const memberships = await GroupUser.findAll({ where: { groupId: id }, - order: [['createdAt', 'DESC']], + order: [["createdAt", "DESC"]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, include: [ { model: User, - as: 'user', + as: "user", where: userWhere, required: true, }, @@ -195,16 +195,16 @@ router.post('groups.memberships', auth(), pagination(), async ctx => { }; }); -router.post('groups.add_user', auth(), async ctx => { +router.post("groups.add_user", auth(), async ctx => { const { id, userId } = ctx.body; - ctx.assertUuid(id, 'id is required'); - ctx.assertUuid(userId, 'userId is required'); + ctx.assertUuid(id, "id is required"); + ctx.assertUuid(userId, "userId is required"); const user = await User.findByPk(userId); - authorize(ctx.state.user, 'read', user); + authorize(ctx.state.user, "read", user); let group = await Group.findByPk(id); - authorize(ctx.state.user, 'update', group); + authorize(ctx.state.user, "update", group); let membership = await GroupUser.findOne({ where: { @@ -230,7 +230,7 @@ router.post('groups.add_user', auth(), async ctx => { group = await Group.findByPk(id); await Event.create({ - name: 'groups.add_user', + name: "groups.add_user", userId, teamId: user.teamId, modelId: group.id, @@ -249,21 +249,21 @@ router.post('groups.add_user', auth(), async ctx => { }; }); -router.post('groups.remove_user', auth(), async ctx => { +router.post("groups.remove_user", auth(), async ctx => { const { id, userId } = ctx.body; - ctx.assertUuid(id, 'id is required'); - ctx.assertUuid(userId, 'userId is required'); + ctx.assertUuid(id, "id is required"); + ctx.assertUuid(userId, "userId is required"); let group = await Group.findByPk(id); - authorize(ctx.state.user, 'update', group); + authorize(ctx.state.user, "update", group); const user = await User.findByPk(userId); - authorize(ctx.state.user, 'read', user); + authorize(ctx.state.user, "read", user); await group.removeUser(user); await Event.create({ - name: 'groups.remove_user', + name: "groups.remove_user", userId, modelId: group.id, teamId: user.teamId, diff --git a/server/api/groups.test.js b/server/api/groups.test.js index 9381721b..732eec9c 100644 --- a/server/api/groups.test.js +++ b/server/api/groups.test.js @@ -1,21 +1,21 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; -import { flushdb } from '../test/support'; -import { buildUser, buildGroup } from '../test/factories'; -import { Event } from '../models'; +import TestServer from "fetch-test-server"; +import app from "../app"; +import { flushdb } from "../test/support"; +import { buildUser, buildGroup } from "../test/factories"; +import { Event } from "../models"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#groups.create', async () => { - it('should create a group', async () => { - const name = 'hello I am a group'; +describe("#groups.create", async () => { + it("should create a group", async () => { + const name = "hello I am a group"; const user = await buildUser({ isAdmin: true }); - const res = await server.post('/api/groups.create', { + const res = await server.post("/api/groups.create", { body: { token: user.getJwtToken(), name }, }); @@ -26,11 +26,11 @@ describe('#groups.create', async () => { }); }); -describe('#groups.update', async () => { - it('should require authentication', async () => { +describe("#groups.update", async () => { + it("should require authentication", async () => { const group = await buildGroup(); - const res = await server.post('/api/groups.update', { - body: { id: group.id, name: 'Test' }, + const res = await server.post("/api/groups.update", { + body: { id: group.id, name: "Test" }, }); const body = await res.json(); @@ -38,26 +38,26 @@ describe('#groups.update', async () => { expect(body).toMatchSnapshot(); }); - it('should require admin', async () => { + it("should require admin", async () => { const group = await buildGroup(); const user = await buildUser(); - const res = await server.post('/api/groups.update', { - body: { token: user.getJwtToken(), id: group.id, name: 'Test' }, + const res = await server.post("/api/groups.update", { + body: { token: user.getJwtToken(), id: group.id, name: "Test" }, }); expect(res.status).toEqual(403); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const group = await buildGroup(); const user = await buildUser({ isAdmin: true }); - const res = await server.post('/api/groups.update', { - body: { token: user.getJwtToken(), id: group.id, name: 'Test' }, + const res = await server.post("/api/groups.update", { + body: { token: user.getJwtToken(), id: group.id, name: "Test" }, }); expect(res.status).toEqual(403); }); - describe('when user is admin', async () => { + describe("when user is admin", async () => { let user, group; beforeEach(async () => { @@ -65,9 +65,9 @@ describe('#groups.update', async () => { group = await buildGroup({ teamId: user.teamId }); }); - it('allows admin to edit a group', async () => { - const res = await server.post('/api/groups.update', { - body: { token: user.getJwtToken(), id: group.id, name: 'Test' }, + it("allows admin to edit a group", async () => { + const res = await server.post("/api/groups.update", { + body: { token: user.getJwtToken(), id: group.id, name: "Test" }, }); const events = await Event.findAll(); @@ -75,11 +75,11 @@ describe('#groups.update', async () => { const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.name).toBe('Test'); + expect(body.data.name).toBe("Test"); }); - it('does not create an event if the update is a noop', async () => { - const res = await server.post('/api/groups.update', { + it("does not create an event if the update is a noop", async () => { + const res = await server.post("/api/groups.update", { body: { token: user.getJwtToken(), id: group.id, name: group.name }, }); @@ -91,17 +91,17 @@ describe('#groups.update', async () => { expect(body.data.name).toBe(group.name); }); - it('fails with validation error when name already taken', async () => { + it("fails with validation error when name already taken", async () => { await buildGroup({ teamId: user.teamId, - name: 'test', + name: "test", }); - const res = await server.post('/api/groups.update', { + const res = await server.post("/api/groups.update", { body: { token: user.getJwtToken(), id: group.id, - name: 'TEST', + name: "TEST", }, }); @@ -112,40 +112,40 @@ describe('#groups.update', async () => { }); }); -describe('#groups.list', async () => { - it('should require authentication', async () => { - const res = await server.post('/api/groups.list'); +describe("#groups.list", async () => { + it("should require authentication", async () => { + const res = await server.post("/api/groups.list"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should return groups with memberships preloaded', async () => { + it("should return groups with memberships preloaded", async () => { const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId }); await group.addUser(user, { through: { createdById: user.id } }); - const res = await server.post('/api/groups.list', { + const res = await server.post("/api/groups.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data['groups'].length).toEqual(1); - expect(body.data['groups'][0].id).toEqual(group.id); + expect(body.data["groups"].length).toEqual(1); + expect(body.data["groups"][0].id).toEqual(group.id); - expect(body.data['groupMemberships'].length).toEqual(1); - expect(body.data['groupMemberships'][0].groupId).toEqual(group.id); - expect(body.data['groupMemberships'][0].user.id).toEqual(user.id); + expect(body.data["groupMemberships"].length).toEqual(1); + expect(body.data["groupMemberships"][0].groupId).toEqual(group.id); + expect(body.data["groupMemberships"][0].user.id).toEqual(user.id); expect(body.policies.length).toEqual(1); expect(body.policies[0].abilities.read).toEqual(true); }); - it('should return groups when membership user is deleted', async () => { + it("should return groups when membership user is deleted", async () => { const me = await buildUser(); const user = await buildUser({ teamId: me.teamId }); const group = await buildGroup({ teamId: user.teamId }); @@ -154,31 +154,31 @@ describe('#groups.list', async () => { await group.addUser(me, { through: { createdById: me.id } }); await user.destroy(); - const res = await server.post('/api/groups.list', { + const res = await server.post("/api/groups.list", { body: { token: me.getJwtToken() }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data['groups'].length).toEqual(1); - expect(body.data['groups'][0].id).toEqual(group.id); + expect(body.data["groups"].length).toEqual(1); + expect(body.data["groups"][0].id).toEqual(group.id); - expect(body.data['groupMemberships'].length).toEqual(1); - expect(body.data['groupMemberships'][0].groupId).toEqual(group.id); - expect(body.data['groupMemberships'][0].user.id).toEqual(me.id); + expect(body.data["groupMemberships"].length).toEqual(1); + expect(body.data["groupMemberships"][0].groupId).toEqual(group.id); + expect(body.data["groupMemberships"][0].user.id).toEqual(me.id); expect(body.policies.length).toEqual(1); expect(body.policies[0].abilities.read).toEqual(true); }); }); -describe('#groups.info', async () => { - it('should return group if admin', async () => { +describe("#groups.info", async () => { + it("should return group if admin", async () => { const user = await buildUser({ isAdmin: true }); const group = await buildGroup({ teamId: user.teamId }); - const res = await server.post('/api/groups.info', { + const res = await server.post("/api/groups.info", { body: { token: user.getJwtToken(), id: group.id }, }); @@ -188,12 +188,12 @@ describe('#groups.info', async () => { expect(body.data.id).toEqual(group.id); }); - it('should return group if member', async () => { + it("should return group if member", async () => { const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId }); await group.addUser(user, { through: { createdById: user.id } }); - const res = await server.post('/api/groups.info', { + const res = await server.post("/api/groups.info", { body: { token: user.getJwtToken(), id: group.id }, }); @@ -203,39 +203,39 @@ describe('#groups.info', async () => { expect(body.data.id).toEqual(group.id); }); - it('should not return group if non-member, non-admin', async () => { + it("should not return group if non-member, non-admin", async () => { const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId }); - const res = await server.post('/api/groups.info', { + const res = await server.post("/api/groups.info", { body: { token: user.getJwtToken(), id: group.id }, }); expect(res.status).toEqual(403); }); - it('should require authentication', async () => { - const res = await server.post('/api/groups.info'); + it("should require authentication", async () => { + const res = await server.post("/api/groups.info"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const user = await buildUser(); const group = await buildGroup(); - const res = await server.post('/api/groups.info', { + const res = await server.post("/api/groups.info", { body: { token: user.getJwtToken(), id: group.id }, }); expect(res.status).toEqual(403); }); }); -describe('#groups.delete', async () => { - it('should require authentication', async () => { +describe("#groups.delete", async () => { + it("should require authentication", async () => { const group = await buildGroup(); - const res = await server.post('/api/groups.delete', { + const res = await server.post("/api/groups.delete", { body: { id: group.id }, }); const body = await res.json(); @@ -244,30 +244,30 @@ describe('#groups.delete', async () => { expect(body).toMatchSnapshot(); }); - it('should require admin', async () => { + it("should require admin", async () => { const group = await buildGroup(); const user = await buildUser(); - const res = await server.post('/api/groups.delete', { + const res = await server.post("/api/groups.delete", { body: { token: user.getJwtToken(), id: group.id }, }); expect(res.status).toEqual(403); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const group = await buildGroup(); const user = await buildUser({ isAdmin: true }); - const res = await server.post('/api/groups.delete', { + const res = await server.post("/api/groups.delete", { body: { token: user.getJwtToken(), id: group.id }, }); expect(res.status).toEqual(403); }); - it('allows admin to delete a group', async () => { + it("allows admin to delete a group", async () => { const user = await buildUser({ isAdmin: true }); const group = await buildGroup({ teamId: user.teamId }); - const res = await server.post('/api/groups.delete', { + const res = await server.post("/api/groups.delete", { body: { token: user.getJwtToken(), id: group.id }, }); @@ -277,14 +277,14 @@ describe('#groups.delete', async () => { }); }); -describe('#groups.memberships', async () => { - it('should return members in a group', async () => { +describe("#groups.memberships", async () => { + it("should return members in a group", async () => { const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId }); await group.addUser(user, { through: { createdById: user.id } }); - const res = await server.post('/api/groups.memberships', { + const res = await server.post("/api/groups.memberships", { body: { token: user.getJwtToken(), id: group.id }, }); @@ -297,10 +297,10 @@ describe('#groups.memberships', async () => { expect(body.data.groupMemberships[0].user.id).toEqual(user.id); }); - it('should allow filtering members in group by name', async () => { + it("should allow filtering members in group by name", async () => { const user = await buildUser(); const user2 = await buildUser({ name: "Won't find" }); - const user3 = await buildUser({ teamId: user.teamId, name: 'Deleted' }); + const user3 = await buildUser({ teamId: user.teamId, name: "Deleted" }); const group = await buildGroup({ teamId: user.teamId }); await group.addUser(user, { through: { createdById: user.id } }); @@ -309,7 +309,7 @@ describe('#groups.memberships', async () => { await user3.destroy(); - const res = await server.post('/api/groups.memberships', { + const res = await server.post("/api/groups.memberships", { body: { token: user.getJwtToken(), id: group.id, @@ -323,33 +323,33 @@ describe('#groups.memberships', async () => { expect(body.data.users[0].id).toEqual(user.id); }); - it('should require authentication', async () => { - const res = await server.post('/api/groups.memberships'); + it("should require authentication", async () => { + const res = await server.post("/api/groups.memberships"); const body = await res.json(); expect(res.status).toEqual(401); expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const user = await buildUser(); const group = await buildGroup(); - const res = await server.post('/api/groups.memberships', { + const res = await server.post("/api/groups.memberships", { body: { token: user.getJwtToken(), id: group.id }, }); expect(res.status).toEqual(403); }); }); -describe('#groups.add_user', async () => { - it('should add user to group', async () => { +describe("#groups.add_user", async () => { + it("should add user to group", async () => { const user = await buildUser({ isAdmin: true }); const group = await buildGroup({ teamId: user.teamId, }); - const res = await server.post('/api/groups.add_user', { + const res = await server.post("/api/groups.add_user", { body: { token: user.getJwtToken(), id: group.id, @@ -362,19 +362,19 @@ describe('#groups.add_user', async () => { expect(users.length).toEqual(1); }); - it('should require authentication', async () => { - const res = await server.post('/api/groups.add_user'); + it("should require authentication", async () => { + const res = await server.post("/api/groups.add_user"); expect(res.status).toEqual(401); }); - it('should require user in team', async () => { + it("should require user in team", async () => { const user = await buildUser({ isAdmin: true }); const group = await buildGroup({ teamId: user.teamId, }); const anotherUser = await buildUser(); - const res = await server.post('/api/groups.add_user', { + const res = await server.post("/api/groups.add_user", { body: { token: user.getJwtToken(), id: group.id, @@ -388,14 +388,14 @@ describe('#groups.add_user', async () => { expect(body).toMatchSnapshot(); }); - it('should require admin', async () => { + it("should require admin", async () => { const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId, }); const anotherUser = await buildUser({ teamId: user.teamId }); - const res = await server.post('/api/groups.add_user', { + const res = await server.post("/api/groups.add_user", { body: { token: user.getJwtToken(), id: group.id, @@ -410,14 +410,14 @@ describe('#groups.add_user', async () => { }); }); -describe('#groups.remove_user', async () => { - it('should remove user from group', async () => { +describe("#groups.remove_user", async () => { + it("should remove user from group", async () => { const user = await buildUser({ isAdmin: true }); const group = await buildGroup({ teamId: user.teamId, }); - await server.post('/api/groups.add_user', { + await server.post("/api/groups.add_user", { body: { token: user.getJwtToken(), id: group.id, @@ -428,7 +428,7 @@ describe('#groups.remove_user', async () => { const users = await group.getUsers(); expect(users.length).toEqual(1); - const res = await server.post('/api/groups.remove_user', { + const res = await server.post("/api/groups.remove_user", { body: { token: user.getJwtToken(), id: group.id, @@ -441,20 +441,20 @@ describe('#groups.remove_user', async () => { expect(users1.length).toEqual(0); }); - it('should require authentication', async () => { - const res = await server.post('/api/groups.remove_user'); + it("should require authentication", async () => { + const res = await server.post("/api/groups.remove_user"); expect(res.status).toEqual(401); }); - it('should require user in team', async () => { + it("should require user in team", async () => { const user = await buildUser({ isAdmin: true }); const group = await buildGroup({ teamId: user.teamId, }); const anotherUser = await buildUser(); - const res = await server.post('/api/groups.remove_user', { + const res = await server.post("/api/groups.remove_user", { body: { token: user.getJwtToken(), id: group.id, @@ -467,7 +467,7 @@ describe('#groups.remove_user', async () => { expect(body).toMatchSnapshot(); }); - it('should require admin', async () => { + it("should require admin", async () => { const user = await buildUser(); const group = await buildGroup({ teamId: user.teamId, @@ -476,7 +476,7 @@ describe('#groups.remove_user', async () => { teamId: user.teamId, }); - const res = await server.post('/api/groups.remove_user', { + const res = await server.post("/api/groups.remove_user", { body: { token: user.getJwtToken(), id: group.id, diff --git a/server/api/hooks.js b/server/api/hooks.js index 118a2313..641ec1a4 100644 --- a/server/api/hooks.js +++ b/server/api/hooks.js @@ -1,35 +1,35 @@ // @flow -import Router from 'koa-router'; -import { escapeRegExp } from 'lodash'; -import { AuthenticationError, InvalidRequestError } from '../errors'; -import { Authentication, Document, User, Team, Collection } from '../models'; -import { presentSlackAttachment } from '../presenters'; -import * as Slack from '../slack'; +import Router from "koa-router"; +import { escapeRegExp } from "lodash"; +import { AuthenticationError, InvalidRequestError } from "../errors"; +import { Authentication, Document, User, Team, Collection } from "../models"; +import { presentSlackAttachment } from "../presenters"; +import * as Slack from "../slack"; const router = new Router(); // triggered by a user posting a getoutline.com link in Slack -router.post('hooks.unfurl', async ctx => { +router.post("hooks.unfurl", async ctx => { const { challenge, token, event } = ctx.body; if (challenge) return (ctx.body = ctx.body.challenge); if (token !== process.env.SLACK_VERIFICATION_TOKEN) { - throw new AuthenticationError('Invalid token'); + throw new AuthenticationError("Invalid token"); } const user = await User.findOne({ - where: { service: 'slack', serviceId: event.user }, + where: { service: "slack", serviceId: event.user }, }); if (!user) return; const auth = await Authentication.findOne({ - where: { service: 'slack', teamId: user.teamId }, + where: { service: "slack", teamId: user.teamId }, }); if (!auth) return; // get content for unfurled links let unfurls = {}; for (let link of event.links) { - const id = link.url.substr(link.url.lastIndexOf('/') + 1); + const id = link.url.substr(link.url.lastIndexOf("/") + 1); const doc = await Document.findByPk(id); if (!doc || doc.teamId !== user.teamId) continue; @@ -40,7 +40,7 @@ router.post('hooks.unfurl', async ctx => { }; } - await Slack.post('chat.unfurl', { + await Slack.post("chat.unfurl", { token: auth.token, channel: event.channel, ts: event.message_ts, @@ -49,17 +49,17 @@ router.post('hooks.unfurl', async ctx => { }); // triggered by interactions with actions, dialogs, message buttons in Slack -router.post('hooks.interactive', async ctx => { +router.post("hooks.interactive", async ctx => { const { payload } = ctx.body; - ctx.assertPresent(payload, 'payload is required'); + ctx.assertPresent(payload, "payload is required"); const data = JSON.parse(payload); const { callback_id, token } = data; - ctx.assertPresent(token, 'token is required'); - ctx.assertPresent(callback_id, 'callback_id is required'); + ctx.assertPresent(token, "token is required"); + ctx.assertPresent(callback_id, "callback_id is required"); if (token !== process.env.SLACK_VERIFICATION_TOKEN) { - throw new AuthenticationError('Invalid verification token'); + throw new AuthenticationError("Invalid verification token"); } const team = await Team.findOne({ @@ -69,8 +69,8 @@ router.post('hooks.interactive', async ctx => { if (!team) { ctx.body = { text: - 'Sorry, we couldn’t find an integration for your team. Head to your Outline settings to set one up.', - response_type: 'ephemeral', + "Sorry, we couldn’t find an integration for your team. Head to your Outline settings to set one up.", + response_type: "ephemeral", replace_original: false, }; return; @@ -83,13 +83,13 @@ router.post('hooks.interactive', async ctx => { teamId: team.id, }, }); - if (!document) throw new InvalidRequestError('Invalid document'); + if (!document) throw new InvalidRequestError("Invalid document"); const collection = await Collection.findByPk(document.collectionId); // respond with a public message that will be posted in the original channel ctx.body = { - response_type: 'in_channel', + response_type: "in_channel", replace_original: false, attachments: [ presentSlackAttachment(document, collection, team, document.getSummary()), @@ -98,25 +98,25 @@ router.post('hooks.interactive', async ctx => { }); // triggered by the /outline command in Slack -router.post('hooks.slack', async ctx => { - const { token, team_id, user_id, text = '' } = ctx.body; - ctx.assertPresent(token, 'token is required'); - ctx.assertPresent(team_id, 'team_id is required'); - ctx.assertPresent(user_id, 'user_id is required'); +router.post("hooks.slack", async ctx => { + const { token, team_id, user_id, text = "" } = ctx.body; + ctx.assertPresent(token, "token is required"); + ctx.assertPresent(team_id, "team_id is required"); + ctx.assertPresent(user_id, "user_id is required"); if (token !== process.env.SLACK_VERIFICATION_TOKEN) { - throw new AuthenticationError('Invalid verification token'); + throw new AuthenticationError("Invalid verification token"); } // Handle "help" command or no input - if (text.trim() === 'help' || !text.trim()) { + if (text.trim() === "help" || !text.trim()) { ctx.body = { - response_type: 'ephemeral', - text: 'How to use /outline', + response_type: "ephemeral", + text: "How to use /outline", attachments: [ { text: - 'To search your knowledgebase use `/outline keyword`. \nYou’ve already learned how to get help with `/outline help`.', + "To search your knowledgebase use `/outline keyword`. \nYou’ve already learned how to get help with `/outline help`.", }, ], }; @@ -128,9 +128,9 @@ router.post('hooks.slack', async ctx => { }); if (!team) { ctx.body = { - response_type: 'ephemeral', + response_type: "ephemeral", text: - 'Sorry, we couldn’t find an integration for your team. Head to your Outline settings to set one up.', + "Sorry, we couldn’t find an integration for your team. Head to your Outline settings to set one up.", }; return; } @@ -138,7 +138,7 @@ router.post('hooks.slack', async ctx => { const user = await User.findOne({ where: { teamId: team.id, - service: 'slack', + service: "slack", serviceId: user_id, }, }); @@ -166,9 +166,9 @@ router.post('hooks.slack', async ctx => { process.env.SLACK_MESSAGE_ACTIONS ? [ { - name: 'post', - text: 'Post to Channel', - type: 'button', + name: "post", + text: "Post to Channel", + type: "button", value: result.document.id, }, ] diff --git a/server/api/hooks.test.js b/server/api/hooks.test.js index 87b2108b..c3e902ad 100644 --- a/server/api/hooks.test.js +++ b/server/api/hooks.test.js @@ -1,43 +1,43 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; -import { Authentication } from '../models'; -import { flushdb, seed } from '../test/support'; -import { buildDocument } from '../test/factories'; -import * as Slack from '../slack'; +import TestServer from "fetch-test-server"; +import app from "../app"; +import { Authentication } from "../models"; +import { flushdb, seed } from "../test/support"; +import { buildDocument } from "../test/factories"; +import * as Slack from "../slack"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -jest.mock('../slack', () => ({ +jest.mock("../slack", () => ({ post: jest.fn(), })); -describe('#hooks.unfurl', async () => { - it('should return documents', async () => { +describe("#hooks.unfurl", async () => { + it("should return documents", async () => { const { user, document } = await seed(); await Authentication.create({ - service: 'slack', + service: "slack", userId: user.id, teamId: user.teamId, - token: '', + token: "", }); - const res = await server.post('/api/hooks.unfurl', { + const res = await server.post("/api/hooks.unfurl", { body: { token: process.env.SLACK_VERIFICATION_TOKEN, - team_id: 'TXXXXXXXX', - api_app_id: 'AXXXXXXXXX', + team_id: "TXXXXXXXX", + api_app_id: "AXXXXXXXXX", event: { - type: 'link_shared', - channel: 'Cxxxxxx', + type: "link_shared", + channel: "Cxxxxxx", user: user.serviceId, - message_ts: '123456789.9875', + message_ts: "123456789.9875", links: [ { - domain: 'getoutline.com', + domain: "getoutline.com", url: document.url, }, ], @@ -49,16 +49,16 @@ describe('#hooks.unfurl', async () => { }); }); -describe('#hooks.slack', async () => { - it('should return no matches', async () => { +describe("#hooks.slack", async () => { + it("should return no matches", async () => { const { user, team } = await seed(); - const res = await server.post('/api/hooks.slack', { + const res = await server.post("/api/hooks.slack", { body: { token: process.env.SLACK_VERIFICATION_TOKEN, user_id: user.serviceId, team_id: team.slackId, - text: 'dsfkndfskndsfkn', + text: "dsfkndfskndsfkn", }, }); const body = await res.json(); @@ -66,19 +66,19 @@ describe('#hooks.slack', async () => { expect(body.attachments).toEqual(undefined); }); - it('should return search results with summary if query is in title', async () => { + it("should return search results with summary if query is in title", async () => { const { user, team } = await seed(); const document = await buildDocument({ - title: 'This title contains a search term', + title: "This title contains a search term", userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/hooks.slack', { + const res = await server.post("/api/hooks.slack", { body: { token: process.env.SLACK_VERIFICATION_TOKEN, user_id: user.serviceId, team_id: team.slackId, - text: 'contains', + text: "contains", }, }); const body = await res.json(); @@ -88,19 +88,19 @@ describe('#hooks.slack', async () => { expect(body.attachments[0].text).toEqual(document.getSummary()); }); - it('should return search results if query is regex-like', async () => { + it("should return search results if query is regex-like", async () => { const { user, team } = await seed(); await buildDocument({ - title: 'This title contains a search term', + title: "This title contains a search term", userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/hooks.slack', { + const res = await server.post("/api/hooks.slack", { body: { token: process.env.SLACK_VERIFICATION_TOKEN, user_id: user.serviceId, team_id: team.slackId, - text: '*contains', + text: "*contains", }, }); const body = await res.json(); @@ -108,19 +108,19 @@ describe('#hooks.slack', async () => { expect(body.attachments.length).toEqual(1); }); - it('should return search results with snippet if query is in text', async () => { + it("should return search results with snippet if query is in text", async () => { const { user, team } = await seed(); const document = await buildDocument({ - text: 'This title contains a search term', + text: "This title contains a search term", userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/hooks.slack', { + const res = await server.post("/api/hooks.slack", { body: { token: process.env.SLACK_VERIFICATION_TOKEN, user_id: user.serviceId, team_id: team.slackId, - text: 'contains', + text: "contains", }, }); const body = await res.json(); @@ -128,62 +128,62 @@ describe('#hooks.slack', async () => { expect(body.attachments.length).toEqual(1); expect(body.attachments[0].title).toEqual(document.title); expect(body.attachments[0].text).toEqual( - 'This title *contains* a search term' + "This title *contains* a search term" ); }); - it('should respond with help content for help keyword', async () => { + it("should respond with help content for help keyword", async () => { const { user, team } = await seed(); - const res = await server.post('/api/hooks.slack', { + const res = await server.post("/api/hooks.slack", { body: { token: process.env.SLACK_VERIFICATION_TOKEN, user_id: user.serviceId, team_id: team.slackId, - text: 'help', + text: "help", }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.text.includes('How to use')).toEqual(true); + expect(body.text.includes("How to use")).toEqual(true); }); - it('should respond with help content for no keyword', async () => { + it("should respond with help content for no keyword", async () => { const { user, team } = await seed(); - const res = await server.post('/api/hooks.slack', { + const res = await server.post("/api/hooks.slack", { body: { token: process.env.SLACK_VERIFICATION_TOKEN, user_id: user.serviceId, team_id: team.slackId, - text: '', + text: "", }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.text.includes('How to use')).toEqual(true); + expect(body.text.includes("How to use")).toEqual(true); }); - it('should return search results with snippet for unknown user', async () => { + it("should return search results with snippet for unknown user", async () => { const { user, team } = await seed(); // unpublished document will not be returned await buildDocument({ - text: 'This title contains a search term', + text: "This title contains a search term", userId: user.id, teamId: user.teamId, publishedAt: null, }); const document = await buildDocument({ - text: 'This title contains a search term', + text: "This title contains a search term", userId: user.id, teamId: user.teamId, }); - const res = await server.post('/api/hooks.slack', { + const res = await server.post("/api/hooks.slack", { body: { token: process.env.SLACK_VERIFICATION_TOKEN, - user_id: 'unknown-slack-user-id', + user_id: "unknown-slack-user-id", team_id: team.slackId, - text: 'contains', + text: "contains", }, }); const body = await res.json(); @@ -191,30 +191,30 @@ describe('#hooks.slack', async () => { expect(body.attachments.length).toEqual(1); expect(body.attachments[0].title).toEqual(document.title); expect(body.attachments[0].text).toEqual( - 'This title *contains* a search term' + "This title *contains* a search term" ); }); - it('should error if incorrect verification token', async () => { + it("should error if incorrect verification token", async () => { const { user, team } = await seed(); - const res = await server.post('/api/hooks.slack', { + const res = await server.post("/api/hooks.slack", { body: { - token: 'wrong-verification-token', + token: "wrong-verification-token", team_id: team.slackId, user_id: user.serviceId, - text: 'Welcome', + text: "Welcome", }, }); expect(res.status).toEqual(401); }); }); -describe('#hooks.interactive', async () => { - it('should respond with replacement message', async () => { +describe("#hooks.interactive", async () => { + it("should respond with replacement message", async () => { const { user, team } = await seed(); const document = await buildDocument({ - title: 'This title contains a search term', + title: "This title contains a search term", userId: user.id, teamId: user.teamId, }); @@ -225,48 +225,48 @@ describe('#hooks.interactive', async () => { team: { id: team.slackId }, callback_id: document.id, }); - const res = await server.post('/api/hooks.interactive', { + const res = await server.post("/api/hooks.interactive", { body: { payload }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.response_type).toEqual('in_channel'); + expect(body.response_type).toEqual("in_channel"); expect(body.attachments.length).toEqual(1); expect(body.attachments[0].title).toEqual(document.title); }); - it('should respond with replacement message if unknown user', async () => { + it("should respond with replacement message if unknown user", async () => { const { user, team } = await seed(); const document = await buildDocument({ - title: 'This title contains a search term', + title: "This title contains a search term", userId: user.id, teamId: user.teamId, }); const payload = JSON.stringify({ token: process.env.SLACK_VERIFICATION_TOKEN, - user: { id: 'unknown-slack-user-id' }, + user: { id: "unknown-slack-user-id" }, team: { id: team.slackId }, callback_id: document.id, }); - const res = await server.post('/api/hooks.interactive', { + const res = await server.post("/api/hooks.interactive", { body: { payload }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.response_type).toEqual('in_channel'); + expect(body.response_type).toEqual("in_channel"); expect(body.attachments.length).toEqual(1); expect(body.attachments[0].title).toEqual(document.title); }); - it('should error if incorrect verification token', async () => { + it("should error if incorrect verification token", async () => { const { user } = await seed(); const payload = JSON.stringify({ - token: 'wrong-verification-token', + token: "wrong-verification-token", user: { id: user.serviceId, name: user.name }, - callback_id: 'doesnt-matter', + callback_id: "doesnt-matter", }); - const res = await server.post('/api/hooks.interactive', { + const res = await server.post("/api/hooks.interactive", { body: { payload }, }); expect(res.status).toEqual(401); diff --git a/server/api/index.js b/server/api/index.js index 743aeba2..36e7f171 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -1,32 +1,32 @@ // @flow -import bodyParser from 'koa-bodyparser'; -import Koa from 'koa'; -import Router from 'koa-router'; +import bodyParser from "koa-bodyparser"; +import Koa from "koa"; +import Router from "koa-router"; -import auth from './auth'; -import events from './events'; -import users from './users'; -import collections from './collections'; -import documents from './documents'; -import revisions from './revisions'; -import views from './views'; -import hooks from './hooks'; -import apiKeys from './apiKeys'; -import shares from './shares'; -import groups from './groups'; -import team from './team'; -import integrations from './integrations'; -import notificationSettings from './notificationSettings'; -import utils from './utils'; -import attachments from './attachments'; +import auth from "./auth"; +import events from "./events"; +import users from "./users"; +import collections from "./collections"; +import documents from "./documents"; +import revisions from "./revisions"; +import views from "./views"; +import hooks from "./hooks"; +import apiKeys from "./apiKeys"; +import shares from "./shares"; +import groups from "./groups"; +import team from "./team"; +import integrations from "./integrations"; +import notificationSettings from "./notificationSettings"; +import utils from "./utils"; +import attachments from "./attachments"; -import { NotFoundError } from '../errors'; -import errorHandling from '../middlewares/errorHandling'; -import validation from '../middlewares/validation'; -import methodOverride from '../middlewares/methodOverride'; -import cache from './middlewares/cache'; -import apiWrapper from './middlewares/apiWrapper'; -import editor from './middlewares/editor'; +import { NotFoundError } from "../errors"; +import errorHandling from "../middlewares/errorHandling"; +import validation from "../middlewares/validation"; +import methodOverride from "../middlewares/methodOverride"; +import cache from "./middlewares/cache"; +import apiWrapper from "./middlewares/apiWrapper"; +import editor from "./middlewares/editor"; const api = new Koa(); const router = new Router(); @@ -41,25 +41,25 @@ api.use(apiWrapper()); api.use(editor()); // routes -router.use('/', auth.routes()); -router.use('/', events.routes()); -router.use('/', users.routes()); -router.use('/', collections.routes()); -router.use('/', documents.routes()); -router.use('/', revisions.routes()); -router.use('/', views.routes()); -router.use('/', hooks.routes()); -router.use('/', apiKeys.routes()); -router.use('/', shares.routes()); -router.use('/', team.routes()); -router.use('/', integrations.routes()); -router.use('/', notificationSettings.routes()); -router.use('/', attachments.routes()); -router.use('/', utils.routes()); -router.use('/', groups.routes()); +router.use("/", auth.routes()); +router.use("/", events.routes()); +router.use("/", users.routes()); +router.use("/", collections.routes()); +router.use("/", documents.routes()); +router.use("/", revisions.routes()); +router.use("/", views.routes()); +router.use("/", hooks.routes()); +router.use("/", apiKeys.routes()); +router.use("/", shares.routes()); +router.use("/", team.routes()); +router.use("/", integrations.routes()); +router.use("/", notificationSettings.routes()); +router.use("/", attachments.routes()); +router.use("/", utils.routes()); +router.use("/", groups.routes()); -router.post('*', ctx => { - ctx.throw(new NotFoundError('Endpoint not found')); +router.post("*", ctx => { + ctx.throw(new NotFoundError("Endpoint not found")); }); // Router is embedded in a Koa application wrapper, because koa-router does not diff --git a/server/api/index.test.js b/server/api/index.test.js index b407716c..98200886 100644 --- a/server/api/index.test.js +++ b/server/api/index.test.js @@ -1,22 +1,22 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; -import { flushdb } from '../test/support'; +import TestServer from "fetch-test-server"; +import app from "../app"; +import { flushdb } from "../test/support"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('POST unknown endpoint', async () => { - it('should be not found', async () => { - const res = await server.post('/api/blah'); +describe("POST unknown endpoint", async () => { + it("should be not found", async () => { + const res = await server.post("/api/blah"); expect(res.status).toEqual(404); }); }); -describe('GET unknown endpoint', async () => { - it('should be not found', async () => { - const res = await server.get('/api/blah'); +describe("GET unknown endpoint", async () => { + it("should be not found", async () => { + const res = await server.get("/api/blah"); expect(res.status).toEqual(404); }); }); diff --git a/server/api/integrations.js b/server/api/integrations.js index 5a7fbcae..d2e2a8d8 100644 --- a/server/api/integrations.js +++ b/server/api/integrations.js @@ -1,18 +1,18 @@ // @flow -import Router from 'koa-router'; -import Integration from '../models/Integration'; -import pagination from './middlewares/pagination'; -import auth from '../middlewares/authentication'; -import { Event } from '../models'; -import { presentIntegration } from '../presenters'; -import policy from '../policies'; +import Router from "koa-router"; +import Integration from "../models/Integration"; +import pagination from "./middlewares/pagination"; +import auth from "../middlewares/authentication"; +import { Event } from "../models"; +import { presentIntegration } from "../presenters"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -router.post('integrations.list', auth(), pagination(), async ctx => { - let { sort = 'updatedAt', direction } = ctx.body; - if (direction !== 'ASC') direction = 'DESC'; +router.post("integrations.list", auth(), pagination(), async ctx => { + let { sort = "updatedAt", direction } = ctx.body; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; const integrations = await Integration.findAll({ @@ -28,18 +28,18 @@ router.post('integrations.list', auth(), pagination(), async ctx => { }; }); -router.post('integrations.delete', auth(), async ctx => { +router.post("integrations.delete", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const integration = await Integration.findByPk(id); - authorize(user, 'delete', integration); + authorize(user, "delete", integration); await integration.destroy(); await Event.create({ - name: 'integrations.delete', + name: "integrations.delete", modelId: integration.id, teamId: integration.teamId, actorId: user.id, diff --git a/server/api/middlewares/apiWrapper.js b/server/api/middlewares/apiWrapper.js index 0f5a7ea5..b8e50a1e 100644 --- a/server/api/middlewares/apiWrapper.js +++ b/server/api/middlewares/apiWrapper.js @@ -1,6 +1,6 @@ // @flow -import stream from 'stream'; -import { type Context } from 'koa'; +import stream from "stream"; +import { type Context } from "koa"; export default function apiWrapper() { return async function apiWrapperMiddleware( @@ -12,7 +12,7 @@ export default function apiWrapper() { const ok = ctx.status < 400; if ( - typeof ctx.body !== 'string' && + typeof ctx.body !== "string" && !(ctx.body instanceof stream.Readable) ) { // $FlowFixMe diff --git a/server/api/middlewares/cache.js b/server/api/middlewares/cache.js index b47008e6..a2a4a931 100644 --- a/server/api/middlewares/cache.js +++ b/server/api/middlewares/cache.js @@ -1,8 +1,8 @@ // @flow -import debug from 'debug'; -import { type Context } from 'koa'; +import debug from "debug"; +import { type Context } from "koa"; -const log = debug('cache'); +const log = debug("cache"); export default function cache() { return async function cacheMiddleware(ctx: Context, next: () => Promise<*>) { diff --git a/server/api/middlewares/editor.js b/server/api/middlewares/editor.js index e0868e93..a6f114e5 100644 --- a/server/api/middlewares/editor.js +++ b/server/api/middlewares/editor.js @@ -1,12 +1,12 @@ // @flow -import semver from 'semver'; -import { type Context } from 'koa'; -import pkg from 'rich-markdown-editor/package.json'; -import { EditorUpdateError } from '../../errors'; +import semver from "semver"; +import { type Context } from "koa"; +import pkg from "rich-markdown-editor/package.json"; +import { EditorUpdateError } from "../../errors"; export default function editor() { return async function editorMiddleware(ctx: Context, next: () => Promise<*>) { - const clientVersion = ctx.headers['x-editor-version']; + const clientVersion = ctx.headers["x-editor-version"]; // If the editor version on the client is behind the current version being // served in production by either a minor (new features), or major (breaking diff --git a/server/api/middlewares/pagination.js b/server/api/middlewares/pagination.js index 2228d783..8adaf6ab 100644 --- a/server/api/middlewares/pagination.js +++ b/server/api/middlewares/pagination.js @@ -1,7 +1,7 @@ // @flow -import querystring from 'querystring'; -import { InvalidRequestError } from '../../errors'; -import { type Context } from 'koa'; +import querystring from "querystring"; +import { InvalidRequestError } from "../../errors"; +import { type Context } from "koa"; export default function pagination(options?: Object) { return async function paginationMiddleware( diff --git a/server/api/middlewares/pagination.test.js b/server/api/middlewares/pagination.test.js index fb61fca7..40811313 100644 --- a/server/api/middlewares/pagination.test.js +++ b/server/api/middlewares/pagination.test.js @@ -1,54 +1,54 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../../app'; -import { flushdb, seed } from '../../test/support'; +import TestServer from "fetch-test-server"; +import app from "../../app"; +import { flushdb, seed } from "../../test/support"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#pagination', async () => { - it('should allow offset and limit', async () => { +describe("#pagination", async () => { + it("should allow offset and limit", async () => { const { user } = await seed(); - const res = await server.post('/api/users.list', { + const res = await server.post("/api/users.list", { body: { token: user.getJwtToken(), limit: 1, offset: 1 }, }); expect(res.status).toEqual(200); }); - it('should not allow negative limit', async () => { + it("should not allow negative limit", async () => { const { user } = await seed(); - const res = await server.post('/api/users.list', { + const res = await server.post("/api/users.list", { body: { token: user.getJwtToken(), limit: -1 }, }); expect(res.status).toEqual(400); }); - it('should not allow non-integer limit', async () => { + it("should not allow non-integer limit", async () => { const { user } = await seed(); - const res = await server.post('/api/users.list', { - body: { token: user.getJwtToken(), limit: 'blah' }, + const res = await server.post("/api/users.list", { + body: { token: user.getJwtToken(), limit: "blah" }, }); expect(res.status).toEqual(400); }); - it('should not allow negative offset', async () => { + it("should not allow negative offset", async () => { const { user } = await seed(); - const res = await server.post('/api/users.list', { + const res = await server.post("/api/users.list", { body: { token: user.getJwtToken(), offset: -1 }, }); expect(res.status).toEqual(400); }); - it('should not allow non-integer offset', async () => { + it("should not allow non-integer offset", async () => { const { user } = await seed(); - const res = await server.post('/api/users.list', { - body: { token: user.getJwtToken(), offset: 'blah' }, + const res = await server.post("/api/users.list", { + body: { token: user.getJwtToken(), offset: "blah" }, }); expect(res.status).toEqual(400); diff --git a/server/api/notificationSettings.js b/server/api/notificationSettings.js index cad6d761..a911d9db 100644 --- a/server/api/notificationSettings.js +++ b/server/api/notificationSettings.js @@ -1,20 +1,20 @@ // @flow -import Router from 'koa-router'; +import Router from "koa-router"; -import auth from '../middlewares/authentication'; -import { NotificationSetting } from '../models'; -import { presentNotificationSetting } from '../presenters'; -import policy from '../policies'; +import auth from "../middlewares/authentication"; +import { NotificationSetting } from "../models"; +import { presentNotificationSetting } from "../presenters"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -router.post('notificationSettings.create', auth(), async ctx => { +router.post("notificationSettings.create", auth(), async ctx => { const { event } = ctx.body; - ctx.assertPresent(event, 'event is required'); + ctx.assertPresent(event, "event is required"); const user = ctx.state.user; - authorize(user, 'create', NotificationSetting); + authorize(user, "create", NotificationSetting); const [setting] = await NotificationSetting.findOrCreate({ where: { @@ -29,7 +29,7 @@ router.post('notificationSettings.create', auth(), async ctx => { }; }); -router.post('notificationSettings.list', auth(), async ctx => { +router.post("notificationSettings.list", auth(), async ctx => { const user = ctx.state.user; const settings = await NotificationSetting.findAll({ where: { @@ -42,13 +42,13 @@ router.post('notificationSettings.list', auth(), async ctx => { }; }); -router.post('notificationSettings.delete', auth(), async ctx => { +router.post("notificationSettings.delete", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const setting = await NotificationSetting.findByPk(id); - authorize(user, 'delete', setting); + authorize(user, "delete", setting); await setting.destroy(); @@ -57,10 +57,10 @@ router.post('notificationSettings.delete', auth(), async ctx => { }; }); -router.post('notificationSettings.unsubscribe', async ctx => { +router.post("notificationSettings.unsubscribe", async ctx => { const { id, token } = ctx.body; - ctx.assertUuid(id, 'id is required'); - ctx.assertPresent(token, 'token is required'); + ctx.assertUuid(id, "id is required"); + ctx.assertPresent(token, "token is required"); const setting = await NotificationSetting.findByPk(id); if (setting) { diff --git a/server/api/revisions.js b/server/api/revisions.js index af305c55..2b758239 100644 --- a/server/api/revisions.js +++ b/server/api/revisions.js @@ -1,18 +1,18 @@ // @flow -import Router from 'koa-router'; -import auth from '../middlewares/authentication'; -import pagination from './middlewares/pagination'; -import { presentRevision } from '../presenters'; -import { Document, Revision } from '../models'; -import { NotFoundError } from '../errors'; -import policy from '../policies'; +import Router from "koa-router"; +import auth from "../middlewares/authentication"; +import pagination from "./middlewares/pagination"; +import { presentRevision } from "../presenters"; +import { Document, Revision } from "../models"; +import { NotFoundError } from "../errors"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -router.post('revisions.info', auth(), async ctx => { +router.post("revisions.info", auth(), async ctx => { let { id } = ctx.body; - ctx.assertPresent(id, 'id is required'); + ctx.assertPresent(id, "id is required"); const user = ctx.state.user; const revision = await Revision.findByPk(id); @@ -23,7 +23,7 @@ router.post('revisions.info', auth(), async ctx => { const document = await Document.findByPk(revision.documentId, { userId: user.id, }); - authorize(user, 'read', document); + authorize(user, "read", document); ctx.body = { pagination: ctx.state.pagination, @@ -31,14 +31,14 @@ router.post('revisions.info', auth(), async ctx => { }; }); -router.post('revisions.list', auth(), pagination(), async ctx => { - let { documentId, sort = 'updatedAt', direction } = ctx.body; - if (direction !== 'ASC') direction = 'DESC'; - ctx.assertPresent(documentId, 'documentId is required'); +router.post("revisions.list", auth(), pagination(), async ctx => { + let { documentId, sort = "updatedAt", direction } = ctx.body; + if (direction !== "ASC") direction = "DESC"; + ctx.assertPresent(documentId, "documentId is required"); const user = ctx.state.user; const document = await Document.findByPk(documentId, { userId: user.id }); - authorize(user, 'read', document); + authorize(user, "read", document); const revisions = await Revision.findAll({ where: { documentId: document.id }, diff --git a/server/api/revisions.test.js b/server/api/revisions.test.js index 0a60bd1b..ceb75585 100644 --- a/server/api/revisions.test.js +++ b/server/api/revisions.test.js @@ -1,24 +1,24 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; -import { flushdb, seed } from '../test/support'; -import { buildDocument, buildUser } from '../test/factories'; -import Revision from '../models/Revision'; +import TestServer from "fetch-test-server"; +import app from "../app"; +import { flushdb, seed } from "../test/support"; +import { buildDocument, buildUser } from "../test/factories"; +import Revision from "../models/Revision"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#revisions.info', async () => { - it('should return a document revision', async () => { +describe("#revisions.info", async () => { + it("should return a document revision", async () => { const { user, document } = await seed(); const revision = await Revision.findOne({ where: { documentId: document.id, }, }); - const res = await server.post('/api/revisions.info', { + const res = await server.post("/api/revisions.info", { body: { token: user.getJwtToken(), id: revision.id, @@ -31,7 +31,7 @@ describe('#revisions.info', async () => { expect(body.data.title).toEqual(document.title); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const document = await buildDocument(); const revision = await Revision.findOne({ where: { @@ -39,7 +39,7 @@ describe('#revisions.info', async () => { }, }); const user = await buildUser(); - const res = await server.post('/api/revisions.info', { + const res = await server.post("/api/revisions.info", { body: { token: user.getJwtToken(), id: revision.id, @@ -49,10 +49,10 @@ describe('#revisions.info', async () => { }); }); -describe('#revisions.list', async () => { +describe("#revisions.list", async () => { it("should return a document's revisions", async () => { const { user, document } = await seed(); - const res = await server.post('/api/revisions.list', { + const res = await server.post("/api/revisions.list", { body: { token: user.getJwtToken(), documentId: document.id, @@ -66,22 +66,22 @@ describe('#revisions.list', async () => { expect(body.data[0].title).toEqual(document.title); }); - it('should not return revisions for document in collection not a member of', async () => { + it("should not return revisions for document in collection not a member of", async () => { const { user, document, collection } = await seed(); collection.private = true; await collection.save(); - const res = await server.post('/api/revisions.list', { + const res = await server.post("/api/revisions.list", { body: { token: user.getJwtToken(), documentId: document.id }, }); expect(res.status).toEqual(403); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const document = await buildDocument(); const user = await buildUser(); - const res = await server.post('/api/revisions.list', { + const res = await server.post("/api/revisions.list", { body: { token: user.getJwtToken(), documentId: document.id, diff --git a/server/api/shares.js b/server/api/shares.js index 53dd6a31..2ed07810 100644 --- a/server/api/shares.js +++ b/server/api/shares.js @@ -1,32 +1,32 @@ // @flow -import Router from 'koa-router'; -import Sequelize from 'sequelize'; -import auth from '../middlewares/authentication'; -import pagination from './middlewares/pagination'; -import { presentShare } from '../presenters'; -import { Document, User, Event, Share, Team } from '../models'; -import policy from '../policies'; +import Router from "koa-router"; +import Sequelize from "sequelize"; +import auth from "../middlewares/authentication"; +import pagination from "./middlewares/pagination"; +import { presentShare } from "../presenters"; +import { Document, User, Event, Share, Team } from "../models"; +import policy from "../policies"; const Op = Sequelize.Op; const { authorize } = policy; const router = new Router(); -router.post('shares.info', auth(), async ctx => { +router.post("shares.info", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const share = await Share.findByPk(id); - authorize(user, 'read', share); + authorize(user, "read", share); ctx.body = { data: presentShare(share), }; }); -router.post('shares.list', auth(), pagination(), async ctx => { - let { sort = 'updatedAt', direction } = ctx.body; - if (direction !== 'ASC') direction = 'DESC'; +router.post("shares.list", auth(), pagination(), async ctx => { + let { sort = "updatedAt", direction } = ctx.body; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; const where = { @@ -47,7 +47,7 @@ router.post('shares.list', auth(), pagination(), async ctx => { { model: Document, required: true, - as: 'document', + as: "document", where: { collectionId: collectionIds, }, @@ -55,7 +55,7 @@ router.post('shares.list', auth(), pagination(), async ctx => { { model: User, required: true, - as: 'user', + as: "user", }, ], offset: ctx.state.pagination.offset, @@ -68,15 +68,15 @@ router.post('shares.list', auth(), pagination(), async ctx => { }; }); -router.post('shares.create', auth(), async ctx => { +router.post("shares.create", auth(), async ctx => { const { documentId } = ctx.body; - ctx.assertPresent(documentId, 'documentId is required'); + ctx.assertPresent(documentId, "documentId is required"); const user = ctx.state.user; const document = await Document.findByPk(documentId, { userId: user.id }); const team = await Team.findByPk(user.teamId); - authorize(user, 'share', document); - authorize(user, 'share', team); + authorize(user, "share", document); + authorize(user, "share", team); const [share] = await Share.findOrCreate({ where: { @@ -88,7 +88,7 @@ router.post('shares.create', auth(), async ctx => { }); await Event.create({ - name: 'shares.create', + name: "shares.create", documentId, collectionId: document.collectionId, modelId: share.id, @@ -106,20 +106,20 @@ router.post('shares.create', auth(), async ctx => { }; }); -router.post('shares.revoke', auth(), async ctx => { +router.post("shares.revoke", auth(), async ctx => { const { id } = ctx.body; - ctx.assertUuid(id, 'id is required'); + ctx.assertUuid(id, "id is required"); const user = ctx.state.user; const share = await Share.findByPk(id); - authorize(user, 'revoke', share); + authorize(user, "revoke", share); await share.revoke(user.id); const document = await Document.findByPk(share.documentId); await Event.create({ - name: 'shares.revoke', + name: "shares.revoke", documentId: document.id, collectionId: document.collectionId, modelId: share.id, diff --git a/server/api/shares.test.js b/server/api/shares.test.js index 0cbba773..4dd501ba 100644 --- a/server/api/shares.test.js +++ b/server/api/shares.test.js @@ -1,17 +1,17 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; -import { CollectionUser } from '../models'; -import { flushdb, seed } from '../test/support'; -import { buildUser, buildShare } from '../test/factories'; +import TestServer from "fetch-test-server"; +import app from "../app"; +import { CollectionUser } from "../models"; +import { flushdb, seed } from "../test/support"; +import { buildUser, buildShare } from "../test/factories"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#shares.list', async () => { - it('should only return shares created by user', async () => { +describe("#shares.list", async () => { + it("should only return shares created by user", async () => { const { user, admin, document } = await seed(); await buildShare({ documentId: document.id, @@ -23,7 +23,7 @@ describe('#shares.list', async () => { teamId: user.teamId, userId: user.id, }); - const res = await server.post('/api/shares.list', { + const res = await server.post("/api/shares.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -34,7 +34,7 @@ describe('#shares.list', async () => { expect(body.data[0].documentTitle).toBe(document.title); }); - it('should not return revoked shares', async () => { + it("should not return revoked shares", async () => { const { user, document } = await seed(); const share = await buildShare({ documentId: document.id, @@ -43,7 +43,7 @@ describe('#shares.list', async () => { }); await share.revoke(user.id); - const res = await server.post('/api/shares.list', { + const res = await server.post("/api/shares.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -52,14 +52,14 @@ describe('#shares.list', async () => { expect(body.data.length).toEqual(0); }); - it('admins should return shares created by all users', async () => { + it("admins should return shares created by all users", async () => { const { user, admin, document } = await seed(); const share = await buildShare({ documentId: document.id, teamId: admin.teamId, userId: user.id, }); - const res = await server.post('/api/shares.list', { + const res = await server.post("/api/shares.list", { body: { token: admin.getJwtToken() }, }); const body = await res.json(); @@ -70,7 +70,7 @@ describe('#shares.list', async () => { expect(body.data[0].documentTitle).toBe(document.title); }); - it('admins should not return shares in collection not a member of', async () => { + it("admins should not return shares in collection not a member of", async () => { const { admin, document, collection } = await seed(); await buildShare({ documentId: document.id, @@ -81,7 +81,7 @@ describe('#shares.list', async () => { collection.private = true; await collection.save(); - const res = await server.post('/api/shares.list', { + const res = await server.post("/api/shares.list", { body: { token: admin.getJwtToken() }, }); const body = await res.json(); @@ -90,8 +90,8 @@ describe('#shares.list', async () => { expect(body.data.length).toEqual(0); }); - it('should require authentication', async () => { - const res = await server.post('/api/shares.list'); + it("should require authentication", async () => { + const res = await server.post("/api/shares.list"); const body = await res.json(); expect(res.status).toEqual(401); @@ -99,10 +99,10 @@ describe('#shares.list', async () => { }); }); -describe('#shares.create', async () => { - it('should allow creating a share record for document', async () => { +describe("#shares.create", async () => { + it("should allow creating a share record for document", async () => { const { user, document } = await seed(); - const res = await server.post('/api/shares.create', { + const res = await server.post("/api/shares.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); const body = await res.json(); @@ -111,7 +111,7 @@ describe('#shares.create', async () => { expect(body.data.documentTitle).toBe(document.title); }); - it('should allow creating a share record for document in read-only collection', async () => { + it("should allow creating a share record for document in read-only collection", async () => { const { user, document, collection } = await seed(); collection.private = true; await collection.save(); @@ -120,10 +120,10 @@ describe('#shares.create', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read', + permission: "read", }); - const res = await server.post('/api/shares.create', { + const res = await server.post("/api/shares.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); const body = await res.json(); @@ -132,7 +132,7 @@ describe('#shares.create', async () => { expect(body.data.documentTitle).toBe(document.title); }); - it('should allow creating a share record if link previously revoked', async () => { + it("should allow creating a share record if link previously revoked", async () => { const { user, document } = await seed(); const share = await buildShare({ documentId: document.id, @@ -140,7 +140,7 @@ describe('#shares.create', async () => { userId: user.id, }); await share.revoke(); - const res = await server.post('/api/shares.create', { + const res = await server.post("/api/shares.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); const body = await res.json(); @@ -150,14 +150,14 @@ describe('#shares.create', async () => { expect(body.data.documentTitle).toBe(document.title); }); - it('should return existing share link for document and user', async () => { + it("should return existing share link for document and user", async () => { const { user, document } = await seed(); const share = await buildShare({ documentId: document.id, teamId: user.teamId, userId: user.id, }); - const res = await server.post('/api/shares.create', { + const res = await server.post("/api/shares.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); const body = await res.json(); @@ -166,18 +166,18 @@ describe('#shares.create', async () => { expect(body.data.id).toBe(share.id); }); - it('should not allow creating a share record if disabled', async () => { + it("should not allow creating a share record if disabled", async () => { const { user, document, team } = await seed(); await team.update({ sharing: false }); - const res = await server.post('/api/shares.create', { + const res = await server.post("/api/shares.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); expect(res.status).toEqual(403); }); - it('should require authentication', async () => { + it("should require authentication", async () => { const { document } = await seed(); - const res = await server.post('/api/shares.create', { + const res = await server.post("/api/shares.create", { body: { documentId: document.id }, }); const body = await res.json(); @@ -186,18 +186,18 @@ describe('#shares.create', async () => { expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/shares.create', { + const res = await server.post("/api/shares.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); expect(res.status).toEqual(403); }); }); -describe('#shares.revoke', async () => { - it('should allow author to revoke a share', async () => { +describe("#shares.revoke", async () => { + it("should allow author to revoke a share", async () => { const { user, document } = await seed(); const share = await buildShare({ documentId: document.id, @@ -205,13 +205,13 @@ describe('#shares.revoke', async () => { userId: user.id, }); - const res = await server.post('/api/shares.revoke', { + const res = await server.post("/api/shares.revoke", { body: { token: user.getJwtToken(), id: share.id }, }); expect(res.status).toEqual(200); }); - it('should allow admin to revoke a share', async () => { + it("should allow admin to revoke a share", async () => { const { user, admin, document } = await seed(); const share = await buildShare({ documentId: document.id, @@ -219,20 +219,20 @@ describe('#shares.revoke', async () => { userId: user.id, }); - const res = await server.post('/api/shares.revoke', { + const res = await server.post("/api/shares.revoke", { body: { token: admin.getJwtToken(), id: share.id }, }); expect(res.status).toEqual(200); }); - it('should require authentication', async () => { + it("should require authentication", async () => { const { user, document } = await seed(); const share = await buildShare({ documentId: document.id, teamId: user.teamId, userId: user.id, }); - const res = await server.post('/api/shares.revoke', { + const res = await server.post("/api/shares.revoke", { body: { id: share.id }, }); const body = await res.json(); @@ -241,10 +241,10 @@ describe('#shares.revoke', async () => { expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/shares.create', { + const res = await server.post("/api/shares.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); expect(res.status).toEqual(403); diff --git a/server/api/team.js b/server/api/team.js index 471fec29..2c856818 100644 --- a/server/api/team.js +++ b/server/api/team.js @@ -1,15 +1,15 @@ // @flow -import Router from 'koa-router'; -import { Team } from '../models'; +import Router from "koa-router"; +import { Team } from "../models"; -import auth from '../middlewares/authentication'; -import { presentTeam, presentPolicies } from '../presenters'; -import policy from '../policies'; +import auth from "../middlewares/authentication"; +import { presentTeam, presentPolicies } from "../presenters"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -router.post('team.update', auth(), async ctx => { +router.post("team.update", auth(), async ctx => { const { name, avatarUrl, @@ -20,10 +20,10 @@ router.post('team.update', auth(), async ctx => { } = ctx.body; const user = ctx.state.user; const team = await Team.findByPk(user.teamId); - authorize(user, 'update', team); + authorize(user, "update", team); - if (subdomain !== undefined && process.env.SUBDOMAINS_ENABLED === 'true') { - team.subdomain = subdomain === '' ? null : subdomain; + if (subdomain !== undefined && process.env.SUBDOMAINS_ENABLED === "true") { + team.subdomain = subdomain === "" ? null : subdomain; } if (name) team.name = name; diff --git a/server/api/team.test.js b/server/api/team.test.js index d817cfa9..d34e7908 100644 --- a/server/api/team.test.js +++ b/server/api/team.test.js @@ -1,37 +1,37 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; +import TestServer from "fetch-test-server"; +import app from "../app"; -import { flushdb, seed } from '../test/support'; +import { flushdb, seed } from "../test/support"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#team.update', async () => { - it('should update team details', async () => { +describe("#team.update", async () => { + it("should update team details", async () => { const { admin } = await seed(); - const res = await server.post('/api/team.update', { - body: { token: admin.getJwtToken(), name: 'New name' }, + const res = await server.post("/api/team.update", { + body: { token: admin.getJwtToken(), name: "New name" }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.name).toEqual('New name'); + expect(body.data.name).toEqual("New name"); }); - it('should require admin', async () => { + it("should require admin", async () => { const { user } = await seed(); - const res = await server.post('/api/team.update', { - body: { token: user.getJwtToken(), name: 'New name' }, + const res = await server.post("/api/team.update", { + body: { token: user.getJwtToken(), name: "New name" }, }); expect(res.status).toEqual(403); }); - it('should require authentication', async () => { + it("should require authentication", async () => { await seed(); - const res = await server.post('/api/team.update'); + const res = await server.post("/api/team.update"); expect(res.status).toEqual(401); }); }); diff --git a/server/api/users.js b/server/api/users.js index bf69320e..0e292f1b 100644 --- a/server/api/users.js +++ b/server/api/users.js @@ -1,20 +1,20 @@ // @flow -import Router from 'koa-router'; -import { Op } from '../sequelize'; -import { Event, User, Team } from '../models'; -import auth from '../middlewares/authentication'; -import pagination from './middlewares/pagination'; -import userInviter from '../commands/userInviter'; -import { presentUser } from '../presenters'; -import policy from '../policies'; +import Router from "koa-router"; +import { Op } from "../sequelize"; +import { Event, User, Team } from "../models"; +import auth from "../middlewares/authentication"; +import pagination from "./middlewares/pagination"; +import userInviter from "../commands/userInviter"; +import { presentUser } from "../presenters"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -router.post('users.list', auth(), pagination(), async ctx => { - const { sort = 'createdAt', query, includeSuspended = false } = ctx.body; +router.post("users.list", auth(), pagination(), async ctx => { + const { sort = "createdAt", query, includeSuspended = false } = ctx.body; let direction = ctx.body.direction; - if (direction !== 'ASC') direction = 'DESC'; + if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; let where = { @@ -54,13 +54,13 @@ router.post('users.list', auth(), pagination(), async ctx => { }; }); -router.post('users.info', auth(), async ctx => { +router.post("users.info", auth(), async ctx => { ctx.body = { data: presentUser(ctx.state.user), }; }); -router.post('users.update', auth(), async ctx => { +router.post("users.update", auth(), async ctx => { const { user } = ctx.state; const { name, avatarUrl } = ctx.body; @@ -76,19 +76,19 @@ router.post('users.update', auth(), async ctx => { // Admin specific -router.post('users.promote', auth(), async ctx => { +router.post("users.promote", auth(), async ctx => { const userId = ctx.body.id; const teamId = ctx.state.user.teamId; - ctx.assertPresent(userId, 'id is required'); + ctx.assertPresent(userId, "id is required"); const user = await User.findByPk(userId); - authorize(ctx.state.user, 'promote', user); + authorize(ctx.state.user, "promote", user); const team = await Team.findByPk(teamId); await team.addAdmin(user); await Event.create({ - name: 'users.promote', + name: "users.promote", actorId: ctx.state.user.id, userId, teamId, @@ -101,19 +101,19 @@ router.post('users.promote', auth(), async ctx => { }; }); -router.post('users.demote', auth(), async ctx => { +router.post("users.demote", auth(), async ctx => { const userId = ctx.body.id; const teamId = ctx.state.user.teamId; - ctx.assertPresent(userId, 'id is required'); + ctx.assertPresent(userId, "id is required"); const user = await User.findByPk(userId); - authorize(ctx.state.user, 'demote', user); + authorize(ctx.state.user, "demote", user); const team = await Team.findByPk(teamId); await team.removeAdmin(user); await Event.create({ - name: 'users.demote', + name: "users.demote", actorId: ctx.state.user.id, userId, teamId, @@ -126,20 +126,20 @@ router.post('users.demote', auth(), async ctx => { }; }); -router.post('users.suspend', auth(), async ctx => { +router.post("users.suspend", auth(), async ctx => { const admin = ctx.state.user; const userId = ctx.body.id; const teamId = ctx.state.user.teamId; - ctx.assertPresent(userId, 'id is required'); + ctx.assertPresent(userId, "id is required"); const user = await User.findByPk(userId); - authorize(ctx.state.user, 'suspend', user); + authorize(ctx.state.user, "suspend", user); const team = await Team.findByPk(teamId); await team.suspendUser(user, admin); await Event.create({ - name: 'users.suspend', + name: "users.suspend", actorId: ctx.state.user.id, userId, teamId, @@ -152,20 +152,20 @@ router.post('users.suspend', auth(), async ctx => { }; }); -router.post('users.activate', auth(), async ctx => { +router.post("users.activate", auth(), async ctx => { const admin = ctx.state.user; const userId = ctx.body.id; const teamId = ctx.state.user.teamId; - ctx.assertPresent(userId, 'id is required'); + ctx.assertPresent(userId, "id is required"); const user = await User.findByPk(userId); - authorize(ctx.state.user, 'activate', user); + authorize(ctx.state.user, "activate", user); const team = await Team.findByPk(teamId); await team.activateUser(user, admin); await Event.create({ - name: 'users.activate', + name: "users.activate", actorId: ctx.state.user.id, userId, teamId, @@ -178,12 +178,12 @@ router.post('users.activate', auth(), async ctx => { }; }); -router.post('users.invite', auth(), async ctx => { +router.post("users.invite", auth(), async ctx => { const { invites } = ctx.body; - ctx.assertPresent(invites, 'invites is required'); + ctx.assertPresent(invites, "invites is required"); const user = ctx.state.user; - authorize(user, 'invite', User); + authorize(user, "invite", User); const response = await userInviter({ user, invites, ip: ctx.request.ip }); @@ -195,17 +195,17 @@ router.post('users.invite', auth(), async ctx => { }; }); -router.post('users.delete', auth(), async ctx => { +router.post("users.delete", auth(), async ctx => { const { confirmation, id } = ctx.body; - ctx.assertPresent(confirmation, 'confirmation is required'); + ctx.assertPresent(confirmation, "confirmation is required"); let user = ctx.state.user; if (id) user = await User.findByPk(id); - authorize(ctx.state.user, 'delete', user); + authorize(ctx.state.user, "delete", user); await user.destroy(); await Event.create({ - name: 'users.delete', + name: "users.delete", actorId: user.id, userId: user.id, teamId: user.teamId, diff --git a/server/api/users.test.js b/server/api/users.test.js index 3457b2f5..db18190b 100644 --- a/server/api/users.test.js +++ b/server/api/users.test.js @@ -1,29 +1,29 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; +import TestServer from "fetch-test-server"; +import app from "../app"; -import { flushdb, seed } from '../test/support'; -import { buildUser } from '../test/factories'; +import { flushdb, seed } from "../test/support"; +import { buildUser } from "../test/factories"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#users.list', async () => { - it('should allow filtering by user name', async () => { - const user = await buildUser({ name: 'Tester' }); +describe("#users.list", async () => { + it("should allow filtering by user name", async () => { + const user = await buildUser({ name: "Tester" }); // suspended user should not be returned await buildUser({ - name: 'Tester', + name: "Tester", teamId: user.teamId, suspendedAt: new Date(), }); - const res = await server.post('/api/users.list', { + const res = await server.post("/api/users.list", { body: { - query: 'test', + query: "test", token: user.getJwtToken(), }, }); @@ -34,17 +34,17 @@ describe('#users.list', async () => { expect(body.data[0].id).toEqual(user.id); }); - it('should allow including suspended', async () => { - const user = await buildUser({ name: 'Tester' }); + it("should allow including suspended", async () => { + const user = await buildUser({ name: "Tester" }); await buildUser({ - name: 'Tester', + name: "Tester", teamId: user.teamId, suspendedAt: new Date(), }); - const res = await server.post('/api/users.list', { + const res = await server.post("/api/users.list", { body: { - query: 'test', + query: "test", includeSuspended: true, token: user.getJwtToken(), }, @@ -55,10 +55,10 @@ describe('#users.list', async () => { expect(body.data.length).toEqual(2); }); - it('should return teams paginated user list', async () => { + it("should return teams paginated user list", async () => { const { admin, user } = await seed(); - const res = await server.post('/api/users.list', { + const res = await server.post("/api/users.list", { body: { token: admin.getJwtToken() }, }); const body = await res.json(); @@ -69,9 +69,9 @@ describe('#users.list', async () => { expect(body.data[1].id).toEqual(admin.id); }); - it('should require admin for detailed info', async () => { + it("should require admin for detailed info", async () => { const { user, admin } = await seed(); - const res = await server.post('/api/users.list', { + const res = await server.post("/api/users.list", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -85,10 +85,10 @@ describe('#users.list', async () => { }); }); -describe('#users.info', async () => { - it('should return known user', async () => { +describe("#users.info", async () => { + it("should return known user", async () => { const user = await buildUser(); - const res = await server.post('/api/users.info', { + const res = await server.post("/api/users.info", { body: { token: user.getJwtToken() }, }); const body = await res.json(); @@ -98,19 +98,19 @@ describe('#users.info', async () => { expect(body.data.name).toEqual(user.name); }); - it('should require authentication', async () => { - const res = await server.post('/api/users.info'); + it("should require authentication", async () => { + const res = await server.post("/api/users.info"); expect(res.status).toEqual(401); }); }); -describe('#users.invite', async () => { - it('should return sent invites', async () => { +describe("#users.invite", async () => { + it("should return sent invites", async () => { const user = await buildUser(); - const res = await server.post('/api/users.invite', { + const res = await server.post("/api/users.invite", { body: { token: user.getJwtToken(), - invites: [{ email: 'test@example.com', name: 'Test', guest: false }], + invites: [{ email: "test@example.com", name: "Test", guest: false }], }, }); const body = await res.json(); @@ -118,70 +118,70 @@ describe('#users.invite', async () => { expect(body.data.sent.length).toEqual(1); }); - it('should require authentication', async () => { - const res = await server.post('/api/users.invite'); + it("should require authentication", async () => { + const res = await server.post("/api/users.invite"); expect(res.status).toEqual(401); }); }); -describe('#users.delete', async () => { - it('should not allow deleting without confirmation', async () => { +describe("#users.delete", async () => { + it("should not allow deleting without confirmation", async () => { const user = await buildUser(); - const res = await server.post('/api/users.delete', { + const res = await server.post("/api/users.delete", { body: { token: user.getJwtToken() }, }); expect(res.status).toEqual(400); }); - it('should allow deleting last admin if only user', async () => { + it("should allow deleting last admin if only user", async () => { const user = await buildUser({ isAdmin: true }); - const res = await server.post('/api/users.delete', { + const res = await server.post("/api/users.delete", { body: { token: user.getJwtToken(), confirmation: true }, }); expect(res.status).toEqual(200); }); - it('should not allow deleting last admin if many users', async () => { + it("should not allow deleting last admin if many users", async () => { const user = await buildUser({ isAdmin: true }); await buildUser({ teamId: user.teamId, isAdmin: false }); - const res = await server.post('/api/users.delete', { + const res = await server.post("/api/users.delete", { body: { token: user.getJwtToken(), confirmation: true }, }); expect(res.status).toEqual(400); }); - it('should allow deleting user account with confirmation', async () => { + it("should allow deleting user account with confirmation", async () => { const user = await buildUser(); - const res = await server.post('/api/users.delete', { + const res = await server.post("/api/users.delete", { body: { token: user.getJwtToken(), confirmation: true }, }); expect(res.status).toEqual(200); }); - it('should allow deleting pending user account with admin', async () => { + it("should allow deleting pending user account with admin", async () => { const user = await buildUser({ isAdmin: true }); const pending = await buildUser({ teamId: user.teamId, lastActiveAt: null, }); - const res = await server.post('/api/users.delete', { + const res = await server.post("/api/users.delete", { body: { token: user.getJwtToken(), id: pending.id, confirmation: true }, }); expect(res.status).toEqual(200); }); - it('should not allow deleting another user account', async () => { + it("should not allow deleting another user account", async () => { const user = await buildUser({ isAdmin: true }); const user2 = await buildUser({ teamId: user.teamId }); - const res = await server.post('/api/users.delete', { + const res = await server.post("/api/users.delete", { body: { token: user.getJwtToken(), id: user2.id, confirmation: true }, }); expect(res.status).toEqual(403); }); - it('should require authentication', async () => { - const res = await server.post('/api/users.delete'); + it("should require authentication", async () => { + const res = await server.post("/api/users.delete"); const body = await res.json(); expect(res.status).toEqual(401); @@ -189,20 +189,20 @@ describe('#users.delete', async () => { }); }); -describe('#users.update', async () => { - it('should update user profile information', async () => { +describe("#users.update", async () => { + it("should update user profile information", async () => { const { user } = await seed(); - const res = await server.post('/api/users.update', { - body: { token: user.getJwtToken(), name: 'New name' }, + const res = await server.post("/api/users.update", { + body: { token: user.getJwtToken(), name: "New name" }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.name).toEqual('New name'); + expect(body.data.name).toEqual("New name"); }); - it('should require authentication', async () => { - const res = await server.post('/api/users.update'); + it("should require authentication", async () => { + const res = await server.post("/api/users.update"); const body = await res.json(); expect(res.status).toEqual(401); @@ -210,11 +210,11 @@ describe('#users.update', async () => { }); }); -describe('#users.promote', async () => { - it('should promote a new admin', async () => { +describe("#users.promote", async () => { + it("should promote a new admin", async () => { const { admin, user } = await seed(); - const res = await server.post('/api/users.promote', { + const res = await server.post("/api/users.promote", { body: { token: admin.getJwtToken(), id: user.id }, }); const body = await res.json(); @@ -223,9 +223,9 @@ describe('#users.promote', async () => { expect(body).toMatchSnapshot(); }); - it('should require admin', async () => { + it("should require admin", async () => { const user = await buildUser(); - const res = await server.post('/api/users.promote', { + const res = await server.post("/api/users.promote", { body: { token: user.getJwtToken(), id: user.id }, }); const body = await res.json(); @@ -235,12 +235,12 @@ describe('#users.promote', async () => { }); }); -describe('#users.demote', async () => { - it('should demote an admin', async () => { +describe("#users.demote", async () => { + it("should demote an admin", async () => { const { admin, user } = await seed(); await user.update({ isAdmin: true }); // Make another admin - const res = await server.post('/api/users.demote', { + const res = await server.post("/api/users.demote", { body: { token: admin.getJwtToken(), id: user.id, @@ -252,10 +252,10 @@ describe('#users.demote', async () => { expect(body).toMatchSnapshot(); }); - it('should not demote admins if only one available', async () => { + it("should not demote admins if only one available", async () => { const admin = await buildUser({ isAdmin: true }); - const res = await server.post('/api/users.demote', { + const res = await server.post("/api/users.demote", { body: { token: admin.getJwtToken(), id: admin.id, @@ -267,9 +267,9 @@ describe('#users.demote', async () => { expect(body).toMatchSnapshot(); }); - it('should require admin', async () => { + it("should require admin", async () => { const user = await buildUser(); - const res = await server.post('/api/users.promote', { + const res = await server.post("/api/users.promote", { body: { token: user.getJwtToken(), id: user.id }, }); const body = await res.json(); @@ -279,11 +279,11 @@ describe('#users.demote', async () => { }); }); -describe('#users.suspend', async () => { - it('should suspend an user', async () => { +describe("#users.suspend", async () => { + it("should suspend an user", async () => { const { admin, user } = await seed(); - const res = await server.post('/api/users.suspend', { + const res = await server.post("/api/users.suspend", { body: { token: admin.getJwtToken(), id: user.id, @@ -295,9 +295,9 @@ describe('#users.suspend', async () => { expect(body).toMatchSnapshot(); }); - it('should not allow suspending the user themselves', async () => { + it("should not allow suspending the user themselves", async () => { const admin = await buildUser({ isAdmin: true }); - const res = await server.post('/api/users.suspend', { + const res = await server.post("/api/users.suspend", { body: { token: admin.getJwtToken(), id: admin.id, @@ -309,9 +309,9 @@ describe('#users.suspend', async () => { expect(body).toMatchSnapshot(); }); - it('should require admin', async () => { + it("should require admin", async () => { const user = await buildUser(); - const res = await server.post('/api/users.suspend', { + const res = await server.post("/api/users.suspend", { body: { token: user.getJwtToken(), id: user.id }, }); const body = await res.json(); @@ -321,8 +321,8 @@ describe('#users.suspend', async () => { }); }); -describe('#users.activate', async () => { - it('should activate a suspended user', async () => { +describe("#users.activate", async () => { + it("should activate a suspended user", async () => { const { admin, user } = await seed(); await user.update({ suspendedById: admin.id, @@ -330,7 +330,7 @@ describe('#users.activate', async () => { }); expect(user.isSuspended).toBe(true); - const res = await server.post('/api/users.activate', { + const res = await server.post("/api/users.activate", { body: { token: admin.getJwtToken(), id: user.id, @@ -342,9 +342,9 @@ describe('#users.activate', async () => { expect(body).toMatchSnapshot(); }); - it('should require admin', async () => { + it("should require admin", async () => { const user = await buildUser(); - const res = await server.post('/api/users.activate', { + const res = await server.post("/api/users.activate", { body: { token: user.getJwtToken(), id: user.id }, }); const body = await res.json(); diff --git a/server/api/utils.js b/server/api/utils.js index 9f135994..19d788b6 100644 --- a/server/api/utils.js +++ b/server/api/utils.js @@ -1,22 +1,22 @@ // @flow -import debug from 'debug'; -import Router from 'koa-router'; -import subDays from 'date-fns/sub_days'; -import { AuthenticationError } from '../errors'; -import { Document, Attachment } from '../models'; -import { Op } from '../sequelize'; +import debug from "debug"; +import Router from "koa-router"; +import subDays from "date-fns/sub_days"; +import { AuthenticationError } from "../errors"; +import { Document, Attachment } from "../models"; +import { Op } from "../sequelize"; const router = new Router(); -const log = debug('utils'); +const log = debug("utils"); -router.post('utils.gc', async ctx => { +router.post("utils.gc", async ctx => { const { token } = ctx.body; if (process.env.UTILS_SECRET !== token) { - throw new AuthenticationError('Invalid secret token'); + throw new AuthenticationError("Invalid secret token"); } - log('Permanently deleting documents older than 30 days…'); + log("Permanently deleting documents older than 30 days…"); const where = { deletedAt: { @@ -24,8 +24,8 @@ router.post('utils.gc', async ctx => { }, }; - const documents = await Document.scope('withUnpublished').findAll({ - attributes: ['id'], + const documents = await Document.scope("withUnpublished").findAll({ + attributes: ["id"], where, }); const documentIds = documents.map(d => d.id); @@ -36,7 +36,7 @@ router.post('utils.gc', async ctx => { }, }); - await Document.scope('withUnpublished').destroy({ + await Document.scope("withUnpublished").destroy({ where, force: true, }); diff --git a/server/api/utils.test.js b/server/api/utils.test.js index 38b43357..a94fd540 100644 --- a/server/api/utils.test.js +++ b/server/api/utils.test.js @@ -1,19 +1,19 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import subDays from 'date-fns/sub_days'; -import app from '../app'; -import { Document } from '../models'; -import { sequelize } from '../sequelize'; -import { flushdb } from '../test/support'; -import { buildDocument } from '../test/factories'; +import TestServer from "fetch-test-server"; +import subDays from "date-fns/sub_days"; +import app from "../app"; +import { Document } from "../models"; +import { sequelize } from "../sequelize"; +import { flushdb } from "../test/support"; +import { buildDocument } from "../test/factories"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#utils.gc', async () => { - it('should destroy documents deleted more than 30 days ago', async () => { +describe("#utils.gc", async () => { + it("should destroy documents deleted more than 30 days ago", async () => { const document = await buildDocument({ publishedAt: new Date(), }); @@ -25,7 +25,7 @@ describe('#utils.gc', async () => { ).toISOString()}' WHERE id = '${document.id}'` ); - const res = await server.post('/api/utils.gc', { + const res = await server.post("/api/utils.gc", { body: { token: process.env.UTILS_SECRET, }, @@ -40,7 +40,7 @@ describe('#utils.gc', async () => { expect(reloaded).toBe(null); }); - it('should destroy draft documents deleted more than 30 days ago', async () => { + it("should destroy draft documents deleted more than 30 days ago", async () => { const document = await buildDocument({ publishedAt: undefined, }); @@ -52,7 +52,7 @@ describe('#utils.gc', async () => { ).toISOString()}' WHERE id = '${document.id}'` ); - const res = await server.post('/api/utils.gc', { + const res = await server.post("/api/utils.gc", { body: { token: process.env.UTILS_SECRET, }, @@ -67,8 +67,8 @@ describe('#utils.gc', async () => { expect(reloaded).toBe(null); }); - it('should require authentication', async () => { - const res = await server.post('/api/utils.gc'); + it("should require authentication", async () => { + const res = await server.post("/api/utils.gc"); expect(res.status).toEqual(401); }); }); diff --git a/server/api/views.js b/server/api/views.js index f64760b9..3d9b0a70 100644 --- a/server/api/views.js +++ b/server/api/views.js @@ -1,20 +1,20 @@ // @flow -import Router from 'koa-router'; -import auth from '../middlewares/authentication'; -import { presentView } from '../presenters'; -import { View, Document, Event } from '../models'; -import policy from '../policies'; +import Router from "koa-router"; +import auth from "../middlewares/authentication"; +import { presentView } from "../presenters"; +import { View, Document, Event } from "../models"; +import policy from "../policies"; const { authorize } = policy; const router = new Router(); -router.post('views.list', auth(), async ctx => { +router.post("views.list", auth(), async ctx => { const { documentId } = ctx.body; - ctx.assertUuid(documentId, 'documentId is required'); + ctx.assertUuid(documentId, "documentId is required"); const user = ctx.state.user; const document = await Document.findByPk(documentId, { userId: user.id }); - authorize(user, 'read', document); + authorize(user, "read", document); const views = await View.findByDocument(documentId); @@ -23,18 +23,18 @@ router.post('views.list', auth(), async ctx => { }; }); -router.post('views.create', auth(), async ctx => { +router.post("views.create", auth(), async ctx => { const { documentId } = ctx.body; - ctx.assertUuid(documentId, 'documentId is required'); + ctx.assertUuid(documentId, "documentId is required"); const user = ctx.state.user; const document = await Document.findByPk(documentId, { userId: user.id }); - authorize(user, 'read', document); + authorize(user, "read", document); const view = await View.increment({ documentId, userId: user.id }); await Event.create({ - name: 'views.create', + name: "views.create", actorId: user.id, documentId: document.id, collectionId: document.collectionId, diff --git a/server/api/views.test.js b/server/api/views.test.js index 2325352c..4fca2753 100644 --- a/server/api/views.test.js +++ b/server/api/views.test.js @@ -1,21 +1,21 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from '../app'; -import { View, CollectionUser } from '../models'; -import { flushdb, seed } from '../test/support'; -import { buildUser } from '../test/factories'; +import TestServer from "fetch-test-server"; +import app from "../app"; +import { View, CollectionUser } from "../models"; +import { flushdb, seed } from "../test/support"; +import { buildUser } from "../test/factories"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#views.list', async () => { - it('should return views for a document', async () => { +describe("#views.list", async () => { + it("should return views for a document", async () => { const { user, document } = await seed(); await View.increment({ documentId: document.id, userId: user.id }); - const res = await server.post('/api/views.list', { + const res = await server.post("/api/views.list", { body: { token: user.getJwtToken(), documentId: document.id }, }); const body = await res.json(); @@ -25,7 +25,7 @@ describe('#views.list', async () => { expect(body.data[0].user.name).toBe(user.name); }); - it('should return views for a document in read-only collection', async () => { + it("should return views for a document in read-only collection", async () => { const { user, document, collection } = await seed(); collection.private = true; await collection.save(); @@ -34,12 +34,12 @@ describe('#views.list', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read', + permission: "read", }); await View.increment({ documentId: document.id, userId: user.id }); - const res = await server.post('/api/views.list', { + const res = await server.post("/api/views.list", { body: { token: user.getJwtToken(), documentId: document.id }, }); const body = await res.json(); @@ -49,9 +49,9 @@ describe('#views.list', async () => { expect(body.data[0].user.name).toBe(user.name); }); - it('should require authentication', async () => { + it("should require authentication", async () => { const { document } = await seed(); - const res = await server.post('/api/views.list', { + const res = await server.post("/api/views.list", { body: { documentId: document.id }, }); const body = await res.json(); @@ -60,20 +60,20 @@ describe('#views.list', async () => { expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/views.list', { + const res = await server.post("/api/views.list", { body: { token: user.getJwtToken(), documentId: document.id }, }); expect(res.status).toEqual(403); }); }); -describe('#views.create', async () => { - it('should allow creating a view record for document', async () => { +describe("#views.create", async () => { + it("should allow creating a view record for document", async () => { const { user, document } = await seed(); - const res = await server.post('/api/views.create', { + const res = await server.post("/api/views.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); const body = await res.json(); @@ -82,7 +82,7 @@ describe('#views.create', async () => { expect(body.data.count).toBe(1); }); - it('should allow creating a view record for document in read-only collection', async () => { + it("should allow creating a view record for document in read-only collection", async () => { const { user, document, collection } = await seed(); collection.private = true; await collection.save(); @@ -91,10 +91,10 @@ describe('#views.create', async () => { createdById: user.id, collectionId: collection.id, userId: user.id, - permission: 'read', + permission: "read", }); - const res = await server.post('/api/views.create', { + const res = await server.post("/api/views.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); const body = await res.json(); @@ -103,9 +103,9 @@ describe('#views.create', async () => { expect(body.data.count).toBe(1); }); - it('should require authentication', async () => { + it("should require authentication", async () => { const { document } = await seed(); - const res = await server.post('/api/views.create', { + const res = await server.post("/api/views.create", { body: { documentId: document.id }, }); const body = await res.json(); @@ -114,10 +114,10 @@ describe('#views.create', async () => { expect(body).toMatchSnapshot(); }); - it('should require authorization', async () => { + it("should require authorization", async () => { const { document } = await seed(); const user = await buildUser(); - const res = await server.post('/api/views.create', { + const res = await server.post("/api/views.create", { body: { token: user.getJwtToken(), documentId: document.id }, }); expect(res.status).toEqual(403); diff --git a/server/app.js b/server/app.js index dd5175c9..a42bce3d 100644 --- a/server/app.js +++ b/server/app.js @@ -1,35 +1,35 @@ // @flow -import compress from 'koa-compress'; -import { compact } from 'lodash'; +import compress from "koa-compress"; +import { compact } from "lodash"; import helmet, { contentSecurityPolicy, dnsPrefetchControl, referrerPolicy, -} from 'koa-helmet'; -import logger from 'koa-logger'; -import mount from 'koa-mount'; -import enforceHttps from 'koa-sslify'; -import Koa from 'koa'; -import onerror from 'koa-onerror'; -import * as Sentry from '@sentry/node'; -import updates from './utils/updates'; +} from "koa-helmet"; +import logger from "koa-logger"; +import mount from "koa-mount"; +import enforceHttps from "koa-sslify"; +import Koa from "koa"; +import onerror from "koa-onerror"; +import * as Sentry from "@sentry/node"; +import updates from "./utils/updates"; -import auth from './auth'; -import api from './api'; -import emails from './emails'; -import routes from './routes'; +import auth from "./auth"; +import api from "./api"; +import emails from "./emails"; +import routes from "./routes"; const app = new Koa(); app.use(compress()); -if (process.env.NODE_ENV === 'development') { +if (process.env.NODE_ENV === "development") { /* eslint-disable global-require */ - const convert = require('koa-convert'); - const webpack = require('webpack'); - const devMiddleware = require('koa-webpack-dev-middleware'); - const hotMiddleware = require('koa-webpack-hot-middleware'); - const config = require('../webpack.config.dev'); + const convert = require("koa-convert"); + const webpack = require("webpack"); + const devMiddleware = require("koa-webpack-dev-middleware"); + const hotMiddleware = require("koa-webpack-hot-middleware"); + const config = require("../webpack.config.dev"); const compile = webpack(config); /* eslint-enable global-require */ @@ -61,24 +61,24 @@ if (process.env.NODE_ENV === 'development') { convert( hotMiddleware(compile, { log: console.log, // eslint-disable-line - path: '/__webpack_hmr', + path: "/__webpack_hmr", heartbeat: 10 * 1000, }) ) ); app.use(logger()); - app.use(mount('/emails', emails)); -} else if (process.env.NODE_ENV === 'production') { + app.use(mount("/emails", emails)); +} else if (process.env.NODE_ENV === "production") { // Force redirect to HTTPS protocol unless explicitly disabled - if (process.env.FORCE_HTTPS !== 'false') { + if (process.env.FORCE_HTTPS !== "false") { app.use( enforceHttps({ trustProtoHeader: true, }) ); } else { - console.warn('Enforced https was disabled with FORCE_HTTPS env variable'); + console.warn("Enforced https was disabled with FORCE_HTTPS env variable"); } // trust header fields set by our proxy. eg X-Forwarded-For @@ -97,24 +97,24 @@ if (process.env.SENTRY_DSN) { // emitted by Koa when bots attempt to snoop on paths such as wp-admin // or the user submits a bad request. These are expected in normal running // of the application - 'BadRequestError', - 'UnauthorizedError', + "BadRequestError", + "UnauthorizedError", ], }); } -app.on('error', (error, ctx) => { +app.on("error", (error, ctx) => { // we don't need to report every time a request stops to the bug tracker - if (error.code === 'EPIPE' || error.code === 'ECONNRESET') { - console.warn('Connection error', { error }); + if (error.code === "EPIPE" || error.code === "ECONNRESET") { + console.warn("Connection error", { error }); return; } if (process.env.SENTRY_DSN) { Sentry.withScope(function(scope) { - const requestId = ctx.headers['x-request-id']; + const requestId = ctx.headers["x-request-id"]; if (requestId) { - scope.setTag('request_id', requestId); + scope.setTag("request_id", requestId); } scope.addEventProcessor(function(event) { return Sentry.Handlers.parseRequest(event, ctx.request); @@ -126,8 +126,8 @@ app.on('error', (error, ctx) => { } }); -app.use(mount('/auth', auth)); -app.use(mount('/api', api)); +app.use(mount("/auth", auth)); +app.use(mount("/api", api)); app.use(helmet()); app.use( @@ -138,25 +138,25 @@ app.use( "'self'", "'unsafe-inline'", "'unsafe-eval'", - 'gist.github.com', - 'www.google-analytics.com', - 'browser.sentry-cdn.com', + "gist.github.com", + "www.google-analytics.com", + "browser.sentry-cdn.com", ], - styleSrc: ["'self'", "'unsafe-inline'", 'github.githubassets.com'], - imgSrc: ['*', 'data:', 'blob:'], - frameSrc: ['*'], + styleSrc: ["'self'", "'unsafe-inline'", "github.githubassets.com"], + imgSrc: ["*", "data:", "blob:"], + frameSrc: ["*"], connectSrc: compact([ "'self'", - process.env.AWS_S3_UPLOAD_BUCKET_URL.replace('s3:', 'localhost:'), - 'www.google-analytics.com', - 'api.github.com', - 'sentry.io', + process.env.AWS_S3_UPLOAD_BUCKET_URL.replace("s3:", "localhost:"), + "www.google-analytics.com", + "api.github.com", + "sentry.io", ]), }, }) ); app.use(dnsPrefetchControl({ allow: true })); -app.use(referrerPolicy({ policy: 'no-referrer' })); +app.use(referrerPolicy({ policy: "no-referrer" })); app.use(mount(routes)); /** @@ -165,8 +165,8 @@ app.use(mount(routes)); * Set ENABLE_UPDATES=false to disable them for your installation */ if ( - process.env.ENABLE_UPDATES !== 'false' && - process.env.NODE_ENV === 'production' + process.env.ENABLE_UPDATES !== "false" && + process.env.NODE_ENV === "production" ) { updates(); setInterval(updates, 24 * 3600 * 1000); diff --git a/server/auth/email.js b/server/auth/email.js index 135bd3d2..b18de1cb 100644 --- a/server/auth/email.js +++ b/server/auth/email.js @@ -1,23 +1,23 @@ // @flow -import Router from 'koa-router'; -import mailer from '../mailer'; -import subMinutes from 'date-fns/sub_minutes'; -import { getUserForEmailSigninToken } from '../utils/jwt'; -import { User, Team } from '../models'; -import methodOverride from '../middlewares/methodOverride'; -import validation from '../middlewares/validation'; -import auth from '../middlewares/authentication'; -import { AuthorizationError } from '../errors'; +import Router from "koa-router"; +import mailer from "../mailer"; +import subMinutes from "date-fns/sub_minutes"; +import { getUserForEmailSigninToken } from "../utils/jwt"; +import { User, Team } from "../models"; +import methodOverride from "../middlewares/methodOverride"; +import validation from "../middlewares/validation"; +import auth from "../middlewares/authentication"; +import { AuthorizationError } from "../errors"; const router = new Router(); router.use(methodOverride()); router.use(validation()); -router.post('email', async ctx => { +router.post("email", async ctx => { const { email } = ctx.body; - ctx.assertEmail(email, 'email is required'); + ctx.assertEmail(email, "email is required"); const user = await User.findOne({ where: { email: email.toLowerCase() }, @@ -29,7 +29,7 @@ router.post('email', async ctx => { // If the user matches an email address associated with an SSO // signin then just forward them directly to that service's // login page - if (user.service && user.service !== 'email') { + if (user.service && user.service !== "email") { return ctx.redirect(`${team.url}/auth/${user.service}`); } @@ -63,10 +63,10 @@ router.post('email', async ctx => { } }); -router.get('email.callback', auth({ required: false }), async ctx => { +router.get("email.callback", auth({ required: false }), async ctx => { const { token } = ctx.request.query; - ctx.assertPresent(token, 'token is required'); + ctx.assertPresent(token, "token is required"); try { const user = await getUserForEmailSigninToken(token); @@ -77,12 +77,12 @@ router.get('email.callback', auth({ required: false }), async ctx => { } if (!user.service) { - user.service = 'email'; + user.service = "email"; await user.save(); } // set cookies on response and redirect to team subdomain - ctx.signIn(user, team, 'email', false); + ctx.signIn(user, team, "email", false); } catch (err) { ctx.redirect(`${process.env.URL}?notice=expired-token`); } diff --git a/server/auth/google.js b/server/auth/google.js index 80bc1b16..779622e7 100644 --- a/server/auth/google.js +++ b/server/auth/google.js @@ -1,11 +1,11 @@ // @flow -import Sequelize from 'sequelize'; -import crypto from 'crypto'; -import Router from 'koa-router'; -import { capitalize } from 'lodash'; -import { OAuth2Client } from 'google-auth-library'; -import { User, Team, Event } from '../models'; -import auth from '../middlewares/authentication'; +import Sequelize from "sequelize"; +import crypto from "crypto"; +import Router from "koa-router"; +import { capitalize } from "lodash"; +import { OAuth2Client } from "google-auth-library"; +import { User, Team, Event } from "../models"; +import auth from "../middlewares/authentication"; const Op = Sequelize.Op; @@ -18,51 +18,51 @@ const client = new OAuth2Client( const allowedDomainsEnv = process.env.GOOGLE_ALLOWED_DOMAINS; // start the oauth process and redirect user to Google -router.get('google', async ctx => { +router.get("google", async ctx => { // Generate the url that will be used for the consent dialog. const authorizeUrl = client.generateAuthUrl({ - access_type: 'offline', + access_type: "offline", scope: [ - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email', + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", ], - prompt: 'consent', + prompt: "consent", }); ctx.redirect(authorizeUrl); }); // signin callback from Google -router.get('google.callback', auth({ required: false }), async ctx => { +router.get("google.callback", auth({ required: false }), async ctx => { const { code } = ctx.request.query; - ctx.assertPresent(code, 'code is required'); + ctx.assertPresent(code, "code is required"); const response = await client.getToken(code); client.setCredentials(response.tokens); const profile = await client.request({ - url: 'https://www.googleapis.com/oauth2/v1/userinfo', + url: "https://www.googleapis.com/oauth2/v1/userinfo", }); if (!profile.data.hd) { - ctx.redirect('/?notice=google-hd'); + ctx.redirect("/?notice=google-hd"); return; } // allow all domains by default if the env is not set - const allowedDomains = allowedDomainsEnv && allowedDomainsEnv.split(','); + const allowedDomains = allowedDomainsEnv && allowedDomainsEnv.split(","); if (allowedDomains && !allowedDomains.includes(profile.data.hd)) { - ctx.redirect('/?notice=hd-not-allowed'); + ctx.redirect("/?notice=hd-not-allowed"); return; } const googleId = profile.data.hd; - const hostname = profile.data.hd.split('.')[0]; + const hostname = profile.data.hd.split(".")[0]; const teamName = capitalize(hostname); // attempt to get logo from Clearbit API. If one doesn't exist then // fall back to using tiley to generate a placeholder logo - const hash = crypto.createHash('sha256'); + const hash = crypto.createHash("sha256"); hash.update(googleId); - const hashedGoogleId = hash.digest('hex'); + const hashedGoogleId = hash.digest("hex"); const cbUrl = `https://logo.clearbit.com/${profile.data.hd}`; const tileyUrl = `https://tiley.herokuapp.com/avatar/${hashedGoogleId}/${ teamName[0] @@ -85,7 +85,7 @@ router.get('google.callback', auth({ required: false }), async ctx => { where: { [Op.or]: [ { - service: 'google', + service: "google", serviceId: profile.data.id, }, { @@ -96,7 +96,7 @@ router.get('google.callback', auth({ required: false }), async ctx => { teamId: team.id, }, defaults: { - service: 'google', + service: "google", serviceId: profile.data.id, name: profile.data.name, email: profile.data.email, @@ -108,7 +108,7 @@ router.get('google.callback', auth({ required: false }), async ctx => { // update the user with fresh details if they just accepted an invite if (!user.serviceId || !user.service) { await user.update({ - service: 'google', + service: "google", serviceId: profile.data.id, avatarUrl: profile.data.picture, }); @@ -126,25 +126,25 @@ router.get('google.callback', auth({ required: false }), async ctx => { if (isFirstSignin) { await Event.create({ - name: 'users.create', + name: "users.create", actorId: user.id, userId: user.id, teamId: team.id, data: { name: user.name, - service: 'google', + service: "google", }, ip: ctx.request.ip, }); } // set cookies on response and redirect to team subdomain - ctx.signIn(user, team, 'google', isFirstSignin); + ctx.signIn(user, team, "google", isFirstSignin); } catch (err) { if (err instanceof Sequelize.UniqueConstraintError) { const exists = await User.findOne({ where: { - service: 'email', + service: "email", email: profile.data.email, teamId: team.id, }, diff --git a/server/auth/index.js b/server/auth/index.js index 5773e051..154da4b8 100644 --- a/server/auth/index.js +++ b/server/auth/index.js @@ -1,38 +1,38 @@ // @flow -import bodyParser from 'koa-bodyparser'; -import Koa from 'koa'; -import Router from 'koa-router'; -import validation from '../middlewares/validation'; -import auth from '../middlewares/authentication'; -import addMonths from 'date-fns/add_months'; -import { Team } from '../models'; -import { getCookieDomain } from '../../shared/utils/domains'; +import bodyParser from "koa-bodyparser"; +import Koa from "koa"; +import Router from "koa-router"; +import validation from "../middlewares/validation"; +import auth from "../middlewares/authentication"; +import addMonths from "date-fns/add_months"; +import { Team } from "../models"; +import { getCookieDomain } from "../../shared/utils/domains"; -import slack from './slack'; -import google from './google'; -import email from './email'; +import slack from "./slack"; +import google from "./google"; +import email from "./email"; const app = new Koa(); const router = new Router(); -router.use('/', slack.routes()); -router.use('/', google.routes()); -router.use('/', email.routes()); +router.use("/", slack.routes()); +router.use("/", google.routes()); +router.use("/", email.routes()); -router.get('/redirect', auth(), async ctx => { +router.get("/redirect", auth(), async ctx => { const user = ctx.state.user; // transfer access token cookie from root to subdomain - const rootToken = ctx.cookies.get('accessToken'); + const rootToken = ctx.cookies.get("accessToken"); const jwtToken = user.getJwtToken(); if (rootToken === jwtToken) { - ctx.cookies.set('accessToken', undefined, { + ctx.cookies.set("accessToken", undefined, { httpOnly: true, domain: getCookieDomain(ctx.request.hostname), }); - ctx.cookies.set('accessToken', jwtToken, { + ctx.cookies.set("accessToken", jwtToken, { httpOnly: false, expires: addMonths(new Date(), 3), }); diff --git a/server/auth/slack.js b/server/auth/slack.js index b2f00203..6071a796 100644 --- a/server/auth/slack.js +++ b/server/auth/slack.js @@ -1,10 +1,10 @@ // @flow -import Sequelize from 'sequelize'; -import Router from 'koa-router'; -import auth from '../middlewares/authentication'; -import addHours from 'date-fns/add_hours'; -import { getCookieDomain } from '../../shared/utils/domains'; -import { slackAuth } from '../../shared/utils/routeHelpers'; +import Sequelize from "sequelize"; +import Router from "koa-router"; +import auth from "../middlewares/authentication"; +import addHours from "date-fns/add_hours"; +import { getCookieDomain } from "../../shared/utils/domains"; +import { slackAuth } from "../../shared/utils/routeHelpers"; import { Authentication, Collection, @@ -12,19 +12,19 @@ import { User, Event, Team, -} from '../models'; -import * as Slack from '../slack'; +} from "../models"; +import * as Slack from "../slack"; const Op = Sequelize.Op; const router = new Router(); // start the oauth process and redirect user to Slack -router.get('slack', async ctx => { +router.get("slack", async ctx => { const state = Math.random() .toString(36) .substring(7); - ctx.cookies.set('state', state, { + ctx.cookies.set("state", state, { httpOnly: false, expires: addHours(new Date(), 1), domain: getCookieDomain(ctx.request.hostname), @@ -33,13 +33,13 @@ router.get('slack', async ctx => { }); // signin callback from Slack -router.get('slack.callback', auth({ required: false }), async ctx => { +router.get("slack.callback", auth({ required: false }), async ctx => { const { code, error, state } = ctx.request.query; - ctx.assertPresent(code || error, 'code is required'); - ctx.assertPresent(state, 'state is required'); + ctx.assertPresent(code || error, "code is required"); + ctx.assertPresent(state, "state is required"); - if (state !== ctx.cookies.get('state')) { - ctx.redirect('/?notice=auth-error&error=state_mismatch'); + if (state !== ctx.cookies.get("state")) { + ctx.redirect("/?notice=auth-error&error=state_mismatch"); return; } if (error) { @@ -64,7 +64,7 @@ router.get('slack.callback', auth({ required: false }), async ctx => { where: { [Op.or]: [ { - service: 'slack', + service: "slack", serviceId: data.user.id, }, { @@ -75,7 +75,7 @@ router.get('slack.callback', auth({ required: false }), async ctx => { teamId: team.id, }, defaults: { - service: 'slack', + service: "slack", serviceId: data.user.id, name: data.user.name, email: data.user.email, @@ -87,7 +87,7 @@ router.get('slack.callback', auth({ required: false }), async ctx => { // update the user with fresh details if they just accepted an invite if (!user.serviceId || !user.service) { await user.update({ - service: 'slack', + service: "slack", serviceId: data.user.id, avatarUrl: data.user.image_192, }); @@ -105,25 +105,25 @@ router.get('slack.callback', auth({ required: false }), async ctx => { if (isFirstSignin) { await Event.create({ - name: 'users.create', + name: "users.create", actorId: user.id, userId: user.id, teamId: team.id, data: { name: user.name, - service: 'slack', + service: "slack", }, ip: ctx.request.ip, }); } // set cookies on response and redirect to team subdomain - ctx.signIn(user, team, 'slack', isFirstSignin); + ctx.signIn(user, team, "slack", isFirstSignin); } catch (err) { if (err instanceof Sequelize.UniqueConstraintError) { const exists = await User.findOne({ where: { - service: 'email', + service: "email", email: data.user.email, teamId: team.id, }, @@ -142,10 +142,10 @@ router.get('slack.callback', auth({ required: false }), async ctx => { } }); -router.get('slack.commands', auth({ required: false }), async ctx => { +router.get("slack.commands", auth({ required: false }), async ctx => { const { code, state, error } = ctx.request.query; const user = ctx.state.user; - ctx.assertPresent(code || error, 'code is required'); + ctx.assertPresent(code || error, "code is required"); if (error) { ctx.redirect(`/settings/integrations/slack?error=${error}`); @@ -172,35 +172,35 @@ router.get('slack.commands', auth({ required: false }), async ctx => { } } - const endpoint = `${process.env.URL || ''}/auth/slack.commands`; + const endpoint = `${process.env.URL || ""}/auth/slack.commands`; const data = await Slack.oauthAccess(code, endpoint); const authentication = await Authentication.create({ - service: 'slack', + service: "slack", userId: user.id, teamId: user.teamId, token: data.access_token, - scopes: data.scope.split(','), + scopes: data.scope.split(","), }); await Integration.create({ - service: 'slack', - type: 'command', + service: "slack", + type: "command", userId: user.id, teamId: user.teamId, authenticationId: authentication.id, }); - ctx.redirect('/settings/integrations/slack'); + ctx.redirect("/settings/integrations/slack"); }); -router.get('slack.post', auth({ required: false }), async ctx => { +router.get("slack.post", auth({ required: false }), async ctx => { const { code, error, state } = ctx.request.query; const user = ctx.state.user; - ctx.assertPresent(code || error, 'code is required'); + ctx.assertPresent(code || error, "code is required"); const collectionId = state; - ctx.assertUuid(collectionId, 'collectionId must be an uuid'); + ctx.assertUuid(collectionId, "collectionId must be an uuid"); if (error) { ctx.redirect(`/settings/integrations/slack?error=${error}`); @@ -222,20 +222,20 @@ router.get('slack.post', auth({ required: false }), async ctx => { } } - const endpoint = `${process.env.URL || ''}/auth/slack.post`; + const endpoint = `${process.env.URL || ""}/auth/slack.post`; const data = await Slack.oauthAccess(code, endpoint); const authentication = await Authentication.create({ - service: 'slack', + service: "slack", userId: user.id, teamId: user.teamId, token: data.access_token, - scopes: data.scope.split(','), + scopes: data.scope.split(","), }); await Integration.create({ - service: 'slack', - type: 'post', + service: "slack", + type: "post", userId: user.id, teamId: user.teamId, authenticationId: authentication.id, @@ -248,7 +248,7 @@ router.get('slack.post', auth({ required: false }), async ctx => { }, }); - ctx.redirect('/settings/integrations/slack'); + ctx.redirect("/settings/integrations/slack"); }); export default router; diff --git a/server/commands/documentMover.js b/server/commands/documentMover.js index d5893d8d..56c4d1d1 100644 --- a/server/commands/documentMover.js +++ b/server/commands/documentMover.js @@ -1,7 +1,7 @@ // @flow -import { Document, Collection, Event } from '../models'; -import { sequelize } from '../sequelize'; -import { type Context } from 'koa'; +import { Document, Collection, Event } from "../models"; +import { sequelize } from "../sequelize"; +import { type Context } from "koa"; export default async function documentMover({ user, @@ -77,7 +77,7 @@ export default async function documentMover({ await transaction.commit(); await Event.create({ - name: 'documents.move', + name: "documents.move", actorId: user.id, documentId: document.id, collectionId, diff --git a/server/commands/documentMover.test.js b/server/commands/documentMover.test.js index 904ed7d4..9df52e18 100644 --- a/server/commands/documentMover.test.js +++ b/server/commands/documentMover.test.js @@ -1,14 +1,14 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import documentMover from '../commands/documentMover'; -import { flushdb, seed } from '../test/support'; -import { buildDocument, buildCollection } from '../test/factories'; +import documentMover from "../commands/documentMover"; +import { flushdb, seed } from "../test/support"; +import { buildDocument, buildCollection } from "../test/factories"; beforeEach(flushdb); -describe('documentMover', async () => { - const ip = '127.0.0.1'; +describe("documentMover", async () => { + const ip = "127.0.0.1"; - it('should move within a collection', async () => { + it("should move within a collection", async () => { const { document, user, collection } = await seed(); const response = await documentMover({ @@ -22,15 +22,15 @@ describe('documentMover', async () => { expect(response.documents.length).toEqual(1); }); - it('should move with children', async () => { + it("should move with children", async () => { const { document, user, collection } = await seed(); const newDocument = await buildDocument({ parentDocumentId: document.id, collectionId: collection.id, teamId: collection.teamId, userId: collection.creatorId, - title: 'Child document', - text: 'content', + title: "Child document", + text: "content", }); await collection.addDocumentToStructure(newDocument); @@ -50,7 +50,7 @@ describe('documentMover', async () => { expect(response.documents.length).toEqual(1); }); - it('should move with children to another collection', async () => { + it("should move with children to another collection", async () => { const { document, user, collection } = await seed(); const newCollection = await buildCollection({ teamId: collection.teamId, @@ -60,8 +60,8 @@ describe('documentMover', async () => { collectionId: collection.id, teamId: collection.teamId, userId: collection.creatorId, - title: 'Child document', - text: 'content', + title: "Child document", + text: "content", }); await collection.addDocumentToStructure(newDocument); diff --git a/server/commands/userInviter.js b/server/commands/userInviter.js index 117c5996..22d39434 100644 --- a/server/commands/userInviter.js +++ b/server/commands/userInviter.js @@ -1,8 +1,8 @@ // @flow -import { uniqBy } from 'lodash'; -import { User, Event, Team } from '../models'; -import mailer from '../mailer'; -import { sequelize } from '../sequelize'; +import { uniqBy } from "lodash"; +import { User, Event, Team } from "../models"; +import mailer from "../mailer"; +import { sequelize } from "../sequelize"; type Invite = { name: string, email: string, guest: boolean }; @@ -19,7 +19,7 @@ export default async function userInviter({ // filter out empties and obvious non-emails const compactedInvites = invites.filter( - invite => !!invite.email.trim() && invite.email.match('@') + invite => !!invite.email.trim() && invite.email.match("@") ); // normalize to lowercase and remove duplicates @@ -28,7 +28,7 @@ export default async function userInviter({ ...invite, email: invite.email.toLowerCase(), })), - 'email' + "email" ); // filter out any existing users in the system @@ -63,7 +63,7 @@ export default async function userInviter({ users.push(newUser); await Event.create( { - name: 'users.invite', + name: "users.invite", actorId: user.id, teamId: user.teamId, data: { diff --git a/server/commands/userInviter.test.js b/server/commands/userInviter.test.js index f75aab71..3a22c820 100644 --- a/server/commands/userInviter.test.js +++ b/server/commands/userInviter.test.js @@ -1,49 +1,49 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import userInviter from '../commands/userInviter'; -import { flushdb } from '../test/support'; -import { buildUser } from '../test/factories'; +import userInviter from "../commands/userInviter"; +import { flushdb } from "../test/support"; +import { buildUser } from "../test/factories"; beforeEach(flushdb); -describe('userInviter', async () => { - const ip = '127.0.0.1'; +describe("userInviter", async () => { + const ip = "127.0.0.1"; - it('should return sent invites', async () => { + it("should return sent invites", async () => { const user = await buildUser(); const response = await userInviter({ - invites: [{ email: 'test@example.com', name: 'Test' }], + invites: [{ email: "test@example.com", name: "Test" }], user, ip, }); expect(response.sent.length).toEqual(1); }); - it('should filter empty invites', async () => { + it("should filter empty invites", async () => { const user = await buildUser(); const response = await userInviter({ - invites: [{ email: ' ', name: 'Test' }], + invites: [{ email: " ", name: "Test" }], user, ip, }); expect(response.sent.length).toEqual(0); }); - it('should filter obviously bunk emails', async () => { + it("should filter obviously bunk emails", async () => { const user = await buildUser(); const response = await userInviter({ - invites: [{ email: 'notanemail', name: 'Test' }], + invites: [{ email: "notanemail", name: "Test" }], user, ip, }); expect(response.sent.length).toEqual(0); }); - it('should not send duplicates', async () => { + it("should not send duplicates", async () => { const user = await buildUser(); const response = await userInviter({ invites: [ - { email: 'the@same.com', name: 'Test' }, - { email: 'the@SAME.COM', name: 'Test' }, + { email: "the@same.com", name: "Test" }, + { email: "the@SAME.COM", name: "Test" }, ], user, ip, @@ -51,7 +51,7 @@ describe('userInviter', async () => { expect(response.sent.length).toEqual(1); }); - it('should not send invites to existing team members', async () => { + it("should not send invites to existing team members", async () => { const user = await buildUser(); const response = await userInviter({ invites: [{ email: user.email, name: user.name }], diff --git a/server/emails/CollectionNotificationEmail.js b/server/emails/CollectionNotificationEmail.js index b5a0238f..62f882a8 100644 --- a/server/emails/CollectionNotificationEmail.js +++ b/server/emails/CollectionNotificationEmail.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import { User, Collection } from '../models'; -import EmailTemplate from './components/EmailLayout'; -import Body from './components/Body'; -import Button from './components/Button'; -import Heading from './components/Heading'; -import Header from './components/Header'; -import Footer from './components/Footer'; -import EmptySpace from './components/EmptySpace'; +import * as React from "react"; +import { User, Collection } from "../models"; +import EmailTemplate from "./components/EmailLayout"; +import Body from "./components/Body"; +import Button from "./components/Button"; +import Heading from "./components/Heading"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import EmptySpace from "./components/EmptySpace"; export type Props = { actor: User, @@ -19,7 +19,7 @@ export type Props = { export const collectionNotificationEmailText = ({ actor, collection, - eventName = 'created', + eventName = "created", }: Props) => ` ${collection.name} @@ -31,7 +31,7 @@ Open Collection: ${process.env.URL}${collection.url} export const CollectionNotificationEmail = ({ actor, collection, - eventName = 'created', + eventName = "created", unsubscribeUrl, }: Props) => { return ( diff --git a/server/emails/DocumentNotificationEmail.js b/server/emails/DocumentNotificationEmail.js index dc3199ae..dc1c4248 100644 --- a/server/emails/DocumentNotificationEmail.js +++ b/server/emails/DocumentNotificationEmail.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import { User, Document, Team, Collection } from '../models'; -import EmailTemplate from './components/EmailLayout'; -import Body from './components/Body'; -import Button from './components/Button'; -import Heading from './components/Heading'; -import Header from './components/Header'; -import Footer from './components/Footer'; -import EmptySpace from './components/EmptySpace'; +import * as React from "react"; +import { User, Document, Team, Collection } from "../models"; +import EmailTemplate from "./components/EmailLayout"; +import Body from "./components/Body"; +import Button from "./components/Button"; +import Heading from "./components/Heading"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import EmptySpace from "./components/EmptySpace"; export type Props = { actor: User, @@ -23,7 +23,7 @@ export const documentNotificationEmailText = ({ team, document, collection, - eventName = 'published', + eventName = "published", }: Props) => ` "${document.title}" ${eventName} @@ -39,7 +39,7 @@ export const DocumentNotificationEmail = ({ team, document, collection, - eventName = 'published', + eventName = "published", unsubscribeUrl, }: Props) => { return ( @@ -51,7 +51,7 @@ export const DocumentNotificationEmail = ({ "{document.title}" {eventName}

    - {actor.name} {eventName} the document "{document.title}", in the{' '} + {actor.name} {eventName} the document "{document.title}", in the{" "} {collection.name} collection.


    diff --git a/server/emails/ExportEmail.js b/server/emails/ExportEmail.js index 14b3572d..7be8245a 100644 --- a/server/emails/ExportEmail.js +++ b/server/emails/ExportEmail.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import EmailTemplate from './components/EmailLayout'; -import Body from './components/Body'; -import Button from './components/Button'; -import Heading from './components/Heading'; -import Header from './components/Header'; -import Footer from './components/Footer'; -import EmptySpace from './components/EmptySpace'; +import * as React from "react"; +import EmailTemplate from "./components/EmailLayout"; +import Body from "./components/Body"; +import Button from "./components/Button"; +import Heading from "./components/Heading"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import EmptySpace from "./components/EmptySpace"; export const exportEmailText = ` Your Data Export diff --git a/server/emails/InviteEmail.js b/server/emails/InviteEmail.js index 46a89cfe..c034cb3e 100644 --- a/server/emails/InviteEmail.js +++ b/server/emails/InviteEmail.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import EmailTemplate from './components/EmailLayout'; -import Body from './components/Body'; -import Button from './components/Button'; -import Heading from './components/Heading'; -import Header from './components/Header'; -import Footer from './components/Footer'; -import EmptySpace from './components/EmptySpace'; +import * as React from "react"; +import EmailTemplate from "./components/EmailLayout"; +import Body from "./components/Body"; +import Button from "./components/Button"; +import Heading from "./components/Heading"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import EmptySpace from "./components/EmptySpace"; export type Props = { name: string, @@ -30,7 +30,7 @@ ${actorName} (${ actorEmail }) has invited you to join Outline, a place for your team to build and share knowledge. -Join now: ${teamUrl}${guest ? '?guest=true' : ''} +Join now: ${teamUrl}${guest ? "?guest=true" : ""} `; export const InviteEmail = ({ @@ -52,7 +52,7 @@ export const InviteEmail = ({

    -

    diff --git a/server/emails/SigninEmail.js b/server/emails/SigninEmail.js index 2bf6e480..939c3447 100644 --- a/server/emails/SigninEmail.js +++ b/server/emails/SigninEmail.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import EmailTemplate from './components/EmailLayout'; -import Body from './components/Body'; -import Button from './components/Button'; -import Heading from './components/Heading'; -import Header from './components/Header'; -import Footer from './components/Footer'; -import EmptySpace from './components/EmptySpace'; +import * as React from "react"; +import EmailTemplate from "./components/EmailLayout"; +import Body from "./components/Body"; +import Button from "./components/Button"; +import Heading from "./components/Heading"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import EmptySpace from "./components/EmptySpace"; export type Props = { token: string, diff --git a/server/emails/WelcomeEmail.js b/server/emails/WelcomeEmail.js index e1a1bb0d..4f4024e5 100644 --- a/server/emails/WelcomeEmail.js +++ b/server/emails/WelcomeEmail.js @@ -1,12 +1,12 @@ // @flow -import * as React from 'react'; -import EmailTemplate from './components/EmailLayout'; -import Body from './components/Body'; -import Button from './components/Button'; -import Heading from './components/Heading'; -import Header from './components/Header'; -import Footer from './components/Footer'; -import EmptySpace from './components/EmptySpace'; +import * as React from "react"; +import EmailTemplate from "./components/EmailLayout"; +import Body from "./components/Body"; +import Button from "./components/Button"; +import Heading from "./components/Heading"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import EmptySpace from "./components/EmptySpace"; export type Props = { teamUrl: string, diff --git a/server/emails/components/Body.js b/server/emails/components/Body.js index 06c494e6..d0f74c96 100644 --- a/server/emails/components/Body.js +++ b/server/emails/components/Body.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import { Table, TBody, TR, TD } from 'oy-vey'; +import * as React from "react"; +import { Table, TBody, TR, TD } from "oy-vey"; -import EmptySpace from './EmptySpace'; +import EmptySpace from "./EmptySpace"; type Props = { children: React.Node, diff --git a/server/emails/components/Button.js b/server/emails/components/Button.js index 6b81faa5..812691d5 100644 --- a/server/emails/components/Button.js +++ b/server/emails/components/Button.js @@ -1,18 +1,18 @@ // @flow -import * as React from 'react'; +import * as React from "react"; type Props = { href: string, children: React.Node }; export default (props: Props) => { const style = { - display: 'inline-block', - padding: '10px 20px', - color: '#FFFFFF', - background: '#000000', - borderRadius: '4px', + display: "inline-block", + padding: "10px 20px", + color: "#FFFFFF", + background: "#000000", + borderRadius: "4px", fontWeight: 500, - textDecoration: 'none', - cursor: 'pointer', + textDecoration: "none", + cursor: "pointer", }; return ( diff --git a/server/emails/components/EmailLayout.js b/server/emails/components/EmailLayout.js index 17e8cf27..80424781 100644 --- a/server/emails/components/EmailLayout.js +++ b/server/emails/components/EmailLayout.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import { Table, TBody, TR, TD } from 'oy-vey'; -import theme from '../../../shared/styles/theme'; +import * as React from "react"; +import { Table, TBody, TR, TD } from "oy-vey"; +import theme from "../../../shared/styles/theme"; type Props = { children: React.Node, diff --git a/server/emails/components/EmptySpace.js b/server/emails/components/EmptySpace.js index 20635374..051fbeb0 100644 --- a/server/emails/components/EmptySpace.js +++ b/server/emails/components/EmptySpace.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import { Table, TBody, TR, TD } from 'oy-vey'; +import * as React from "react"; +import { Table, TBody, TR, TD } from "oy-vey"; const EmptySpace = ({ height }: { height?: number }) => { height = height || 16; const style = { lineHeight: `${height}px`, - fontSize: '1px', - msoLineHeightRule: 'exactly', + fontSize: "1px", + msoLineHeightRule: "exactly", }; return ( @@ -18,7 +18,7 @@ const EmptySpace = ({ height }: { height?: number }) => { width="100%" height={`${height}px`} style={style} - dangerouslySetInnerHTML={{ __html: ' ' }} + dangerouslySetInnerHTML={{ __html: " " }} /> diff --git a/server/emails/components/Footer.js b/server/emails/components/Footer.js index 548d529e..f32d59e8 100644 --- a/server/emails/components/Footer.js +++ b/server/emails/components/Footer.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import { Table, TBody, TR, TD } from 'oy-vey'; -import { twitterUrl, spectrumUrl } from '../../../shared/utils/routeHelpers'; -import theme from '../../../shared/styles/theme'; +import * as React from "react"; +import { Table, TBody, TR, TD } from "oy-vey"; +import { twitterUrl, spectrumUrl } from "../../../shared/utils/routeHelpers"; +import theme from "../../../shared/styles/theme"; type Props = { unsubscribeUrl?: string, @@ -10,29 +10,29 @@ type Props = { export default ({ unsubscribeUrl }: Props) => { const footerStyle = { - padding: '20px 0', + padding: "20px 0", borderTop: `1px solid ${theme.smokeDark}`, color: theme.slate, - fontSize: '14px', + fontSize: "14px", }; const unsubStyle = { - padding: '0', + padding: "0", color: theme.slate, - fontSize: '14px', + fontSize: "14px", }; const linkStyle = { color: theme.slate, fontWeight: 500, - textDecoration: 'none', - marginRight: '10px', + textDecoration: "none", + marginRight: "10px", }; const externalLinkStyle = { color: theme.slate, - textDecoration: 'none', - margin: '0 10px', + textDecoration: "none", + margin: "0 10px", }; return ( diff --git a/server/emails/components/Header.js b/server/emails/components/Header.js index fd25e4cb..54d0fe9d 100644 --- a/server/emails/components/Header.js +++ b/server/emails/components/Header.js @@ -1,7 +1,7 @@ // @flow -import * as React from 'react'; -import { Table, TBody, TR, TD } from 'oy-vey'; -import EmptySpace from './EmptySpace'; +import * as React from "react"; +import { Table, TBody, TR, TD } from "oy-vey"; +import EmptySpace from "./EmptySpace"; export default () => { return ( diff --git a/server/emails/components/Heading.js b/server/emails/components/Heading.js index 3d7a1009..6138b2ac 100644 --- a/server/emails/components/Heading.js +++ b/server/emails/components/Heading.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; +import * as React from "react"; const style = { fontWeight: 500, - fontSize: '18px', + fontSize: "18px", }; type Props = { diff --git a/server/emails/index.js b/server/emails/index.js index cb6c86b6..83d187a2 100644 --- a/server/emails/index.js +++ b/server/emails/index.js @@ -1,13 +1,13 @@ // @flow -import Koa from 'koa'; -import Router from 'koa-router'; -import { NotFoundError } from '../errors'; -import { Mailer } from '../mailer'; +import Koa from "koa"; +import Router from "koa-router"; +import { NotFoundError } from "../errors"; +import { Mailer } from "../mailer"; const emailPreviews = new Koa(); const router = new Router(); -router.get('/:type/:format', async ctx => { +router.get("/:type/:format", async ctx => { let mailerOutput; let mailer = new Mailer(); mailer.transporter = { @@ -21,13 +21,13 @@ router.get('/:type/:format', async ctx => { default: if (Object.getOwnPropertyNames(mailer).includes(ctx.params.type)) { // $FlowIssue flow doesn't like this but we're ok with it - mailer[ctx.params.type]('user@example.com'); - } else throw new NotFoundError('Email template could not be found'); + mailer[ctx.params.type]("user@example.com"); + } else throw new NotFoundError("Email template could not be found"); } if (!mailerOutput) return; - if (ctx.params.format === 'text') { + if (ctx.params.format === "text") { ctx.body = mailerOutput.text; } else { ctx.body = mailerOutput.html; diff --git a/server/errors.js b/server/errors.js index 26a1111c..4e72e981 100644 --- a/server/errors.js +++ b/server/errors.js @@ -1,53 +1,53 @@ // @flow -import httpErrors from 'http-errors'; +import httpErrors from "http-errors"; export function AuthenticationError( - message: string = 'Invalid authentication' + message: string = "Invalid authentication" ) { - return httpErrors(401, message, { id: 'authentication_required' }); + return httpErrors(401, message, { id: "authentication_required" }); } export function AuthorizationError( - message: string = 'You do not have permission to access this resource' + message: string = "You do not have permission to access this resource" ) { - return httpErrors(403, message, { id: 'permission_required' }); + return httpErrors(403, message, { id: "permission_required" }); } export function AdminRequiredError( - message: string = 'An admin role is required to access this resource' + message: string = "An admin role is required to access this resource" ) { - return httpErrors(403, message, { id: 'admin_required' }); + return httpErrors(403, message, { id: "admin_required" }); } export function UserSuspendedError({ adminEmail }: { adminEmail: string }) { - return httpErrors(403, 'Your access has been suspended by the team admin', { - id: 'user_suspended', + return httpErrors(403, "Your access has been suspended by the team admin", { + id: "user_suspended", errorData: { adminEmail, }, }); } -export function InvalidRequestError(message: string = 'Request invalid') { - return httpErrors(400, message, { id: 'invalid_request' }); +export function InvalidRequestError(message: string = "Request invalid") { + return httpErrors(400, message, { id: "invalid_request" }); } -export function NotFoundError(message: string = 'Resource not found') { - return httpErrors(404, message, { id: 'not_found' }); +export function NotFoundError(message: string = "Resource not found") { + return httpErrors(404, message, { id: "not_found" }); } export function ParamRequiredError( - message: string = 'Required parameter missing' + message: string = "Required parameter missing" ) { - return httpErrors(400, message, { id: 'param_required' }); + return httpErrors(400, message, { id: "param_required" }); } -export function ValidationError(message: string = 'Validation failed') { - return httpErrors(400, message, { id: 'validation_error' }); +export function ValidationError(message: string = "Validation failed") { + return httpErrors(400, message, { id: "validation_error" }); } export function EditorUpdateError( - message: string = 'The client editor is out of date and must be reloaded' + message: string = "The client editor is out of date and must be reloaded" ) { - return httpErrors(400, message, { id: 'editor_update_required' }); + return httpErrors(400, message, { id: "editor_update_required" }); } diff --git a/server/events.js b/server/events.js index 3619466f..c1747188 100644 --- a/server/events.js +++ b/server/events.js @@ -1,21 +1,21 @@ // @flow -import * as Sentry from '@sentry/node'; -import { createQueue } from './utils/queue'; -import services from './services'; +import * as Sentry from "@sentry/node"; +import { createQueue } from "./utils/queue"; +import services from "./services"; export type UserEvent = | { name: | 'users.create' // eslint-disable-line - | 'users.update' - | 'users.suspend' - | 'users.activate' - | 'users.delete', + | "users.update" + | "users.suspend" + | "users.activate" + | "users.delete", userId: string, teamId: string, actorId: string, } | { - name: 'users.invite', + name: "users.invite", teamId: string, actorId: string, data: { @@ -27,22 +27,22 @@ export type UserEvent = export type DocumentEvent = | { name: | 'documents.create' // eslint-disable-line - | 'documents.publish' - | 'documents.delete' - | 'documents.pin' - | 'documents.unpin' - | 'documents.archive' - | 'documents.unarchive' - | 'documents.restore' - | 'documents.star' - | 'documents.unstar', + | "documents.publish" + | "documents.delete" + | "documents.pin" + | "documents.unpin" + | "documents.archive" + | "documents.unarchive" + | "documents.restore" + | "documents.star" + | "documents.unstar", documentId: string, collectionId: string, teamId: string, actorId: string, } | { - name: 'documents.move', + name: "documents.move", documentId: string, collectionId: string, teamId: string, @@ -53,7 +53,7 @@ export type DocumentEvent = }, } | { - name: 'documents.update', + name: "documents.update", documentId: string, collectionId: string, teamId: string, @@ -67,21 +67,21 @@ export type DocumentEvent = export type CollectionEvent = | { name: | 'collections.create' // eslint-disable-line - | 'collections.update' - | 'collections.delete', + | "collections.update" + | "collections.delete", collectionId: string, teamId: string, actorId: string, } | { - name: 'collections.add_user' | 'collections.remove_user', + name: "collections.add_user" | "collections.remove_user", userId: string, collectionId: string, teamId: string, actorId: string, } | { - name: 'collections.add_group' | 'collections.remove_group', + name: "collections.add_group" | "collections.remove_group", collectionId: string, teamId: string, actorId: string, @@ -91,7 +91,7 @@ export type CollectionEvent = export type GroupEvent = | { - name: 'groups.create' | 'groups.delete' | 'groups.update', + name: "groups.create" | "groups.delete" | "groups.update", actorId: string, modelId: string, teamId: string, @@ -99,7 +99,7 @@ export type GroupEvent = ip: string, } | { - name: 'groups.add_user' | 'groups.remove_user', + name: "groups.add_user" | "groups.remove_user", actorId: string, userId: string, modelId: string, @@ -109,7 +109,7 @@ export type GroupEvent = }; export type IntegrationEvent = { - name: 'integrations.create' | 'integrations.update', + name: "integrations.create" | "integrations.update", modelId: string, teamId: string, actorId: string, @@ -122,8 +122,8 @@ export type Event = | IntegrationEvent | GroupEvent; -const globalEventsQueue = createQueue('global events'); -const serviceEventsQueue = createQueue('service events'); +const globalEventsQueue = createQueue("global events"); +const serviceEventsQueue = createQueue("service events"); // this queue processes global events and hands them off to service hooks globalEventsQueue.process(async job => { @@ -148,7 +148,7 @@ serviceEventsQueue.process(async job => { service.on(event).catch(error => { if (process.env.SENTRY_DSN) { Sentry.withScope(function(scope) { - scope.setExtra('event', event); + scope.setExtra("event", event); Sentry.captureException(error); }); } else { diff --git a/server/index.js b/server/index.js index 4f103101..1ec39eda 100644 --- a/server/index.js +++ b/server/index.js @@ -1,22 +1,22 @@ // @flow -import http from 'http'; -import IO from 'socket.io'; -import SocketAuth from 'socketio-auth'; -import socketRedisAdapter from 'socket.io-redis'; -import { getUserForJWT } from './utils/jwt'; -import { Document, Collection, View } from './models'; -import { client, subscriber } from './redis'; -import app from './app'; -import policy from './policies'; +import http from "http"; +import IO from "socket.io"; +import SocketAuth from "socketio-auth"; +import socketRedisAdapter from "socket.io-redis"; +import { getUserForJWT } from "./utils/jwt"; +import { Document, Collection, View } from "./models"; +import { client, subscriber } from "./redis"; +import app from "./app"; +import policy from "./policies"; const server = http.createServer(app.callback()); let io; -if (process.env.WEBSOCKETS_ENABLED === 'true') { +if (process.env.WEBSOCKETS_ENABLED === "true") { const { can } = policy; io = IO(server, { - path: '/realtime', + path: "/realtime", serveClient: false, cookie: false, }); @@ -38,7 +38,7 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { // store the mapping between socket id and user id in redis // so that it is accessible across multiple server nodes - await client.hset(socket.id, 'userId', user.id); + await client.hset(socket.id, "userId", user.id); return callback(null, true); } catch (err) { @@ -64,15 +64,15 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { socket.join(rooms); // allow the client to request to join rooms - socket.on('join', async event => { + socket.on("join", async event => { // user is joining a collection channel, because their permissions have // changed, granting them access. if (event.collectionId) { const collection = await Collection.scope({ - method: ['withMembership', user.id], + method: ["withMembership", user.id], }).findByPk(event.collectionId); - if (can(user, 'read', collection)) { + if (can(user, "read", collection)) { socket.join(`collection-${event.collectionId}`); } } @@ -84,7 +84,7 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { userId: user.id, }); - if (can(user, 'read', document)) { + if (can(user, "read", document)) { const room = `document-${event.documentId}`; await View.touch(event.documentId, user.id, event.isEditing); @@ -94,7 +94,7 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { socket.join(room, () => { // let everyone else in the room know that a new user joined - io.to(room).emit('user.join', { + io.to(room).emit("user.join", { userId: user.id, documentId: event.documentId, isEditing: event.isEditing, @@ -109,10 +109,10 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { // makes this easy. let userIds = new Map(); for (const socketId of sockets) { - const userId = await client.hget(socketId, 'userId'); + const userId = await client.hget(socketId, "userId"); userIds.set(userId, userId); } - socket.emit('document.presence', { + socket.emit("document.presence", { documentId: event.documentId, userIds: Array.from(userIds.keys()), editingIds: editing.map(view => view.userId), @@ -124,14 +124,14 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { }); // allow the client to request to leave rooms - socket.on('leave', event => { + socket.on("leave", event => { if (event.collectionId) { socket.leave(`collection-${event.collectionId}`); } if (event.documentId) { const room = `document-${event.documentId}`; socket.leave(room, () => { - io.to(room).emit('user.leave', { + io.to(room).emit("user.leave", { userId: user.id, documentId: event.documentId, }); @@ -139,13 +139,13 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { } }); - socket.on('disconnecting', () => { + socket.on("disconnecting", () => { const rooms = Object.keys(socket.rooms); rooms.forEach(room => { - if (room.startsWith('document-')) { - const documentId = room.replace('document-', ''); - io.to(room).emit('user.leave', { + if (room.startsWith("document-")) { + const documentId = room.replace("document-", ""); + io.to(room).emit("user.leave", { userId: user.id, documentId, }); @@ -153,7 +153,7 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { }); }); - socket.on('presence', async event => { + socket.on("presence", async event => { const room = `document-${event.documentId}`; if (event.documentId && socket.rooms[room]) { @@ -164,7 +164,7 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { ); view.user = user; - io.to(room).emit('user.presence', { + io.to(room).emit("user.presence", { userId: user.id, documentId: event.documentId, isEditing: event.isEditing, @@ -175,16 +175,16 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') { }); } -server.on('error', err => { +server.on("error", err => { throw err; }); -server.on('listening', () => { +server.on("listening", () => { const address = server.address(); console.log(`\n> Listening on http://localhost:${address.port}\n`); }); -server.listen(process.env.PORT || '3000'); +server.listen(process.env.PORT || "3000"); export const socketio = io; diff --git a/server/logistics.js b/server/logistics.js index 2cd19037..37d589b1 100644 --- a/server/logistics.js +++ b/server/logistics.js @@ -1,31 +1,31 @@ // @flow -import debug from 'debug'; -import mailer from './mailer'; -import { Collection, Team } from './models'; -import { archiveCollections } from './utils/zip'; -import { createQueue } from './utils/queue'; +import debug from "debug"; +import mailer from "./mailer"; +import { Collection, Team } from "./models"; +import { archiveCollections } from "./utils/zip"; +import { createQueue } from "./utils/queue"; -const log = debug('logistics'); -const logisticsQueue = createQueue('logistics'); +const log = debug("logistics"); +const logisticsQueue = createQueue("logistics"); const queueOptions = { attempts: 2, removeOnComplete: true, backoff: { - type: 'exponential', + type: "exponential", delay: 60 * 1000, }, }; async function exportAndEmailCollections(teamId: string, email: string) { - log('Archiving team', teamId); + log("Archiving team", teamId); const team = await Team.findByPk(teamId); const collections = await Collection.findAll({ where: { teamId }, - order: [['name', 'ASC']], + order: [["name", "ASC"]], }); const filePath = await archiveCollections(collections); - log('Archive path', filePath); + log("Archive path", filePath); mailer.export({ to: email, @@ -39,10 +39,10 @@ async function exportAndEmailCollections(teamId: string, email: string) { } logisticsQueue.process(async job => { - log('Process', job.data); + log("Process", job.data); switch (job.data.type) { - case 'export-collections': + case "export-collections": return await exportAndEmailCollections(job.data.teamId, job.data.email); default: } @@ -51,7 +51,7 @@ logisticsQueue.process(async job => { export const exportCollections = (teamId: string, email: string) => { logisticsQueue.add( { - type: 'export-collections', + type: "export-collections", teamId, email, }, diff --git a/server/mailer.js b/server/mailer.js index 80d31a30..04eee026 100644 --- a/server/mailer.js +++ b/server/mailer.js @@ -1,33 +1,33 @@ // @flow -import * as React from 'react'; -import debug from 'debug'; -import * as Sentry from '@sentry/node'; -import nodemailer from 'nodemailer'; -import Oy from 'oy-vey'; -import { createQueue } from './utils/queue'; -import { baseStyles } from './emails/components/EmailLayout'; -import { WelcomeEmail, welcomeEmailText } from './emails/WelcomeEmail'; -import { ExportEmail, exportEmailText } from './emails/ExportEmail'; -import { SigninEmail, signinEmailText } from './emails/SigninEmail'; +import * as React from "react"; +import debug from "debug"; +import * as Sentry from "@sentry/node"; +import nodemailer from "nodemailer"; +import Oy from "oy-vey"; +import { createQueue } from "./utils/queue"; +import { baseStyles } from "./emails/components/EmailLayout"; +import { WelcomeEmail, welcomeEmailText } from "./emails/WelcomeEmail"; +import { ExportEmail, exportEmailText } from "./emails/ExportEmail"; +import { SigninEmail, signinEmailText } from "./emails/SigninEmail"; import { type Props as InviteEmailT, InviteEmail, inviteEmailText, -} from './emails/InviteEmail'; +} from "./emails/InviteEmail"; import { type Props as DocumentNotificationEmailT, DocumentNotificationEmail, documentNotificationEmailText, -} from './emails/DocumentNotificationEmail'; +} from "./emails/DocumentNotificationEmail"; import { type Props as CollectionNotificationEmailT, CollectionNotificationEmail, collectionNotificationEmailText, -} from './emails/CollectionNotificationEmail'; +} from "./emails/CollectionNotificationEmail"; -const log = debug('emails'); +const log = debug("emails"); -type Emails = 'welcome' | 'export'; +type Emails = "welcome" | "export"; type SendMailType = { to: string, @@ -67,7 +67,7 @@ export class Mailer { if (transporter) { const html = Oy.renderTemplate(data.html, { title: data.title, - headCSS: [baseStyles, data.headCSS].join(' '), + headCSS: [baseStyles, data.headCSS].join(" "), previewText: data.previewText, }); @@ -94,9 +94,9 @@ export class Mailer { welcome = async (opts: { to: string, teamUrl: string }) => { this.sendMail({ to: opts.to, - title: 'Welcome to Outline', + title: "Welcome to Outline", previewText: - 'Outline is a place for your team to build and share knowledge.', + "Outline is a place for your team to build and share knowledge.", html: , text: welcomeEmailText(opts), }); @@ -106,7 +106,7 @@ export class Mailer { this.sendMail({ to: opts.to, attachments: opts.attachments, - title: 'Your requested export', + title: "Your requested export", previewText: "Here's your request data export from Outline", html: , text: exportEmailText, @@ -120,7 +120,7 @@ export class Mailer { opts.teamName }’s knowledgebase`, previewText: - 'Outline is a place for your team to build and share knowledge.', + "Outline is a place for your team to build and share knowledge.", html: , text: inviteEmailText(opts), }); @@ -129,8 +129,8 @@ export class Mailer { signin = async (opts: { to: string, token: string, teamUrl: string }) => { this.sendMail({ to: opts.to, - title: 'Magic signin link', - previewText: 'Here’s your link to signin to Outline.', + title: "Magic signin link", + previewText: "Here’s your link to signin to Outline.", html: , text: signinEmailText(opts), }); @@ -165,7 +165,7 @@ export class Mailer { let smtpConfig = { host: process.env.SMTP_HOST, port: process.env.SMTP_PORT, - secure: process.env.NODE_ENV === 'production', + secure: process.env.NODE_ENV === "production", auth: undefined, }; @@ -184,7 +184,7 @@ export class Mailer { const mailer = new Mailer(); export default mailer; -export const mailerQueue = createQueue('email'); +export const mailerQueue = createQueue("email"); mailerQueue.process(async (job: EmailJob) => { // $FlowIssue flow doesn't like dynamic values @@ -204,7 +204,7 @@ export const sendEmail = (type: Emails, to: string, options?: Object = {}) => { attempts: 5, removeOnComplete: true, backoff: { - type: 'exponential', + type: "exponential", delay: 60 * 1000, }, } diff --git a/server/mailer.test.js b/server/mailer.test.js index fc0d4945..31c00b40 100644 --- a/server/mailer.test.js +++ b/server/mailer.test.js @@ -1,13 +1,13 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import mailer from './mailer'; +import mailer from "./mailer"; -describe('Mailer', () => { +describe("Mailer", () => { let fakeMailer = mailer; let sendMailOutput; beforeEach(() => { - process.env.URL = 'http://localhost:3000'; - process.env.SMTP_FROM_EMAIL = 'hello@example.com'; + process.env.URL = "http://localhost:3000"; + process.env.SMTP_FROM_EMAIL = "hello@example.com"; jest.resetModules(); fakeMailer.transporter = { @@ -15,10 +15,10 @@ describe('Mailer', () => { }; }); - test('#welcome', () => { + test("#welcome", () => { fakeMailer.welcome({ - to: 'user@example.com', - teamUrl: 'http://example.com', + to: "user@example.com", + teamUrl: "http://example.com", }); expect(sendMailOutput).toMatchSnapshot(); }); diff --git a/server/middlewares/apexRedirect.js b/server/middlewares/apexRedirect.js index 61651844..992837ad 100644 --- a/server/middlewares/apexRedirect.js +++ b/server/middlewares/apexRedirect.js @@ -1,12 +1,12 @@ // @flow -import type { Context } from 'koa'; +import type { Context } from "koa"; export default function apexRedirect() { return async function apexRedirectMiddleware( ctx: Context, next: () => Promise<*> ) { - if (ctx.headers.host === 'getoutline.com') { + if (ctx.headers.host === "getoutline.com") { ctx.redirect(`https://www.${ctx.headers.host}${ctx.path}`); } else { return next(); diff --git a/server/middlewares/authentication.js b/server/middlewares/authentication.js index cbc7cca6..12a81641 100644 --- a/server/middlewares/authentication.js +++ b/server/middlewares/authentication.js @@ -1,20 +1,20 @@ // @flow -import JWT from 'jsonwebtoken'; -import { type Context } from 'koa'; -import { User, ApiKey } from '../models'; -import { getUserForJWT } from '../utils/jwt'; -import { AuthenticationError, UserSuspendedError } from '../errors'; -import addMonths from 'date-fns/add_months'; -import addMinutes from 'date-fns/add_minutes'; -import { getCookieDomain } from '../../shared/utils/domains'; +import JWT from "jsonwebtoken"; +import { type Context } from "koa"; +import { User, ApiKey } from "../models"; +import { getUserForJWT } from "../utils/jwt"; +import { AuthenticationError, UserSuspendedError } from "../errors"; +import addMonths from "date-fns/add_months"; +import addMinutes from "date-fns/add_minutes"; +import { getCookieDomain } from "../../shared/utils/domains"; export default function auth(options?: { required?: boolean } = {}) { return async function authMiddleware(ctx: Context, next: () => Promise<*>) { let token; - const authorizationHeader = ctx.request.get('authorization'); + const authorizationHeader = ctx.request.get("authorization"); if (authorizationHeader) { - const parts = authorizationHeader.split(' '); + const parts = authorizationHeader.split(" "); if (parts.length === 2) { const scheme = parts[0]; const credentials = parts[1]; @@ -33,11 +33,11 @@ export default function auth(options?: { required?: boolean } = {}) { } else if (ctx.request.query.token) { token = ctx.request.query.token; } else { - token = ctx.cookies.get('accessToken'); + token = ctx.cookies.get("accessToken"); } if (!token && options.required !== false) { - throw new AuthenticationError('Authentication required'); + throw new AuthenticationError("Authentication required"); } let user; @@ -52,13 +52,13 @@ export default function auth(options?: { required?: boolean } = {}) { }, }); } catch (e) { - throw new AuthenticationError('Invalid API key'); + throw new AuthenticationError("Invalid API key"); } - if (!apiKey) throw new AuthenticationError('Invalid API key'); + if (!apiKey) throw new AuthenticationError("Invalid API key"); user = await User.findByPk(apiKey.userId); - if (!user) throw new AuthenticationError('Invalid API key'); + if (!user) throw new AuthenticationError("Invalid API key"); } else { // JWT user = await getUserForJWT(token); @@ -83,7 +83,7 @@ export default function auth(options?: { required?: boolean } = {}) { ctx.signIn = async (user, team, service, isFirstSignin = false) => { if (user.isSuspended) { - return ctx.redirect('/?notice=suspended'); + return ctx.redirect("/?notice=suspended"); } // update the database when the user last signed in @@ -94,18 +94,18 @@ export default function auth(options?: { required?: boolean } = {}) { // set a cookie for which service we last signed in with. This is // only used to display a UI hint for the user for next time - ctx.cookies.set('lastSignedIn', service, { + ctx.cookies.set("lastSignedIn", service, { httpOnly: false, - expires: new Date('2100'), + expires: new Date("2100"), domain, }); // set a transfer cookie for the access token itself and redirect // to the teams subdomain if subdomains are enabled - if (process.env.SUBDOMAINS_ENABLED === 'true' && team.subdomain) { + if (process.env.SUBDOMAINS_ENABLED === "true" && team.subdomain) { // get any existing sessions (teams signed in) and add this team const existing = JSON.parse( - decodeURIComponent(ctx.cookies.get('sessions') || '') || '{}' + decodeURIComponent(ctx.cookies.get("sessions") || "") || "{}" ); const sessions = encodeURIComponent( JSON.stringify({ @@ -117,24 +117,24 @@ export default function auth(options?: { required?: boolean } = {}) { }, }) ); - ctx.cookies.set('sessions', sessions, { + ctx.cookies.set("sessions", sessions, { httpOnly: false, expires, domain, }); - ctx.cookies.set('accessToken', user.getJwtToken(), { + ctx.cookies.set("accessToken", user.getJwtToken(), { httpOnly: true, expires: addMinutes(new Date(), 1), domain, }); ctx.redirect(`${team.url}/auth/redirect`); } else { - ctx.cookies.set('accessToken', user.getJwtToken(), { + ctx.cookies.set("accessToken", user.getJwtToken(), { httpOnly: false, expires, }); - ctx.redirect(`${team.url}/home${isFirstSignin ? '?welcome' : ''}`); + ctx.redirect(`${team.url}/home${isFirstSignin ? "?welcome" : ""}`); } }; diff --git a/server/middlewares/authentication.test.js b/server/middlewares/authentication.test.js index 751b5523..c01e8bf2 100644 --- a/server/middlewares/authentication.test.js +++ b/server/middlewares/authentication.test.js @@ -1,15 +1,15 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import { flushdb, seed } from '../test/support'; -import { buildUser } from '../test/factories'; -import { ApiKey } from '../models'; -import randomstring from 'randomstring'; -import auth from './authentication'; +import { flushdb, seed } from "../test/support"; +import { buildUser } from "../test/factories"; +import { ApiKey } from "../models"; +import randomstring from "randomstring"; +import auth from "./authentication"; beforeEach(flushdb); -describe('Authentication middleware', async () => { - describe('with JWT', () => { - it('should authenticate with correct token', async () => { +describe("Authentication middleware", async () => { + describe("with JWT", () => { + it("should authenticate with correct token", async () => { const state = {}; const { user } = await seed(); const authMiddleware = auth(); @@ -27,7 +27,7 @@ describe('Authentication middleware', async () => { expect(state.user.id).toEqual(user.id); }); - it('should return error with invalid token', async () => { + it("should return error with invalid token", async () => { const state = {}; const { user } = await seed(); const authMiddleware = auth(); @@ -44,13 +44,13 @@ describe('Authentication middleware', async () => { jest.fn() ); } catch (e) { - expect(e.message).toBe('Invalid token'); + expect(e.message).toBe("Invalid token"); } }); }); - describe('with API key', () => { - it('should authenticate user with valid API key', async () => { + describe("with API key", () => { + it("should authenticate user with valid API key", async () => { const state = {}; const { user } = await seed(); const authMiddleware = auth(); @@ -71,7 +71,7 @@ describe('Authentication middleware', async () => { expect(state.user.id).toEqual(user.id); }); - it('should return error with invalid API key', async () => { + it("should return error with invalid API key", async () => { const state = {}; const authMiddleware = auth(); @@ -87,12 +87,12 @@ describe('Authentication middleware', async () => { jest.fn() ); } catch (e) { - expect(e.message).toBe('Invalid API key'); + expect(e.message).toBe("Invalid API key"); } }); }); - it('should return error message if no auth token is available', async () => { + it("should return error message if no auth token is available", async () => { const state = {}; const authMiddleware = auth(); @@ -100,7 +100,7 @@ describe('Authentication middleware', async () => { await authMiddleware( { request: { - get: jest.fn(() => 'error'), + get: jest.fn(() => "error"), }, state, cache: {}, @@ -114,7 +114,7 @@ describe('Authentication middleware', async () => { } }); - it('should allow passing auth token as a GET param', async () => { + it("should allow passing auth token as a GET param", async () => { const state = {}; const { user } = await seed(); const authMiddleware = auth(); @@ -136,7 +136,7 @@ describe('Authentication middleware', async () => { expect(state.user.id).toEqual(user.id); }); - it('should allow passing auth token in body params', async () => { + it("should allow passing auth token in body params", async () => { const state = {}; const { user } = await seed(); const authMiddleware = auth(); @@ -157,7 +157,7 @@ describe('Authentication middleware', async () => { expect(state.user.id).toEqual(user.id); }); - it('should return an error for suspended users', async () => { + it("should return an error for suspended users", async () => { const state = {}; const admin = await buildUser({}); const user = await buildUser({ @@ -179,7 +179,7 @@ describe('Authentication middleware', async () => { ); } catch (e) { expect(e.message).toEqual( - 'Your access has been suspended by the team admin' + "Your access has been suspended by the team admin" ); expect(e.errorData.adminEmail).toEqual(admin.email); } diff --git a/server/middlewares/errorHandling.js b/server/middlewares/errorHandling.js index 9c5100b5..904fece5 100644 --- a/server/middlewares/errorHandling.js +++ b/server/middlewares/errorHandling.js @@ -1,7 +1,7 @@ // @flow -import Sequelize from 'sequelize'; -import { snakeCase } from 'lodash'; -import { type Context } from 'koa'; +import Sequelize from "sequelize"; +import { snakeCase } from "lodash"; +import { type Context } from "koa"; export default function errorHandling() { return async function errorHandlingMiddleware( @@ -25,18 +25,18 @@ export default function errorHandling() { if (message.match(/Not found/i)) { ctx.status = 404; - error = 'not_found'; + error = "not_found"; } if (message.match(/Authorization error/i)) { ctx.status = 403; - error = 'authorization_error'; + error = "authorization_error"; } if (ctx.status === 500) { - message = 'Internal Server Error'; - error = 'internal_server_error'; - ctx.app.emit('error', err, ctx); + message = "Internal Server Error"; + error = "internal_server_error"; + ctx.app.emit("error", err, ctx); } ctx.body = { diff --git a/server/middlewares/methodOverride.js b/server/middlewares/methodOverride.js index 7872dfc1..fed29e57 100644 --- a/server/middlewares/methodOverride.js +++ b/server/middlewares/methodOverride.js @@ -1,16 +1,16 @@ // @flow -import queryString from 'query-string'; -import { type Context } from 'koa'; +import queryString from "query-string"; +import { type Context } from "koa"; export default function methodOverride() { return async function methodOverrideMiddleware( ctx: Context, next: () => Promise<*> ) { - if (ctx.method === 'POST') { + if (ctx.method === "POST") { // $FlowFixMe ctx.body = ctx.request.body; - } else if (ctx.method === 'GET') { + } else if (ctx.method === "GET") { ctx.method = 'POST'; // eslint-disable-line ctx.body = queryString.parse(ctx.querystring); } diff --git a/server/middlewares/validation.js b/server/middlewares/validation.js index ae918250..d68f8025 100644 --- a/server/middlewares/validation.js +++ b/server/middlewares/validation.js @@ -1,13 +1,13 @@ // @flow -import validator from 'validator'; -import { type Context } from 'koa'; -import { ParamRequiredError, ValidationError } from '../errors'; -import { validateColorHex } from '../../shared/utils/color'; +import validator from "validator"; +import { type Context } from "koa"; +import { ParamRequiredError, ValidationError } from "../errors"; +import { validateColorHex } from "../../shared/utils/color"; export default function validation() { return function validationMiddleware(ctx: Context, next: () => Promise<*>) { ctx.assertPresent = (value, message) => { - if (value === undefined || value === null || value === '') { + if (value === undefined || value === null || value === "") { throw new ParamRequiredError(message); } }; @@ -19,18 +19,18 @@ export default function validation() { }; ctx.assertNotEmpty = (value, message) => { - if (value === '') { + if (value === "") { throw new ValidationError(message); } }; - ctx.assertEmail = (value = '', message) => { + ctx.assertEmail = (value = "", message) => { if (!validator.isEmail(value)) { throw new ValidationError(message); } }; - ctx.assertUuid = (value = '', message) => { + ctx.assertUuid = (value = "", message) => { if (!validator.isUUID(value)) { throw new ValidationError(message); } diff --git a/server/models/ApiKey.js b/server/models/ApiKey.js index f0c0580b..0c89a119 100644 --- a/server/models/ApiKey.js +++ b/server/models/ApiKey.js @@ -1,9 +1,9 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; -import randomstring from 'randomstring'; +import { DataTypes, sequelize } from "../sequelize"; +import randomstring from "randomstring"; const ApiKey = sequelize.define( - 'apiKeys', + "apiKeys", { id: { type: DataTypes.UUID, @@ -17,12 +17,12 @@ const ApiKey = sequelize.define( type: DataTypes.UUID, allowNull: false, references: { - model: 'users', + model: "users", }, }, }, { - tableName: 'apiKeys', + tableName: "apiKeys", paranoid: true, hooks: { beforeValidate: key => { @@ -34,8 +34,8 @@ const ApiKey = sequelize.define( ApiKey.associate = models => { ApiKey.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", }); }; diff --git a/server/models/Attachment.js b/server/models/Attachment.js index 7e14987a..54ff666c 100644 --- a/server/models/Attachment.js +++ b/server/models/Attachment.js @@ -1,10 +1,10 @@ // @flow -import path from 'path'; -import { DataTypes, sequelize } from '../sequelize'; -import { deleteFromS3 } from '../utils/s3'; +import path from "path"; +import { DataTypes, sequelize } from "../sequelize"; +import { deleteFromS3 } from "../utils/s3"; const Attachment = sequelize.define( - 'attachment', + "attachment", { id: { type: DataTypes.UUID, @@ -30,9 +30,9 @@ const Attachment = sequelize.define( acl: { type: DataTypes.STRING, allowNull: false, - defaultValue: 'public-read', + defaultValue: "public-read", validate: { - isIn: [['private', 'public-read']], + isIn: [["private", "public-read"]], }, }, }, @@ -45,7 +45,7 @@ const Attachment = sequelize.define( return `/api/attachments.redirect?id=${this.id}`; }, isPrivate: function() { - return this.acl === 'private'; + return this.acl === "private"; }, }, } diff --git a/server/models/Authentication.js b/server/models/Authentication.js index e4a479dd..d9d48e52 100644 --- a/server/models/Authentication.js +++ b/server/models/Authentication.js @@ -1,7 +1,7 @@ // @flow -import { DataTypes, sequelize, encryptedFields } from '../sequelize'; +import { DataTypes, sequelize, encryptedFields } from "../sequelize"; -const Authentication = sequelize.define('authentication', { +const Authentication = sequelize.define("authentication", { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, @@ -9,17 +9,17 @@ const Authentication = sequelize.define('authentication', { }, service: DataTypes.STRING, scopes: DataTypes.ARRAY(DataTypes.STRING), - token: encryptedFields.vault('token'), + token: encryptedFields.vault("token"), }); Authentication.associate = models => { Authentication.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", }); Authentication.belongsTo(models.Team, { - as: 'team', - foreignKey: 'teamId', + as: "team", + foreignKey: "teamId", }); }; diff --git a/server/models/Backlink.js b/server/models/Backlink.js index dfc60077..6188a78a 100644 --- a/server/models/Backlink.js +++ b/server/models/Backlink.js @@ -1,7 +1,7 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; +import { DataTypes, sequelize } from "../sequelize"; -const Backlink = sequelize.define('backlink', { +const Backlink = sequelize.define("backlink", { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, @@ -11,16 +11,16 @@ const Backlink = sequelize.define('backlink', { Backlink.associate = models => { Backlink.belongsTo(models.Document, { - as: 'document', - foreignKey: 'documentId', + as: "document", + foreignKey: "documentId", }); Backlink.belongsTo(models.Document, { - as: 'reverseDocument', - foreignKey: 'reverseDocumentId', + as: "reverseDocument", + foreignKey: "reverseDocumentId", }); Backlink.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", }); }; diff --git a/server/models/Collection.js b/server/models/Collection.js index 2b25b1de..7830bd2c 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -1,15 +1,15 @@ // @flow -import { find, concat, remove, uniq } from 'lodash'; -import slug from 'slug'; -import randomstring from 'randomstring'; -import { DataTypes, sequelize } from '../sequelize'; -import Document from './Document'; -import CollectionUser from './CollectionUser'; +import { find, concat, remove, uniq } from "lodash"; +import slug from "slug"; +import randomstring from "randomstring"; +import { DataTypes, sequelize } from "../sequelize"; +import Document from "./Document"; +import CollectionUser from "./CollectionUser"; -slug.defaults.mode = 'rfc3986'; +slug.defaults.mode = "rfc3986"; const Collection = sequelize.define( - 'collection', + "collection", { id: { type: DataTypes.UUID, @@ -25,14 +25,14 @@ const Collection = sequelize.define( maintainerApprovalRequired: DataTypes.BOOLEAN, type: { type: DataTypes.STRING, - validate: { isIn: [['atlas', 'journal']] }, + validate: { isIn: [["atlas", "journal"]] }, }, /* type: atlas */ documentStructure: DataTypes.JSONB, }, { - tableName: 'collections', + tableName: "collections", paranoid: true, hooks: { beforeValidate: (collection: Collection) => { @@ -47,8 +47,8 @@ const Collection = sequelize.define( } ); -Collection.addHook('beforeSave', async model => { - if (model.icon === 'collection') { +Collection.addHook("beforeSave", async model => { + if (model.icon === "collection") { model.icon = null; } }); @@ -57,48 +57,48 @@ Collection.addHook('beforeSave', async model => { Collection.associate = models => { Collection.hasMany(models.Document, { - as: 'documents', - foreignKey: 'collectionId', - onDelete: 'cascade', + as: "documents", + foreignKey: "collectionId", + onDelete: "cascade", }); Collection.hasMany(models.CollectionUser, { - as: 'memberships', - foreignKey: 'collectionId', - onDelete: 'cascade', + as: "memberships", + foreignKey: "collectionId", + onDelete: "cascade", }); Collection.hasMany(models.CollectionGroup, { - as: 'collectionGroupMemberships', - foreignKey: 'collectionId', - onDelete: 'cascade', + as: "collectionGroupMemberships", + foreignKey: "collectionId", + onDelete: "cascade", }); Collection.belongsToMany(models.User, { - as: 'users', + as: "users", through: models.CollectionUser, - foreignKey: 'collectionId', + foreignKey: "collectionId", }); Collection.belongsToMany(models.Group, { - as: 'groups', + as: "groups", through: models.CollectionGroup, - foreignKey: 'collectionId', + foreignKey: "collectionId", }); Collection.belongsTo(models.User, { - as: 'user', - foreignKey: 'creatorId', + as: "user", + foreignKey: "creatorId", }); Collection.belongsTo(models.Team, { - as: 'team', + as: "team", }); - Collection.addScope('withMembership', userId => ({ + Collection.addScope("withMembership", userId => ({ include: [ { model: models.CollectionUser, - as: 'memberships', + as: "memberships", where: { userId }, required: false, }, { model: models.CollectionGroup, - as: 'collectionGroupMemberships', + as: "collectionGroupMemberships", required: false, // use of "separate" property: sequelize breaks when there are @@ -111,11 +111,11 @@ Collection.associate = models => { // CollectionGroup [inner join] Group [inner join] GroupUser [where] userId include: { model: models.Group, - as: 'group', + as: "group", required: true, include: { model: models.GroupUser, - as: 'groupMemberships', + as: "groupMemberships", required: true, where: { userId }, }, @@ -123,16 +123,16 @@ Collection.associate = models => { }, ], })); - Collection.addScope('withAllMemberships', { + Collection.addScope("withAllMemberships", { include: [ { model: models.CollectionUser, - as: 'memberships', + as: "memberships", required: false, }, { model: models.CollectionGroup, - as: 'collectionGroupMemberships', + as: "collectionGroupMemberships", required: false, // use of "separate" property: sequelize breaks when there are @@ -145,11 +145,11 @@ Collection.associate = models => { // CollectionGroup [inner join] Group [inner join] GroupUser [where] userId include: { model: models.Group, - as: 'group', + as: "group", required: true, include: { model: models.GroupUser, - as: 'groupMemberships', + as: "groupMemberships", required: true, }, }, @@ -158,7 +158,7 @@ Collection.associate = models => { }); }; -Collection.addHook('afterDestroy', async (model: Collection) => { +Collection.addHook("afterDestroy", async (model: Collection) => { await Document.destroy({ where: { collectionId: model.id, @@ -166,7 +166,7 @@ Collection.addHook('afterDestroy', async (model: Collection) => { }); }); -Collection.addHook('afterCreate', (model: Collection, options) => { +Collection.addHook("afterCreate", (model: Collection, options) => { if (model.private) { return CollectionUser.findOrCreate({ where: { @@ -174,7 +174,7 @@ Collection.addHook('afterCreate', (model: Collection, options) => { userId: model.creatorId, }, defaults: { - permission: 'read_write', + permission: "read_write", createdById: model.creatorId, }, transaction: options.transaction, @@ -186,7 +186,7 @@ Collection.addHook('afterCreate', (model: Collection, options) => { // get all the membership relationshps a user could have with the collection Collection.membershipUserIds = async (collectionId: string) => { - const collection = await Collection.scope('withAllMemberships').findByPk( + const collection = await Collection.scope("withAllMemberships").findByPk( collectionId ); diff --git a/server/models/Collection.test.js b/server/models/Collection.test.js index 23a68758..f1c30fcb 100644 --- a/server/models/Collection.test.js +++ b/server/models/Collection.test.js @@ -1,31 +1,31 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import { flushdb, seed } from '../test/support'; -import { Collection, Document } from '../models'; +import { flushdb, seed } from "../test/support"; +import { Collection, Document } from "../models"; import { buildUser, buildGroup, buildCollection, buildTeam, -} from '../test/factories'; -import uuid from 'uuid'; +} from "../test/factories"; +import uuid from "uuid"; beforeEach(flushdb); beforeEach(jest.resetAllMocks); -describe('#url', () => { - test('should return correct url for the collection', () => { - const collection = new Collection({ id: '1234' }); - expect(collection.url).toBe('/collections/1234'); +describe("#url", () => { + test("should return correct url for the collection", () => { + const collection = new Collection({ id: "1234" }); + expect(collection.url).toBe("/collections/1234"); }); }); -describe('#addDocumentToStructure', async () => { - test('should add as last element without index', async () => { +describe("#addDocumentToStructure", async () => { + test("should add as last element without index", async () => { const { collection } = await seed(); const id = uuid.v4(); const newDocument = new Document({ id, - title: 'New end node', + title: "New end node", parentDocumentId: null, }); @@ -34,12 +34,12 @@ describe('#addDocumentToStructure', async () => { expect(collection.documentStructure[1].id).toBe(id); }); - test('should add with an index', async () => { + test("should add with an index", async () => { const { collection } = await seed(); const id = uuid.v4(); const newDocument = new Document({ id, - title: 'New end node', + title: "New end node", parentDocumentId: null, }); @@ -48,12 +48,12 @@ describe('#addDocumentToStructure', async () => { expect(collection.documentStructure[1].id).toBe(id); }); - test('should add as a child if with parent', async () => { + test("should add as a child if with parent", async () => { const { collection, document } = await seed(); const id = uuid.v4(); const newDocument = new Document({ id, - title: 'New end node', + title: "New end node", parentDocumentId: document.id, }); @@ -64,17 +64,17 @@ describe('#addDocumentToStructure', async () => { expect(collection.documentStructure[0].children[0].id).toBe(id); }); - test('should add as a child if with parent with index', async () => { + test("should add as a child if with parent with index", async () => { const { collection, document } = await seed(); const newDocument = new Document({ id: uuid.v4(), - title: 'node', + title: "node", parentDocumentId: document.id, }); const id = uuid.v4(); const secondDocument = new Document({ id, - title: 'New start node', + title: "New start node", parentDocumentId: document.id, }); @@ -86,13 +86,13 @@ describe('#addDocumentToStructure', async () => { expect(collection.documentStructure[0].children[0].id).toBe(id); }); - describe('options: documentJson', async () => { + describe("options: documentJson", async () => { test("should append supplied json over document's own", async () => { const { collection } = await seed(); const id = uuid.v4(); const newDocument = new Document({ id: uuid.v4(), - title: 'New end node', + title: "New end node", parentDocumentId: null, }); @@ -101,7 +101,7 @@ describe('#addDocumentToStructure', async () => { children: [ { id, - title: 'Totally fake', + title: "Totally fake", children: [], }, ], @@ -113,16 +113,16 @@ describe('#addDocumentToStructure', async () => { }); }); -describe('#updateDocument', () => { +describe("#updateDocument", () => { test("should update root document's data", async () => { const { collection, document } = await seed(); - document.title = 'Updated title'; + document.title = "Updated title"; await document.save(); await collection.updateDocument(document); - expect(collection.documentStructure[0].title).toBe('Updated title'); + expect(collection.documentStructure[0].title).toBe("Updated title"); }); test("should update child document's data", async () => { @@ -134,32 +134,32 @@ describe('#updateDocument', () => { userId: collection.creatorId, lastModifiedById: collection.creatorId, createdById: collection.creatorId, - title: 'Child document', - text: 'content', + title: "Child document", + text: "content", }); await collection.addDocumentToStructure(newDocument); - newDocument.title = 'Updated title'; + newDocument.title = "Updated title"; await newDocument.save(); await collection.updateDocument(newDocument); expect(collection.documentStructure[0].children[0].title).toBe( - 'Updated title' + "Updated title" ); }); }); -describe('#removeDocument', () => { - test('should save if removing', async () => { +describe("#removeDocument", () => { + test("should save if removing", async () => { const { collection, document } = await seed(); - jest.spyOn(collection, 'save'); + jest.spyOn(collection, "save"); await collection.deleteDocument(document); expect(collection.save).toBeCalled(); }); - test('should remove documents from root', async () => { + test("should remove documents from root", async () => { const { collection, document } = await seed(); await collection.deleteDocument(document); @@ -174,7 +174,7 @@ describe('#removeDocument', () => { expect(collectionDocuments.count).toBe(0); }); - test('should remove a document with child documents', async () => { + test("should remove a document with child documents", async () => { const { collection, document } = await seed(); // Add a child for testing @@ -185,8 +185,8 @@ describe('#removeDocument', () => { userId: collection.creatorId, lastModifiedById: collection.creatorId, createdById: collection.creatorId, - title: 'Child document', - text: 'content', + title: "Child document", + text: "content", }); await collection.addDocumentToStructure(newDocument); expect(collection.documentStructure[0].children.length).toBe(1); @@ -202,7 +202,7 @@ describe('#removeDocument', () => { expect(collectionDocuments.count).toBe(0); }); - test('should remove a child document', async () => { + test("should remove a child document", async () => { const { collection, document } = await seed(); // Add a child for testing @@ -214,8 +214,8 @@ describe('#removeDocument', () => { lastModifiedById: collection.creatorId, createdById: collection.creatorId, publishedAt: new Date(), - title: 'Child document', - text: 'content', + title: "Child document", + text: "content", }); await collection.addDocumentToStructure(newDocument); expect(collection.documentStructure.length).toBe(1); @@ -236,8 +236,8 @@ describe('#removeDocument', () => { }); }); -describe('#membershipUserIds', () => { - test('should return collection and group memberships', async () => { +describe("#membershipUserIds", () => { + test("should return collection and group memberships", async () => { const team = await buildTeam(); const teamId = team.id; diff --git a/server/models/CollectionGroup.js b/server/models/CollectionGroup.js index 5988fc97..4a3ca253 100644 --- a/server/models/CollectionGroup.js +++ b/server/models/CollectionGroup.js @@ -1,14 +1,14 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; +import { DataTypes, sequelize } from "../sequelize"; const CollectionGroup = sequelize.define( - 'collection_group', + "collection_group", { permission: { type: DataTypes.STRING, - defaultValue: 'read_write', + defaultValue: "read_write", validate: { - isIn: [['read', 'read_write', 'maintainer']], + isIn: [["read", "read_write", "maintainer"]], }, }, }, @@ -20,18 +20,18 @@ const CollectionGroup = sequelize.define( CollectionGroup.associate = models => { CollectionGroup.belongsTo(models.Collection, { - as: 'collection', - foreignKey: 'collectionId', + as: "collection", + foreignKey: "collectionId", primary: true, }); CollectionGroup.belongsTo(models.Group, { - as: 'group', - foreignKey: 'groupId', + as: "group", + foreignKey: "groupId", primary: true, }); CollectionGroup.belongsTo(models.User, { - as: 'createdBy', - foreignKey: 'createdById', + as: "createdBy", + foreignKey: "createdById", }); }; diff --git a/server/models/CollectionUser.js b/server/models/CollectionUser.js index b0085d12..73cd448d 100644 --- a/server/models/CollectionUser.js +++ b/server/models/CollectionUser.js @@ -1,14 +1,14 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; +import { DataTypes, sequelize } from "../sequelize"; const CollectionUser = sequelize.define( - 'collection_user', + "collection_user", { permission: { type: DataTypes.STRING, - defaultValue: 'read_write', + defaultValue: "read_write", validate: { - isIn: [['read', 'read_write', 'maintainer']], + isIn: [["read", "read_write", "maintainer"]], }, }, }, @@ -19,16 +19,16 @@ const CollectionUser = sequelize.define( CollectionUser.associate = models => { CollectionUser.belongsTo(models.Collection, { - as: 'collection', - foreignKey: 'collectionId', + as: "collection", + foreignKey: "collectionId", }); CollectionUser.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", }); CollectionUser.belongsTo(models.User, { - as: 'createdBy', - foreignKey: 'createdById', + as: "createdBy", + foreignKey: "createdById", }); }; diff --git a/server/models/Document.js b/server/models/Document.js index 256dcbad..7dcee840 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -1,17 +1,17 @@ // @flow -import { map, find, compact, uniq } from 'lodash'; -import MarkdownSerializer from 'slate-md-serializer'; -import randomstring from 'randomstring'; -import Sequelize, { type Transaction } from 'sequelize'; -import removeMarkdown from '@tommoor/remove-markdown'; +import { map, find, compact, uniq } from "lodash"; +import MarkdownSerializer from "slate-md-serializer"; +import randomstring from "randomstring"; +import Sequelize, { type Transaction } from "sequelize"; +import removeMarkdown from "@tommoor/remove-markdown"; -import isUUID from 'validator/lib/isUUID'; -import { Collection, User } from '../models'; -import { DataTypes, sequelize } from '../sequelize'; -import parseTitle from '../../shared/utils/parseTitle'; -import unescape from '../../shared/utils/unescape'; -import slugify from '../utils/slugify'; -import Revision from './Revision'; +import isUUID from "validator/lib/isUUID"; +import { Collection, User } from "../models"; +import { DataTypes, sequelize } from "../sequelize"; +import parseTitle from "../../shared/utils/parseTitle"; +import unescape from "../../shared/utils/unescape"; +import slugify from "../utils/slugify"; +import Revision from "./Revision"; const Op = Sequelize.Op; const URL_REGEX = /^[0-9a-zA-Z-_~]*-([a-zA-Z0-9]{10,15})$/; @@ -25,8 +25,8 @@ const createRevision = (doc, options = {}) => { // we don't create revisions if identical to previous if ( - doc.text === doc.previous('text') && - doc.title === doc.previous('title') + doc.text === doc.previous("text") && + doc.title === doc.previous("title") ) { return; } @@ -64,7 +64,7 @@ const beforeSave = async doc => { doc.emoji = emoji; // ensure documents have a title - doc.title = doc.title || ''; + doc.title = doc.title || ""; // add the current user as a collaborator on this doc if (!doc.collaboratorIds) doc.collaboratorIds = []; @@ -77,7 +77,7 @@ const beforeSave = async doc => { }; const Document = sequelize.define( - 'document', + "document", { id: { type: DataTypes.UUID, @@ -93,7 +93,7 @@ const Document = sequelize.define( validate: { len: { args: [0, 100], - msg: 'Document title must be less than 100 characters', + msg: "Document title must be less than 100 characters", }, }, }, @@ -134,45 +134,45 @@ const Document = sequelize.define( Document.associate = models => { Document.belongsTo(models.Collection, { - as: 'collection', - foreignKey: 'collectionId', - onDelete: 'cascade', + as: "collection", + foreignKey: "collectionId", + onDelete: "cascade", }); Document.belongsTo(models.Team, { - as: 'team', - foreignKey: 'teamId', + as: "team", + foreignKey: "teamId", }); Document.belongsTo(models.User, { - as: 'createdBy', - foreignKey: 'createdById', + as: "createdBy", + foreignKey: "createdById", }); Document.belongsTo(models.User, { - as: 'updatedBy', - foreignKey: 'lastModifiedById', + as: "updatedBy", + foreignKey: "lastModifiedById", }); Document.belongsTo(models.User, { - as: 'pinnedBy', - foreignKey: 'pinnedById', + as: "pinnedBy", + foreignKey: "pinnedById", }); Document.hasMany(models.Revision, { - as: 'revisions', - onDelete: 'cascade', + as: "revisions", + onDelete: "cascade", }); Document.hasMany(models.Backlink, { - as: 'backlinks', - onDelete: 'cascade', + as: "backlinks", + onDelete: "cascade", }); Document.hasMany(models.Star, { - as: 'starred', - onDelete: 'cascade', + as: "starred", + onDelete: "cascade", }); Document.hasMany(models.View, { - as: 'views', + as: "views", }); - Document.addScope('defaultScope', { + Document.addScope("defaultScope", { include: [ - { model: models.User, as: 'createdBy', paranoid: false }, - { model: models.User, as: 'updatedBy', paranoid: false }, + { model: models.User, as: "createdBy", paranoid: false }, + { model: models.User, as: "updatedBy", paranoid: false }, ], where: { publishedAt: { @@ -180,38 +180,38 @@ Document.associate = models => { }, }, }); - Document.addScope('withCollection', userId => { + Document.addScope("withCollection", userId => { if (userId) { return { include: [ { model: models.Collection.scope({ - method: ['withMembership', userId], + method: ["withMembership", userId], }), - as: 'collection', + as: "collection", }, ], }; } return { - include: [{ model: models.Collection, as: 'collection' }], + include: [{ model: models.Collection, as: "collection" }], }; }); - Document.addScope('withUnpublished', { + Document.addScope("withUnpublished", { include: [ - { model: models.User, as: 'createdBy', paranoid: false }, - { model: models.User, as: 'updatedBy', paranoid: false }, + { model: models.User, as: "createdBy", paranoid: false }, + { model: models.User, as: "updatedBy", paranoid: false }, ], }); - Document.addScope('withViews', userId => ({ + Document.addScope("withViews", userId => ({ include: [ - { model: models.View, as: 'views', where: { userId }, required: false }, + { model: models.View, as: "views", where: { userId }, required: false }, ], })); - Document.addScope('withStarred', userId => ({ + Document.addScope("withStarred", userId => ({ include: [ - { model: models.Star, as: 'starred', where: { userId }, required: false }, + { model: models.Star, as: "starred", where: { userId }, required: false }, ], })); }; @@ -219,8 +219,8 @@ Document.associate = models => { Document.findByPk = async function(id, options = {}) { // allow default preloading of collection membership if `userId` is passed in find options // almost every endpoint needs the collection membership to determine policy permissions. - const scope = this.scope('withUnpublished', { - method: ['withCollection', options.userId], + const scope = this.scope("withUnpublished", { + method: ["withCollection", options.userId], }); if (isUUID(id)) { @@ -248,7 +248,7 @@ type SearchOptions = { limit?: number, offset?: number, collectionId?: string, - dateFilter?: 'day' | 'week' | 'month' | 'year', + dateFilter?: "day" | "week" | "month" | "year", collaboratorIds?: string[], includeArchived?: boolean, includeDrafts?: boolean, @@ -257,7 +257,7 @@ type SearchOptions = { function escape(query: string): string { // replace "\" with escaped "\\" because sequelize.escape doesn't do it // https://github.com/sequelize/sequelize/issues/2950 - return sequelize.escape(query).replace('\\', '\\\\'); + return sequelize.escape(query).replace("\\", "\\\\"); } Document.searchForTeam = async ( @@ -308,12 +308,12 @@ Document.searchForTeam = async ( // Final query to get associated document data const documents = await Document.findAll({ where: { - id: map(results, 'id'), + id: map(results, "id"), }, include: [ - { model: Collection, as: 'collection' }, - { model: User, as: 'createdBy', paranoid: false }, - { model: User, as: 'updatedBy', paranoid: false }, + { model: Collection, as: "collection" }, + { model: User, as: "createdBy", paranoid: false }, + { model: User, as: "updatedBy", paranoid: false }, ], }); @@ -366,14 +366,14 @@ Document.searchForUser = async ( "teamId" = :teamId AND "collectionId" IN(:collectionIds) AND ${ - options.dateFilter ? '"updatedAt" > now() - interval :dateFilter AND' : '' + options.dateFilter ? '"updatedAt" > now() - interval :dateFilter AND' : "" } ${ options.collaboratorIds ? '"collaboratorIds" @> ARRAY[:collaboratorIds]::uuid[] AND' - : '' + : "" } - ${options.includeArchived ? '' : '"archivedAt" IS NULL AND'} + ${options.includeArchived ? "" : '"archivedAt" IS NULL AND'} "deletedAt" IS NULL AND ${ options.includeDrafts @@ -404,18 +404,18 @@ Document.searchForUser = async ( // Final query to get associated document data const documents = await Document.scope( { - method: ['withViews', user.id], + method: ["withViews", user.id], }, { - method: ['withCollection', user.id], + method: ["withCollection", user.id], } ).findAll({ where: { - id: map(results, 'id'), + id: map(results, "id"), }, include: [ - { model: User, as: 'createdBy', paranoid: false }, - { model: User, as: 'updatedBy', paranoid: false }, + { model: User, as: "createdBy", paranoid: false }, + { model: User, as: "updatedBy", paranoid: false }, ], }); @@ -430,21 +430,21 @@ Document.searchForUser = async ( // Hooks -Document.addHook('beforeSave', async model => { +Document.addHook("beforeSave", async model => { if (!model.publishedAt) return; const collection = await Collection.findByPk(model.collectionId); - if (!collection || collection.type !== 'atlas') return; + if (!collection || collection.type !== "atlas") return; await collection.updateDocument(model); model.collection = collection; }); -Document.addHook('afterCreate', async model => { +Document.addHook("afterCreate", async model => { if (!model.publishedAt) return; const collection = await Collection.findByPk(model.collectionId); - if (!collection || collection.type !== 'atlas') return; + if (!collection || collection.type !== "atlas") return; await collection.addDocumentToStructure(model); model.collection = collection; @@ -470,7 +470,7 @@ Document.prototype.migrateVersion = function() { // migrate from document version 0 -> 1 if (!this.version) { // removing the title from the document text attribute - this.text = this.text.replace(/^#\s(.*)\n/, ''); + this.text = this.text.replace(/^#\s(.*)\n/, ""); this.version = 1; migrated = true; } @@ -535,7 +535,7 @@ Document.prototype.publish = async function(options) { if (this.publishedAt) return this.save(options); const collection = await Collection.findByPk(this.collectionId); - if (collection.type !== 'atlas') return this.save(options); + if (collection.type !== "atlas") return this.save(options); await collection.addDocumentToStructure(this); @@ -618,14 +618,14 @@ Document.prototype.getSummary = function() { const plain = removeMarkdown(unescape(this.text), { stripHTML: false, }); - const lines = compact(plain.split('\n')); + const lines = compact(plain.split("\n")); const notEmpty = lines.length >= 1; if (this.version) { - return notEmpty ? lines[0] : ''; + return notEmpty ? lines[0] : ""; } - return notEmpty ? lines[1] : ''; + return notEmpty ? lines[1] : ""; }; Document.prototype.toJSON = function() { diff --git a/server/models/Document.test.js b/server/models/Document.test.js index b87c1ff6..f70b28ad 100644 --- a/server/models/Document.test.js +++ b/server/models/Document.test.js @@ -1,13 +1,13 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import { flushdb } from '../test/support'; -import { Document } from '../models'; -import { buildDocument, buildCollection, buildTeam } from '../test/factories'; +import { flushdb } from "../test/support"; +import { Document } from "../models"; +import { buildDocument, buildCollection, buildTeam } from "../test/factories"; beforeEach(flushdb); beforeEach(jest.resetAllMocks); -describe('#getSummary', () => { - test('should strip markdown', async () => { +describe("#getSummary", () => { + test("should strip markdown", async () => { const document = await buildDocument({ version: 1, text: `*paragraph* @@ -15,10 +15,10 @@ describe('#getSummary', () => { paragraph 2`, }); - expect(document.getSummary()).toBe('paragraph'); + expect(document.getSummary()).toBe("paragraph"); }); - test('should strip title when no version', async () => { + test("should strip title when no version", async () => { const document = await buildDocument({ version: null, text: `# Heading @@ -26,12 +26,12 @@ paragraph 2`, *paragraph*`, }); - expect(document.getSummary()).toBe('paragraph'); + expect(document.getSummary()).toBe("paragraph"); }); }); -describe('#migrateVersion', () => { - test('should maintain empty paragraph under headings', async () => { +describe("#migrateVersion", () => { + test("should maintain empty paragraph under headings", async () => { const document = await buildDocument({ version: 1, text: `# Heading @@ -44,7 +44,7 @@ paragraph`, paragraph`); }); - test('should add breaks under headings with extra paragraphs', async () => { + test("should add breaks under headings with extra paragraphs", async () => { const document = await buildDocument({ version: 1, text: `# Heading @@ -60,7 +60,7 @@ paragraph`, paragraph`); }); - test('should add breaks between paragraphs', async () => { + test("should add breaks between paragraphs", async () => { const document = await buildDocument({ version: 1, text: `paragraph @@ -74,7 +74,7 @@ paragraph`, paragraph`); }); - test('should add breaks for multiple empty paragraphs', async () => { + test("should add breaks for multiple empty paragraphs", async () => { const document = await buildDocument({ version: 1, text: `paragraph @@ -90,7 +90,7 @@ paragraph`, paragraph`); }); - test('should add breaks with non-latin characters', async () => { + test("should add breaks with non-latin characters", async () => { const document = await buildDocument({ version: 1, text: `除。 @@ -104,7 +104,7 @@ paragraph`); 通`); }); - test('should update task list formatting', async () => { + test("should update task list formatting", async () => { const document = await buildDocument({ version: 1, text: `[ ] list item @@ -115,7 +115,7 @@ paragraph`); `); }); - test('should update task list with multiple items', async () => { + test("should update task list with multiple items", async () => { const document = await buildDocument({ version: 1, text: `[ ] list item @@ -128,7 +128,7 @@ paragraph`); `); }); - test('should update checked task list formatting', async () => { + test("should update checked task list formatting", async () => { const document = await buildDocument({ version: 1, text: `[x] list item @@ -139,7 +139,7 @@ paragraph`); `); }); - test('should update nested task list formatting', async () => { + test("should update nested task list formatting", async () => { const document = await buildDocument({ version: 1, text: `[x] list item @@ -155,22 +155,22 @@ paragraph`); }); }); -describe('#searchForTeam', () => { - test('should return search results from public collections', async () => { +describe("#searchForTeam", () => { + test("should return search results from public collections", async () => { const team = await buildTeam(); const collection = await buildCollection({ teamId: team.id }); const document = await buildDocument({ teamId: team.id, collectionId: collection.id, - title: 'test', + title: "test", }); - const results = await Document.searchForTeam(team, 'test'); + const results = await Document.searchForTeam(team, "test"); expect(results.length).toBe(1); expect(results[0].document.id).toBe(document.id); }); - test('should not return search results from private collections', async () => { + test("should not return search results from private collections", async () => { const team = await buildTeam(); const collection = await buildCollection({ private: true, @@ -179,16 +179,16 @@ describe('#searchForTeam', () => { await buildDocument({ teamId: team.id, collectionId: collection.id, - title: 'test', + title: "test", }); - const results = await Document.searchForTeam(team, 'test'); + const results = await Document.searchForTeam(team, "test"); expect(results.length).toBe(0); }); - test('should handle no collections', async () => { + test("should handle no collections", async () => { const team = await buildTeam(); - const results = await Document.searchForTeam(team, 'test'); + const results = await Document.searchForTeam(team, "test"); expect(results.length).toBe(0); }); }); diff --git a/server/models/Event.js b/server/models/Event.js index eaf43181..8d7df44f 100644 --- a/server/models/Event.js +++ b/server/models/Event.js @@ -1,8 +1,8 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; -import events from '../events'; +import { DataTypes, sequelize } from "../sequelize"; +import events from "../events"; -const Event = sequelize.define('event', { +const Event = sequelize.define("event", { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, @@ -16,31 +16,31 @@ const Event = sequelize.define('event', { Event.associate = models => { Event.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", }); Event.belongsTo(models.User, { - as: 'actor', - foreignKey: 'actorId', + as: "actor", + foreignKey: "actorId", }); Event.belongsTo(models.Collection, { - as: 'collection', - foreignKey: 'collectionId', + as: "collection", + foreignKey: "collectionId", }); Event.belongsTo(models.Collection, { - as: 'document', - foreignKey: 'documentId', + as: "document", + foreignKey: "documentId", }); Event.belongsTo(models.Team, { - as: 'team', - foreignKey: 'teamId', + as: "team", + foreignKey: "teamId", }); }; Event.beforeCreate(event => { if (event.ip) { // cleanup IPV6 representations of IPV4 addresses - event.ip = event.ip.replace(/^::ffff:/, ''); + event.ip = event.ip.replace(/^::ffff:/, ""); } }); @@ -49,48 +49,48 @@ Event.afterCreate(event => { }); Event.ACTIVITY_EVENTS = [ - 'users.create', - 'documents.publish', - 'documents.archive', - 'documents.unarchive', - 'documents.pin', - 'documents.unpin', - 'documents.delete', - 'documents.restore', - 'collections.create', - 'collections.delete', + "users.create", + "documents.publish", + "documents.archive", + "documents.unarchive", + "documents.pin", + "documents.unpin", + "documents.delete", + "documents.restore", + "collections.create", + "collections.delete", ]; Event.AUDIT_EVENTS = [ - 'api_keys.create', - 'api_keys.delete', - 'users.create', - 'users.promote', - 'users.demote', - 'users.invite', - 'users.suspend', - 'users.activate', - 'users.delete', - 'documents.publish', - 'documents.update', - 'documents.archive', - 'documents.unarchive', - 'documents.pin', - 'documents.unpin', - 'documents.move', - 'documents.delete', - 'shares.create', - 'shares.revoke', - 'groups.create', - 'groups.update', - 'groups.delete', - 'collections.create', - 'collections.update', - 'collections.add_user', - 'collections.remove_user', - 'collections.add_group', - 'collections.remove_group', - 'collections.delete', + "api_keys.create", + "api_keys.delete", + "users.create", + "users.promote", + "users.demote", + "users.invite", + "users.suspend", + "users.activate", + "users.delete", + "documents.publish", + "documents.update", + "documents.archive", + "documents.unarchive", + "documents.pin", + "documents.unpin", + "documents.move", + "documents.delete", + "shares.create", + "shares.revoke", + "groups.create", + "groups.update", + "groups.delete", + "collections.create", + "collections.update", + "collections.add_user", + "collections.remove_user", + "collections.add_group", + "collections.remove_group", + "collections.delete", ]; export default Event; diff --git a/server/models/Group.js b/server/models/Group.js index 2dccd10c..2fea3813 100644 --- a/server/models/Group.js +++ b/server/models/Group.js @@ -1,9 +1,9 @@ // @flow -import { Op, DataTypes, sequelize } from '../sequelize'; -import { CollectionGroup, GroupUser } from '../models'; +import { Op, DataTypes, sequelize } from "../sequelize"; +import { CollectionGroup, GroupUser } from "../models"; const Group = sequelize.define( - 'group', + "group", { id: { type: DataTypes.UUID, @@ -32,7 +32,7 @@ const Group = sequelize.define( }, }); if (foundItem) { - throw new Error('The name of this group is already in use'); + throw new Error("The name of this group is already in use"); } }, }, @@ -41,39 +41,39 @@ const Group = sequelize.define( Group.associate = models => { Group.hasMany(models.GroupUser, { - as: 'groupMemberships', - foreignKey: 'groupId', + as: "groupMemberships", + foreignKey: "groupId", }); Group.hasMany(models.CollectionGroup, { - as: 'collectionGroupMemberships', - foreignKey: 'groupId', + as: "collectionGroupMemberships", + foreignKey: "groupId", }); Group.belongsTo(models.Team, { - as: 'team', - foreignKey: 'teamId', + as: "team", + foreignKey: "teamId", }); Group.belongsTo(models.User, { - as: 'createdBy', - foreignKey: 'createdById', + as: "createdBy", + foreignKey: "createdById", }); Group.belongsToMany(models.User, { - as: 'users', + as: "users", through: models.GroupUser, - foreignKey: 'groupId', + foreignKey: "groupId", }); - Group.addScope('defaultScope', { + Group.addScope("defaultScope", { include: [ { - association: 'groupMemberships', + association: "groupMemberships", required: false, }, ], - order: [['name', 'ASC']], + order: [["name", "ASC"]], }); }; // Cascade deletes to group and collection relations -Group.addHook('afterDestroy', async (group, options) => { +Group.addHook("afterDestroy", async (group, options) => { if (!group.deletedAt) return; await GroupUser.destroy({ where: { groupId: group.id } }); diff --git a/server/models/Group.test.js b/server/models/Group.test.js index 8f98d075..9909b898 100644 --- a/server/models/Group.test.js +++ b/server/models/Group.test.js @@ -1,13 +1,13 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import { flushdb } from '../test/support'; -import { CollectionGroup, GroupUser } from '../models'; -import { buildUser, buildGroup, buildCollection } from '../test/factories'; +import { flushdb } from "../test/support"; +import { CollectionGroup, GroupUser } from "../models"; +import { buildUser, buildGroup, buildCollection } from "../test/factories"; beforeEach(flushdb); beforeEach(jest.resetAllMocks); -describe('afterDestroy hook', () => { - test('should destroy associated group and collection join relations', async () => { +describe("afterDestroy hook", () => { + test("should destroy associated group and collection join relations", async () => { const group = await buildGroup(); const teamId = group.teamId; diff --git a/server/models/GroupUser.js b/server/models/GroupUser.js index 68da219c..1c0a92f7 100644 --- a/server/models/GroupUser.js +++ b/server/models/GroupUser.js @@ -1,8 +1,8 @@ // @flow -import { sequelize } from '../sequelize'; +import { sequelize } from "../sequelize"; const GroupUser = sequelize.define( - 'group_user', + "group_user", {}, { timestamps: true, @@ -12,21 +12,21 @@ const GroupUser = sequelize.define( GroupUser.associate = models => { GroupUser.belongsTo(models.Group, { - as: 'group', - foreignKey: 'groupId', + as: "group", + foreignKey: "groupId", primary: true, }); GroupUser.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", primary: true, }); GroupUser.belongsTo(models.User, { - as: 'createdBy', - foreignKey: 'createdById', + as: "createdBy", + foreignKey: "createdById", }); - GroupUser.addScope('defaultScope', { - include: [{ association: 'user' }], + GroupUser.addScope("defaultScope", { + include: [{ association: "user" }], }); }; diff --git a/server/models/Integration.js b/server/models/Integration.js index 9251ba7c..b7dc24a9 100644 --- a/server/models/Integration.js +++ b/server/models/Integration.js @@ -1,7 +1,7 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; +import { DataTypes, sequelize } from "../sequelize"; -const Integration = sequelize.define('integration', { +const Integration = sequelize.define("integration", { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, @@ -15,20 +15,20 @@ const Integration = sequelize.define('integration', { Integration.associate = models => { Integration.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", }); Integration.belongsTo(models.Team, { - as: 'team', - foreignKey: 'teamId', + as: "team", + foreignKey: "teamId", }); Integration.belongsTo(models.Collection, { - as: 'collection', - foreignKey: 'collectionId', + as: "collection", + foreignKey: "collectionId", }); Integration.belongsTo(models.Authentication, { - as: 'authentication', - foreignKey: 'authenticationId', + as: "authentication", + foreignKey: "authenticationId", }); }; diff --git a/server/models/Notification.js b/server/models/Notification.js index 2302b219..0174e0f1 100644 --- a/server/models/Notification.js +++ b/server/models/Notification.js @@ -1,8 +1,8 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; +import { DataTypes, sequelize } from "../sequelize"; const Notification = sequelize.define( - 'notification', + "notification", { id: { type: DataTypes.UUID, @@ -24,12 +24,12 @@ const Notification = sequelize.define( Notification.associate = models => { Notification.belongsTo(models.User, { - as: 'actor', - foreignKey: 'actorId', + as: "actor", + foreignKey: "actorId", }); Notification.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", }); }; diff --git a/server/models/NotificationSetting.js b/server/models/NotificationSetting.js index 6e5be653..24cdd03c 100644 --- a/server/models/NotificationSetting.js +++ b/server/models/NotificationSetting.js @@ -1,9 +1,9 @@ // @flow -import crypto from 'crypto'; -import { DataTypes, sequelize } from '../sequelize'; +import crypto from "crypto"; +import { DataTypes, sequelize } from "../sequelize"; const NotificationSetting = sequelize.define( - 'notification_setting', + "notification_setting", { id: { type: DataTypes.UUID, @@ -15,11 +15,11 @@ const NotificationSetting = sequelize.define( validate: { isIn: [ [ - 'documents.publish', - 'documents.update', - 'collections.create', - 'emails.onboarding', - 'emails.features', + "documents.publish", + "documents.update", + "collections.create", + "emails.onboarding", + "emails.features", ], ], }, @@ -43,20 +43,20 @@ const NotificationSetting = sequelize.define( ); NotificationSetting.getUnsubscribeToken = userId => { - const hash = crypto.createHash('sha256'); + const hash = crypto.createHash("sha256"); hash.update(`${userId}-${process.env.SECRET_KEY}`); - return hash.digest('hex'); + return hash.digest("hex"); }; NotificationSetting.associate = models => { NotificationSetting.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', - onDelete: 'cascade', + as: "user", + foreignKey: "userId", + onDelete: "cascade", }); NotificationSetting.belongsTo(models.Team, { - as: 'team', - foreignKey: 'teamId', + as: "team", + foreignKey: "teamId", }); }; diff --git a/server/models/Revision.js b/server/models/Revision.js index 75c45861..9377d447 100644 --- a/server/models/Revision.js +++ b/server/models/Revision.js @@ -1,10 +1,10 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; -import MarkdownSerializer from 'slate-md-serializer'; +import { DataTypes, sequelize } from "../sequelize"; +import MarkdownSerializer from "slate-md-serializer"; const serializer = new MarkdownSerializer(); -const Revision = sequelize.define('revision', { +const Revision = sequelize.define("revision", { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, @@ -23,18 +23,18 @@ const Revision = sequelize.define('revision', { Revision.associate = models => { Revision.belongsTo(models.Document, { - as: 'document', - foreignKey: 'documentId', - onDelete: 'cascade', + as: "document", + foreignKey: "documentId", + onDelete: "cascade", }); Revision.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", }); Revision.addScope( - 'defaultScope', + "defaultScope", { - include: [{ model: models.User, as: 'user', paranoid: false }], + include: [{ model: models.User, as: "user", paranoid: false }], }, { override: true } ); @@ -46,7 +46,7 @@ Revision.prototype.migrateVersion = function() { // migrate from document version 0 -> 1 if (!this.version) { // removing the title from the document text attribute - this.text = this.text.replace(/^#\s(.*)\n/, ''); + this.text = this.text.replace(/^#\s(.*)\n/, ""); this.version = 1; migrated = true; } diff --git a/server/models/Share.js b/server/models/Share.js index 6d77b631..f76a2a12 100644 --- a/server/models/Share.js +++ b/server/models/Share.js @@ -1,8 +1,8 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; +import { DataTypes, sequelize } from "../sequelize"; const Share = sequelize.define( - 'share', + "share", { id: { type: DataTypes.UUID, @@ -23,16 +23,16 @@ const Share = sequelize.define( Share.associate = models => { Share.belongsTo(models.User, { - as: 'user', - foreignKey: 'userId', + as: "user", + foreignKey: "userId", }); Share.belongsTo(models.Team, { - as: 'team', - foreignKey: 'teamId', + as: "team", + foreignKey: "teamId", }); Share.belongsTo(models.Document, { - as: 'document', - foreignKey: 'documentId', + as: "document", + foreignKey: "documentId", }); }; diff --git a/server/models/Star.js b/server/models/Star.js index 43d40452..74f3dc98 100644 --- a/server/models/Star.js +++ b/server/models/Star.js @@ -1,7 +1,7 @@ // @flow -import { DataTypes, sequelize } from '../sequelize'; +import { DataTypes, sequelize } from "../sequelize"; -const Star = sequelize.define('star', { +const Star = sequelize.define("star", { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, diff --git a/server/models/Team.js b/server/models/Team.js index bdbed537..fa9d63cd 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -1,25 +1,25 @@ // @flow -import uuid from 'uuid'; -import { URL } from 'url'; -import fs from 'fs'; -import util from 'util'; -import path from 'path'; -import { DataTypes, sequelize, Op } from '../sequelize'; -import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3'; +import uuid from "uuid"; +import { URL } from "url"; +import fs from "fs"; +import util from "util"; +import path from "path"; +import { DataTypes, sequelize, Op } from "../sequelize"; +import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3"; import { stripSubdomain, RESERVED_SUBDOMAINS, -} from '../../shared/utils/domains'; -import { ValidationError } from '../errors'; +} from "../../shared/utils/domains"; +import { ValidationError } from "../errors"; -import Collection from './Collection'; -import Document from './Document'; -import User from './User'; +import Collection from "./Collection"; +import Document from "./Document"; +import User from "./User"; const readFile = util.promisify(fs.readFile); const Team = sequelize.define( - 'team', + "team", { id: { type: DataTypes.UUID, @@ -33,16 +33,16 @@ const Team = sequelize.define( validate: { isLowercase: true, is: { - args: [/^[a-z\d-]+$/, 'i'], - msg: 'Must be only alphanumeric and dashes', + args: [/^[a-z\d-]+$/, "i"], + msg: "Must be only alphanumeric and dashes", }, len: { args: [4, 32], - msg: 'Must be between 4 and 32 characters', + msg: "Must be between 4 and 32 characters", }, notIn: { args: [RESERVED_SUBDOMAINS], - msg: 'You chose a restricted word, please try another.', + msg: "You chose a restricted word, please try another.", }, }, unique: true, @@ -66,13 +66,13 @@ const Team = sequelize.define( { getterMethods: { url() { - if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== 'true') { + if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== "true") { return process.env.URL; } const url = new URL(process.env.URL); url.host = `${this.subdomain}.${stripSubdomain(url.host)}`; - return url.href.replace(/\/$/, ''); + return url.href.replace(/\/$/, ""); }, logoUrl() { return ( @@ -84,9 +84,9 @@ const Team = sequelize.define( ); Team.associate = models => { - Team.hasMany(models.Collection, { as: 'collections' }); - Team.hasMany(models.Document, { as: 'documents' }); - Team.hasMany(models.User, { as: 'users' }); + Team.hasMany(models.Collection, { as: "collections" }); + Team.hasMany(models.Document, { as: "documents" }); + Team.hasMany(models.User, { as: "users" }); }; const uploadAvatar = async model => { @@ -95,14 +95,14 @@ const uploadAvatar = async model => { if ( avatarUrl && - !avatarUrl.startsWith('/api') && + !avatarUrl.startsWith("/api") && !avatarUrl.startsWith(endpoint) ) { try { const newUrl = await uploadToS3FromUrl( avatarUrl, `avatars/${model.id}/${uuid.v4()}`, - 'public-read' + "public-read" ); if (newUrl) model.avatarUrl = newUrl; } catch (err) { @@ -131,10 +131,10 @@ Team.prototype.provisionSubdomain = async function(subdomain) { Team.prototype.provisionFirstCollection = async function(userId) { const collection = await Collection.create({ - name: 'Welcome', + name: "Welcome", description: - 'This collection is a quick guide to what Outline is all about. Feel free to delete this collection once your team is up to speed with the basics!', - type: 'atlas', + "This collection is a quick guide to what Outline is all about. Feel free to delete this collection once your team is up to speed with the basics!", + type: "atlas", teamId: this.id, creatorId: userId, }); @@ -142,15 +142,15 @@ Team.prototype.provisionFirstCollection = async function(userId) { // For the first collection we go ahead and create some intitial documents to get // the team started. You can edit these in /server/onboarding/x.md const onboardingDocs = [ - '❤️ Support', - '🚀 Integrations & API', - '📝 Our Editor', - '👋 What is Outline', + "❤️ Support", + "🚀 Integrations & API", + "📝 Our Editor", + "👋 What is Outline", ]; for (const title of onboardingDocs) { const text = await readFile( - path.join(__dirname, '..', 'onboarding', `${title}.md`), - 'utf8' + path.join(__dirname, "..", "onboarding", `${title}.md`), + "utf8" ); const document = await Document.create({ version: 1, @@ -186,13 +186,13 @@ Team.prototype.removeAdmin = async function(user: User) { if (res.count >= 1) { return user.update({ isAdmin: false }); } else { - throw new ValidationError('At least one admin is required'); + throw new ValidationError("At least one admin is required"); } }; Team.prototype.suspendUser = async function(user: User, admin: User) { if (user.id === admin.id) - throw new ValidationError('Unable to suspend the current user'); + throw new ValidationError("Unable to suspend the current user"); return user.update({ suspendedById: admin.id, suspendedAt: new Date(), @@ -208,7 +208,7 @@ Team.prototype.activateUser = async function(user: User, admin: User) { Team.prototype.collectionIds = async function(paranoid: boolean = true) { let models = await Collection.findAll({ - attributes: ['id', 'private'], + attributes: ["id", "private"], where: { teamId: this.id, private: false }, paranoid, }); diff --git a/server/models/Team.test.js b/server/models/Team.test.js index 4563bc32..2373309d 100644 --- a/server/models/Team.test.js +++ b/server/models/Team.test.js @@ -1,28 +1,28 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import { flushdb } from '../test/support'; -import { buildTeam } from '../test/factories'; +import { flushdb } from "../test/support"; +import { buildTeam } from "../test/factories"; beforeEach(flushdb); -it('should set subdomain if available', async () => { +it("should set subdomain if available", async () => { const team = await buildTeam(); - const subdomain = await team.provisionSubdomain('testy'); - expect(subdomain).toEqual('testy'); - expect(team.subdomain).toEqual('testy'); + const subdomain = await team.provisionSubdomain("testy"); + expect(subdomain).toEqual("testy"); + expect(team.subdomain).toEqual("testy"); }); -it('should set subdomain with append if unavailable', async () => { - await buildTeam({ subdomain: 'myteam' }); +it("should set subdomain with append if unavailable", async () => { + await buildTeam({ subdomain: "myteam" }); const team = await buildTeam(); - const subdomain = await team.provisionSubdomain('myteam'); - expect(subdomain).toEqual('myteam1'); - expect(team.subdomain).toEqual('myteam1'); + const subdomain = await team.provisionSubdomain("myteam"); + expect(subdomain).toEqual("myteam1"); + expect(team.subdomain).toEqual("myteam1"); }); -it('should do nothing if subdomain already set', async () => { - const team = await buildTeam({ subdomain: 'example' }); - const subdomain = await team.provisionSubdomain('myteam'); - expect(subdomain).toEqual('example'); - expect(team.subdomain).toEqual('example'); +it("should do nothing if subdomain already set", async () => { + const team = await buildTeam({ subdomain: "example" }); + const subdomain = await team.provisionSubdomain("myteam"); + expect(subdomain).toEqual("example"); + expect(team.subdomain).toEqual("example"); }); diff --git a/server/models/User.js b/server/models/User.js index d2afaf48..4a1a49c0 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,18 +1,18 @@ // @flow -import crypto from 'crypto'; -import uuid from 'uuid'; -import JWT from 'jsonwebtoken'; -import subMinutes from 'date-fns/sub_minutes'; -import { ValidationError } from '../errors'; -import { DataTypes, sequelize, encryptedFields } from '../sequelize'; -import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3'; -import { sendEmail } from '../mailer'; -import { Star, Team, Collection, NotificationSetting, ApiKey } from '.'; +import crypto from "crypto"; +import uuid from "uuid"; +import JWT from "jsonwebtoken"; +import subMinutes from "date-fns/sub_minutes"; +import { ValidationError } from "../errors"; +import { DataTypes, sequelize, encryptedFields } from "../sequelize"; +import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3"; +import { sendEmail } from "../mailer"; +import { Star, Team, Collection, NotificationSetting, ApiKey } from "."; -const DEFAULT_AVATAR_HOST = 'https://tiley.herokuapp.com'; +const DEFAULT_AVATAR_HOST = "https://tiley.herokuapp.com"; const User = sequelize.define( - 'user', + "user", { id: { type: DataTypes.UUID, @@ -27,7 +27,7 @@ const User = sequelize.define( service: { type: DataTypes.STRING, allowNull: true }, serviceId: { type: DataTypes.STRING, allowNull: true, unique: true }, slackData: DataTypes.JSONB, - jwtSecret: encryptedFields.vault('jwtSecret'), + jwtSecret: encryptedFields.vault("jwtSecret"), lastActiveAt: DataTypes.DATE, lastActiveIp: { type: DataTypes.STRING, allowNull: true }, lastSignedInAt: DataTypes.DATE, @@ -43,15 +43,15 @@ const User = sequelize.define( return !!this.suspendedAt; }, avatarUrl() { - const original = this.getDataValue('avatarUrl'); + const original = this.getDataValue("avatarUrl"); if (original) { return original; } const hash = crypto - .createHash('md5') - .update(this.email || '') - .digest('hex'); + .createHash("md5") + .update(this.email || "") + .digest("hex"); return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${this.name[0]}.png`; }, }, @@ -60,22 +60,22 @@ const User = sequelize.define( // Class methods User.associate = models => { - User.hasMany(models.ApiKey, { as: 'apiKeys', onDelete: 'cascade' }); + User.hasMany(models.ApiKey, { as: "apiKeys", onDelete: "cascade" }); User.hasMany(models.NotificationSetting, { - as: 'notificationSettings', - onDelete: 'cascade', + as: "notificationSettings", + onDelete: "cascade", }); - User.hasMany(models.Document, { as: 'documents' }); - User.hasMany(models.View, { as: 'views' }); + User.hasMany(models.Document, { as: "documents" }); + User.hasMany(models.View, { as: "views" }); User.belongsTo(models.Team); }; // Instance methods User.prototype.collectionIds = async function(paranoid: boolean = true) { const collectionStubs = await Collection.scope({ - method: ['withMembership', this.id], + method: ["withMembership", this.id], }).findAll({ - attributes: ['id', 'private'], + attributes: ["id", "private"], where: { teamId: this.teamId }, paranoid, }); @@ -113,8 +113,8 @@ User.prototype.getJwtToken = function() { }; User.prototype.getEmailSigninToken = function() { - if (this.service && this.service !== 'email') { - throw new Error('Cannot generate email signin token for OAuth user'); + if (this.service && this.service !== "email") { + throw new Error("Cannot generate email signin token for OAuth user"); } return JWT.sign( @@ -129,7 +129,7 @@ const uploadAvatar = async model => { if ( avatarUrl && - !avatarUrl.startsWith('/api') && + !avatarUrl.startsWith("/api") && !avatarUrl.startsWith(endpoint) && !avatarUrl.startsWith(DEFAULT_AVATAR_HOST) ) { @@ -137,7 +137,7 @@ const uploadAvatar = async model => { const newUrl = await uploadToS3FromUrl( avatarUrl, `avatars/${model.id}/${uuid.v4()}`, - 'public-read' + "public-read" ); if (newUrl) model.avatarUrl = newUrl; } catch (err) { @@ -148,7 +148,7 @@ const uploadAvatar = async model => { }; const setRandomJwtSecret = model => { - model.jwtSecret = crypto.randomBytes(64).toString('hex'); + model.jwtSecret = crypto.randomBytes(64).toString("hex"); }; const removeIdentifyingInfo = async (model, options) => { @@ -166,8 +166,8 @@ const removeIdentifyingInfo = async (model, options) => { }); model.email = null; - model.name = 'Unknown'; - model.avatarUrl = ''; + model.name = "Unknown"; + model.avatarUrl = ""; model.serviceId = null; model.username = null; model.slackData = null; @@ -188,7 +188,7 @@ const checkLastAdmin = async model => { if (userCount > 1 && adminCount <= 1) { throw new ValidationError( - 'Cannot delete account as only admin. Please transfer admin permissions to another user and try again.' + "Cannot delete account as only admin. Please transfer admin permissions to another user and try again." ); } } @@ -204,8 +204,8 @@ User.afterCreate(async user => { // From Slack support: // If you wish to contact users at an email address obtained through Slack, // you need them to opt-in through a clear and separate process. - if (user.service && user.service !== 'slack') { - sendEmail('welcome', user.email, { teamUrl: team.url }); + if (user.service && user.service !== "slack") { + sendEmail("welcome", user.email, { teamUrl: team.url }); } }); @@ -217,7 +217,7 @@ User.afterCreate(async (user, options) => { where: { userId: user.id, teamId: user.teamId, - event: 'documents.update', + event: "documents.update", }, transaction: options.transaction, }), @@ -225,7 +225,7 @@ User.afterCreate(async (user, options) => { where: { userId: user.id, teamId: user.teamId, - event: 'emails.onboarding', + event: "emails.onboarding", }, transaction: options.transaction, }), diff --git a/server/models/User.test.js b/server/models/User.test.js index cce0a743..cb49fbe9 100644 --- a/server/models/User.test.js +++ b/server/models/User.test.js @@ -1,10 +1,10 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import { flushdb } from '../test/support'; -import { buildUser } from '../test/factories'; +import { flushdb } from "../test/support"; +import { buildUser } from "../test/factories"; beforeEach(flushdb); -it('should set JWT secret', async () => { +it("should set JWT secret", async () => { const user = await buildUser(); expect(user.getJwtToken()).toBeTruthy(); }); diff --git a/server/models/View.js b/server/models/View.js index b24cfc08..4de79ad4 100644 --- a/server/models/View.js +++ b/server/models/View.js @@ -1,11 +1,11 @@ // @flow -import subMilliseconds from 'date-fns/sub_milliseconds'; -import { Op, DataTypes, sequelize } from '../sequelize'; -import { User } from '../models'; -import { USER_PRESENCE_INTERVAL } from '../../shared/constants'; +import subMilliseconds from "date-fns/sub_milliseconds"; +import { Op, DataTypes, sequelize } from "../sequelize"; +import { User } from "../models"; +import { USER_PRESENCE_INTERVAL } from "../../shared/constants"; const View = sequelize.define( - 'view', + "view", { id: { type: DataTypes.UUID, @@ -42,7 +42,7 @@ View.increment = async where => { View.findByDocument = async documentId => { return View.findAll({ where: { documentId }, - order: [['updatedAt', 'DESC']], + order: [["updatedAt", "DESC"]], include: [ { model: User, @@ -60,7 +60,7 @@ View.findRecentlyEditingByDocument = async documentId => { [Op.gt]: subMilliseconds(new Date(), USER_PRESENCE_INTERVAL * 2), }, }, - order: [['lastEditingAt', 'DESC']], + order: [["lastEditingAt", "DESC"]], }); }; diff --git a/server/models/index.js b/server/models/index.js index cf5c27d1..73c219aa 100644 --- a/server/models/index.js +++ b/server/models/index.js @@ -1,24 +1,24 @@ // @flow -import ApiKey from './ApiKey'; -import Attachment from './Attachment'; -import Authentication from './Authentication'; -import Backlink from './Backlink'; -import Collection from './Collection'; -import CollectionUser from './CollectionUser'; -import CollectionGroup from './CollectionGroup'; -import Document from './Document'; -import Event from './Event'; -import Integration from './Integration'; -import Group from './Group'; -import GroupUser from './GroupUser'; -import Notification from './Notification'; -import NotificationSetting from './NotificationSetting'; -import Revision from './Revision'; -import Share from './Share'; -import Star from './Star'; -import Team from './Team'; -import User from './User'; -import View from './View'; +import ApiKey from "./ApiKey"; +import Attachment from "./Attachment"; +import Authentication from "./Authentication"; +import Backlink from "./Backlink"; +import Collection from "./Collection"; +import CollectionUser from "./CollectionUser"; +import CollectionGroup from "./CollectionGroup"; +import Document from "./Document"; +import Event from "./Event"; +import Integration from "./Integration"; +import Group from "./Group"; +import GroupUser from "./GroupUser"; +import Notification from "./Notification"; +import NotificationSetting from "./NotificationSetting"; +import Revision from "./Revision"; +import Share from "./Share"; +import Star from "./Star"; +import Team from "./Team"; +import User from "./User"; +import View from "./View"; const models = { ApiKey, @@ -45,7 +45,7 @@ const models = { // based on https://github.com/sequelize/express-example/blob/master/models/index.js Object.keys(models).forEach(modelName => { - if ('associate' in models[modelName]) { + if ("associate" in models[modelName]) { models[modelName].associate(models); } }); diff --git a/server/pages/Home.js b/server/pages/Home.js index 2f5207b4..789963f2 100644 --- a/server/pages/Home.js +++ b/server/pages/Home.js @@ -1,17 +1,17 @@ // @flow -import * as React from 'react'; -import { Helmet } from 'react-helmet'; -import styled from 'styled-components'; -import Grid from 'styled-components-grid'; -import AuthNotices from './components/AuthNotices'; -import Hero from './components/Hero'; -import HeroText from './components/HeroText'; -import SigninButtons from './components/SigninButtons'; -import Branding from '../../shared/components/Branding'; -import { githubUrl } from '../../shared/utils/routeHelpers'; +import * as React from "react"; +import { Helmet } from "react-helmet"; +import styled from "styled-components"; +import Grid from "styled-components-grid"; +import AuthNotices from "./components/AuthNotices"; +import Hero from "./components/Hero"; +import HeroText from "./components/HeroText"; +import SigninButtons from "./components/SigninButtons"; +import Branding from "../../shared/components/Branding"; +import { githubUrl } from "../../shared/utils/routeHelpers"; type Props = { - notice?: 'google-hd' | 'auth-error' | 'hd-not-allowed', + notice?: "google-hd" | "auth-error" | "hd-not-allowed", lastSignedIn: string, googleSigninEnabled: boolean, slackSigninEnabled: boolean, diff --git a/server/pages/SubdomainSignin.js b/server/pages/SubdomainSignin.js index be1c7416..90038606 100644 --- a/server/pages/SubdomainSignin.js +++ b/server/pages/SubdomainSignin.js @@ -1,20 +1,20 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Grid from 'styled-components-grid'; -import Hero from './components/Hero'; -import HeroText from './components/HeroText'; -import Button from './components/Button'; -import SigninButtons from './components/SigninButtons'; -import AuthNotices from './components/AuthNotices'; -import Centered from './components/Centered'; -import PageTitle from './components/PageTitle'; -import { Team } from '../models'; +import * as React from "react"; +import styled from "styled-components"; +import Grid from "styled-components-grid"; +import Hero from "./components/Hero"; +import HeroText from "./components/HeroText"; +import Button from "./components/Button"; +import SigninButtons from "./components/SigninButtons"; +import AuthNotices from "./components/AuthNotices"; +import Centered from "./components/Centered"; +import PageTitle from "./components/PageTitle"; +import { Team } from "../models"; type Props = { team: Team, guest?: boolean, - notice?: 'google-hd' | 'auth-error' | 'hd-not-allowed' | 'guest-success', + notice?: "google-hd" | "auth-error" | "hd-not-allowed" | "guest-success", lastSignedIn: string, googleSigninEnabled: boolean, slackSigninEnabled: boolean, @@ -37,7 +37,7 @@ function SubdomainSignin({ const guestSigninForm = (
    - {' '} + {" "} @@ -54,7 +54,7 @@ function SubdomainSignin({ -

    {lastSignedIn ? 'Welcome back,' : 'Hey there,'}

    +

    {lastSignedIn ? "Welcome back," : "Hey there,"}

    {guest && guestSigninEnabled ? ( @@ -100,7 +100,7 @@ function SubdomainSignin({

    - Trying to create or sign in to a different team?{' '} + Trying to create or sign in to a different team?{" "} Head to the homepage.

    diff --git a/server/pages/components/Analytics.js b/server/pages/components/Analytics.js index 533a8260..552f0295 100644 --- a/server/pages/components/Analytics.js +++ b/server/pages/components/Analytics.js @@ -1,5 +1,5 @@ // @flow -import * as React from 'react'; +import * as React from "react"; function Analytics() { if (!process.env.GOOGLE_ANALYTICS_ID) return null; diff --git a/server/pages/components/AuthNotices.js b/server/pages/components/AuthNotices.js index 060885b8..6e032e6e 100644 --- a/server/pages/components/AuthNotices.js +++ b/server/pages/components/AuthNotices.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import Notice from '../../../shared/components/Notice'; +import * as React from "react"; +import Notice from "../../../shared/components/Notice"; type Props = { notice?: string, @@ -9,49 +9,49 @@ type Props = { export default function AuthNotices({ notice }: Props) { return ( - {notice === 'guest-success' && ( + {notice === "guest-success" && ( A magic sign-in link has been sent to your email address, no password needed. )} - {notice === 'google-hd' && ( + {notice === "google-hd" && ( Sorry, Google sign in cannot be used with a personal email. Please try signing in with your company Google account. )} - {notice === 'hd-not-allowed' && ( + {notice === "hd-not-allowed" && ( Sorry, your Google apps domain is not allowed. Please try again with an allowed company domain. )} - {notice === 'email-auth-required' && ( + {notice === "email-auth-required" && ( Your account uses email sign-in, please sign-in with email to continue. )} - {notice === 'email-auth-ratelimit' && ( + {notice === "email-auth-ratelimit" && ( An email sign-in link was recently sent, please check your inbox and try again in a few minutes. )} - {notice === 'auth-error' && ( + {notice === "auth-error" && ( Authentication failed - we were unable to sign you in at this time. Please try again. )} - {notice === 'expired-token' && ( + {notice === "expired-token" && ( Sorry, it looks like that sign-in link is no longer valid, please try requesting another. )} - {notice === 'suspended' && ( + {notice === "suspended" && ( Your Outline account has been suspended. To re-activate your account, please contact a team admin. diff --git a/server/pages/components/Button.js b/server/pages/components/Button.js index ccbeb618..f991f804 100644 --- a/server/pages/components/Button.js +++ b/server/pages/components/Button.js @@ -1,5 +1,5 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const Button = styled.a` border: 0; diff --git a/server/pages/components/Centered.js b/server/pages/components/Centered.js index 62cdd3c7..1e0ef2ad 100644 --- a/server/pages/components/Centered.js +++ b/server/pages/components/Centered.js @@ -1,5 +1,5 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const Centered = styled.div` margin: 0 auto; diff --git a/server/pages/components/Content.js b/server/pages/components/Content.js index 587298a2..9444eb3b 100644 --- a/server/pages/components/Content.js +++ b/server/pages/components/Content.js @@ -1,5 +1,5 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; export default styled.div` width: 100%; diff --git a/server/pages/components/Header.js b/server/pages/components/Header.js index c82cec15..4880ebaa 100644 --- a/server/pages/components/Header.js +++ b/server/pages/components/Header.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import breakpoint from 'styled-components-breakpoint'; -import styled from 'styled-components'; -import Centered from './Centered'; +import * as React from "react"; +import breakpoint from "styled-components-breakpoint"; +import styled from "styled-components"; +import Centered from "./Centered"; type Props = { children: React.Node, @@ -25,17 +25,17 @@ const Wrapper = styled.div` margin-top: -70px; margin-bottom: 2em; text-align: center; - background: ${props => props.background || 'transparent'}; + background: ${props => props.background || "transparent"}; z-index: -1; &:before { - content: ''; + content: ""; position: absolute; top: 0; left: -30px; width: 100vw; height: 100%; - background: ${props => props.background || 'transparent'}; + background: ${props => props.background || "transparent"}; z-index: -10; } @@ -51,7 +51,7 @@ const Wrapper = styled.div` margin: 0 0 0.1em; } - ${breakpoint('tablet')` + ${breakpoint("tablet")` padding: 8em 3em 3em 3em; h1 { diff --git a/server/pages/components/Hero.js b/server/pages/components/Hero.js index 270bd2ab..b6c726c9 100644 --- a/server/pages/components/Hero.js +++ b/server/pages/components/Hero.js @@ -1,6 +1,6 @@ // @flow -import styled from 'styled-components'; -import Centered from './Centered'; +import styled from "styled-components"; +import Centered from "./Centered"; const Hero = styled(Centered)` width: 100%; diff --git a/server/pages/components/HeroText.js b/server/pages/components/HeroText.js index b075b05d..bb90e963 100644 --- a/server/pages/components/HeroText.js +++ b/server/pages/components/HeroText.js @@ -1,5 +1,5 @@ // @flow -import styled from 'styled-components'; +import styled from "styled-components"; const HeroText = styled.p` font-size: 22px; diff --git a/server/pages/components/Layout.js b/server/pages/components/Layout.js index c10fde62..e9904402 100644 --- a/server/pages/components/Layout.js +++ b/server/pages/components/Layout.js @@ -1,15 +1,15 @@ // @flow -import * as React from 'react'; -import { Helmet } from 'react-helmet'; -import styled from 'styled-components'; -import breakpoint from 'styled-components-breakpoint'; -import Analytics from './Analytics'; -import GlobalStyles from '../../../shared/styles/globals'; -import prefetchTags from '../../utils/prefetchTags'; +import * as React from "react"; +import { Helmet } from "react-helmet"; +import styled from "styled-components"; +import breakpoint from "styled-components-breakpoint"; +import Analytics from "./Analytics"; +import GlobalStyles from "../../../shared/styles/globals"; +import prefetchTags from "../../utils/prefetchTags"; -export const title = 'Outline'; +export const title = "Outline"; export const description = - 'Your team’s knowledge base - Team wiki, documentation, playbooks, onboarding & more…'; + "Your team’s knowledge base - Team wiki, documentation, playbooks, onboarding & more…"; export const screenshotUrl = `${process.env.URL}/screenshot.png`; type Props = { @@ -62,8 +62,8 @@ function Layout({ children, loggedIn, sessions }: Props) { - {'{{HEAD}}'} - {'{{CSS}}'} + {"{{HEAD}}"} + {"{{CSS}}"} {children} @@ -73,7 +73,7 @@ function Layout({ children, loggedIn, sessions }: Props) { const Body = styled.body` padding: 0 30px; - ${breakpoint('desktop')` + ${breakpoint("desktop")` padding: 0; `}; `; diff --git a/server/pages/components/PageTitle.js b/server/pages/components/PageTitle.js index f9d6f37e..41273a0a 100644 --- a/server/pages/components/PageTitle.js +++ b/server/pages/components/PageTitle.js @@ -1,6 +1,6 @@ // @flow -import * as React from 'react'; -import { Helmet } from 'react-helmet'; +import * as React from "react"; +import { Helmet } from "react-helmet"; function PageTitle({ title }: { title: string }) { return ( diff --git a/server/pages/components/SigninButtons.js b/server/pages/components/SigninButtons.js index ff8fd613..b560feb2 100644 --- a/server/pages/components/SigninButtons.js +++ b/server/pages/components/SigninButtons.js @@ -1,13 +1,13 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Button from './Button'; -import { signin } from '../../../shared/utils/routeHelpers'; -import Flex from '../../../shared/components/Flex'; -import Notice from '../../../shared/components/Notice'; -import GoogleLogo from '../../../shared/components/GoogleLogo'; -import SlackLogo from '../../../shared/components/SlackLogo'; -import breakpoint from 'styled-components-breakpoint'; +import * as React from "react"; +import styled from "styled-components"; +import Button from "./Button"; +import { signin } from "../../../shared/utils/routeHelpers"; +import Flex from "../../../shared/components/Flex"; +import Notice from "../../../shared/components/Notice"; +import GoogleLogo from "../../../shared/components/GoogleLogo"; +import SlackLogo from "../../../shared/components/SlackLogo"; +import breakpoint from "styled-components-breakpoint"; type Props = { lastSignedIn?: string, @@ -33,24 +33,24 @@ const SigninButtons = ({ )} {slackSigninEnabled && ( - - {lastSignedIn === 'slack' && 'You signed in with Slack previously'} + {lastSignedIn === "slack" && "You signed in with Slack previously"} )} {googleSigninEnabled && ( - - {lastSignedIn === 'google' && - 'You signed in with Google previously'} + {lastSignedIn === "google" && + "You signed in with Google previously"} )} @@ -61,7 +61,7 @@ const SigninButtons = ({ const Column = styled(Flex)` text-align: center; - ${breakpoint('tablet')` + ${breakpoint("tablet")` &:first-child { margin-right: 8px; } @@ -73,7 +73,7 @@ const Wrapper = styled(Flex)` justify-content: center; margin-top: 16px; - ${breakpoint('tablet')` + ${breakpoint("tablet")` display: flex; justify-content: flex-start; margin-top: 0; diff --git a/server/pages/developers/Api.js b/server/pages/developers/Api.js index b4e451c7..24839aef 100644 --- a/server/pages/developers/Api.js +++ b/server/pages/developers/Api.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import styled from 'styled-components'; -import Grid from 'styled-components-grid'; -import PageTitle from '../components/PageTitle'; -import Header from '../components/Header'; -import Content from '../components/Content'; +import * as React from "react"; +import styled from "styled-components"; +import Grid from "styled-components-grid"; +import PageTitle from "../components/PageTitle"; +import Header from "../components/Header"; +import Content from "../components/Content"; export default function Api() { return ( @@ -386,7 +386,7 @@ export default function Api() { This method allows you to publish a new document under an existing collection. By default a document is set to the parent collection - root. If you want to create a subdocument, you can pass{' '} + root. If you want to create a subdocument, you can pass{" "} parentDocumentId to set parent document. @@ -958,7 +958,7 @@ const Argument = (props: ArgumentProps) => ( {props.id} - {props.required ? 'required' : 'optional'} + {props.required ? "required" : "optional"} {props.description} diff --git a/server/pages/developers/index.js b/server/pages/developers/index.js index 63304c7e..df72adb7 100644 --- a/server/pages/developers/index.js +++ b/server/pages/developers/index.js @@ -1,10 +1,10 @@ // @flow -import * as React from 'react'; -import Grid from 'styled-components-grid'; -import styled from 'styled-components'; -import PageTitle from '../components/PageTitle'; -import Header from '../components/Header'; -import Content from '../components/Content'; +import * as React from "react"; +import Grid from "styled-components-grid"; +import styled from "styled-components"; +import PageTitle from "../components/PageTitle"; +import Header from "../components/Header"; +import Content from "../components/Content"; export default function Developers() { return ( @@ -51,8 +51,8 @@ export default function Developers() {

    Making requests

    Outline’s API follows simple RPC style conventions where each API - endpoint is a method on{' '} - https://www.getoutline.com/api/<METHOD>. Both{' '} + endpoint is a method on{" "} + https://www.getoutline.com/api/<METHOD>. Both{" "} GET and POST methods are supported but it’s recommended that you make all calls using POST. Only HTTPS is supported in production. @@ -61,10 +61,10 @@ export default function Developers() {

    For GET requests query string parameters are expected (e.g. - /api/document.info?id=...&token=...). When making{' '} + /api/document.info?id=...&token=...). When making{" "} POST requests, request parameters are parsed depending on Content-Type header. To make a call - using JSON payload, one must pass{' '} + using JSON payload, one must pass{" "} Content-Type: application/json header:

    @@ -97,7 +97,7 @@ export default function Developers() {

    To access private API endpoints, you must provide a valid API key. - You can create new API keys in your{' '} + You can create new API keys in your{" "} account settings. Be careful when handling your keys as they give access to all of your documents. @@ -119,7 +119,7 @@ export default function Developers() {

    Errors

    - All successful API requests will be returned with 200{' '} + All successful API requests will be returned with 200{" "} status code and ok: true in the response payload. If there’s an error while making the request, appropriate status code is returned with the error message: diff --git a/server/policies/apiKey.js b/server/policies/apiKey.js index 68c49d08..45db56b6 100644 --- a/server/policies/apiKey.js +++ b/server/policies/apiKey.js @@ -1,14 +1,14 @@ // @flow -import policy from './policy'; -import { ApiKey, User } from '../models'; +import policy from "./policy"; +import { ApiKey, User } from "../models"; const { allow } = policy; -allow(User, 'create', ApiKey); +allow(User, "create", ApiKey); allow( User, - ['read', 'update', 'delete'], + ["read", "update", "delete"], ApiKey, (user, apiKey) => user && user.id === apiKey.userId ); diff --git a/server/policies/collection.js b/server/policies/collection.js index e9d1052a..7147fb29 100644 --- a/server/policies/collection.js +++ b/server/policies/collection.js @@ -1,21 +1,21 @@ // @flow -import invariant from 'invariant'; -import policy from './policy'; -import { concat, some } from 'lodash'; -import { Collection, User } from '../models'; -import { AdminRequiredError } from '../errors'; +import invariant from "invariant"; +import policy from "./policy"; +import { concat, some } from "lodash"; +import { Collection, User } from "../models"; +import { AdminRequiredError } from "../errors"; const { allow } = policy; -allow(User, 'create', Collection); +allow(User, "create", Collection); -allow(User, ['read', 'export'], Collection, (user, collection) => { +allow(User, ["read", "export"], Collection, (user, collection) => { if (!collection || user.teamId !== collection.teamId) return false; if (collection.private) { invariant( collection.memberships, - 'membership should be preloaded, did you forget withMembership scope?' + "membership should be preloaded, did you forget withMembership scope?" ); const allMemberships = concat( @@ -24,20 +24,20 @@ allow(User, ['read', 'export'], Collection, (user, collection) => { ); return some(allMemberships, m => - ['read', 'read_write', 'maintainer'].includes(m.permission) + ["read", "read_write", "maintainer"].includes(m.permission) ); } return true; }); -allow(User, ['publish', 'update'], Collection, (user, collection) => { +allow(User, ["publish", "update"], Collection, (user, collection) => { if (!collection || user.teamId !== collection.teamId) return false; if (collection.private) { invariant( collection.memberships, - 'membership should be preloaded, did you forget withMembership scope?' + "membership should be preloaded, did you forget withMembership scope?" ); const allMemberships = concat( @@ -46,20 +46,20 @@ allow(User, ['publish', 'update'], Collection, (user, collection) => { ); return some(allMemberships, m => - ['read_write', 'maintainer'].includes(m.permission) + ["read_write", "maintainer"].includes(m.permission) ); } return true; }); -allow(User, 'delete', Collection, (user, collection) => { +allow(User, "delete", Collection, (user, collection) => { if (!collection || user.teamId !== collection.teamId) return false; if (collection.private) { invariant( collection.memberships, - 'membership should be preloaded, did you forget withMembership scope?' + "membership should be preloaded, did you forget withMembership scope?" ); const allMemberships = concat( collection.memberships, @@ -67,7 +67,7 @@ allow(User, 'delete', Collection, (user, collection) => { ); return some(allMemberships, m => - ['read_write', 'maintainer'].includes(m.permission) + ["read_write", "maintainer"].includes(m.permission) ); } diff --git a/server/policies/document.js b/server/policies/document.js index 4bf4ac87..fb5578ec 100644 --- a/server/policies/document.js +++ b/server/policies/document.js @@ -1,89 +1,89 @@ // @flow -import invariant from 'invariant'; -import policy from './policy'; -import { Document, Revision, User } from '../models'; +import invariant from "invariant"; +import policy from "./policy"; +import { Document, Revision, User } from "../models"; const { allow, cannot } = policy; -allow(User, 'create', Document); +allow(User, "create", Document); -allow(User, ['read', 'download'], Document, (user, document) => { +allow(User, ["read", "download"], Document, (user, document) => { // existance of collection option is not required here to account for share tokens - if (document.collection && cannot(user, 'read', document.collection)) { + if (document.collection && cannot(user, "read", document.collection)) { return false; } return user.teamId === document.teamId; }); -allow(User, ['share'], Document, (user, document) => { +allow(User, ["share"], Document, (user, document) => { if (document.archivedAt) return false; if (document.deletedAt) return false; // existance of collection option is not required here to account for share tokens - if (document.collection && cannot(user, 'read', document.collection)) { + if (document.collection && cannot(user, "read", document.collection)) { return false; } return user.teamId === document.teamId; }); -allow(User, ['star', 'unstar'], Document, (user, document) => { +allow(User, ["star", "unstar"], Document, (user, document) => { if (document.archivedAt) return false; if (document.deletedAt) return false; if (!document.publishedAt) return false; invariant( document.collection, - 'collection is missing, did you forget to include in the query scope?' + "collection is missing, did you forget to include in the query scope?" ); - if (cannot(user, 'read', document.collection)) return false; + if (cannot(user, "read", document.collection)) return false; return user.teamId === document.teamId; }); -allow(User, 'update', Document, (user, document) => { +allow(User, "update", Document, (user, document) => { if (document.archivedAt) return false; if (document.deletedAt) return false; invariant( document.collection, - 'collection is missing, did you forget to include in the query scope?' + "collection is missing, did you forget to include in the query scope?" ); - if (cannot(user, 'update', document.collection)) return false; + if (cannot(user, "update", document.collection)) return false; return user.teamId === document.teamId; }); -allow(User, 'createChildDocument', Document, (user, document) => { +allow(User, "createChildDocument", Document, (user, document) => { if (document.archivedAt) return false; if (document.archivedAt) return false; if (!document.publishedAt) return false; invariant( document.collection, - 'collection is missing, did you forget to include in the query scope?' + "collection is missing, did you forget to include in the query scope?" ); - if (cannot(user, 'update', document.collection)) return false; + if (cannot(user, "update", document.collection)) return false; return user.teamId === document.teamId; }); -allow(User, ['move', 'pin', 'unpin'], Document, (user, document) => { +allow(User, ["move", "pin", "unpin"], Document, (user, document) => { if (document.archivedAt) return false; if (document.deletedAt) return false; if (!document.publishedAt) return false; invariant( document.collection, - 'collection is missing, did you forget to include in the query scope?' + "collection is missing, did you forget to include in the query scope?" ); - if (cannot(user, 'update', document.collection)) return false; + if (cannot(user, "update", document.collection)) return false; return user.teamId === document.teamId; }); -allow(User, 'delete', Document, (user, document) => { +allow(User, "delete", Document, (user, document) => { // unpublished drafts can always be deleted if ( !document.deletedAt && @@ -94,7 +94,7 @@ allow(User, 'delete', Document, (user, document) => { } // allow deleting document without a collection - if (document.collection && cannot(user, 'update', document.collection)) { + if (document.collection && cannot(user, "update", document.collection)) { return false; } @@ -103,17 +103,17 @@ allow(User, 'delete', Document, (user, document) => { return user.teamId === document.teamId; }); -allow(User, 'restore', Document, (user, document) => { +allow(User, "restore", Document, (user, document) => { if (!document.deletedAt) return false; return user.teamId === document.teamId; }); -allow(User, 'archive', Document, (user, document) => { +allow(User, "archive", Document, (user, document) => { invariant( document.collection, - 'collection is missing, did you forget to include in the query scope?' + "collection is missing, did you forget to include in the query scope?" ); - if (cannot(user, 'update', document.collection)) return false; + if (cannot(user, "update", document.collection)) return false; if (!document.publishedAt) return false; if (document.archivedAt) return false; @@ -122,12 +122,12 @@ allow(User, 'archive', Document, (user, document) => { return user.teamId === document.teamId; }); -allow(User, 'unarchive', Document, (user, document) => { +allow(User, "unarchive", Document, (user, document) => { invariant( document.collection, - 'collection is missing, did you forget to include in the query scope?' + "collection is missing, did you forget to include in the query scope?" ); - if (cannot(user, 'update', document.collection)) return false; + if (cannot(user, "update", document.collection)) return false; if (!document.archivedAt) return false; @@ -136,7 +136,7 @@ allow(User, 'unarchive', Document, (user, document) => { allow( Document, - 'restore', + "restore", Revision, (document, revision) => document.id === revision.documentId ); diff --git a/server/policies/group.js b/server/policies/group.js index 58e23066..a8cc124d 100644 --- a/server/policies/group.js +++ b/server/policies/group.js @@ -1,22 +1,22 @@ // @flow -import policy from './policy'; -import { Group, User } from '../models'; -import { AdminRequiredError } from '../errors'; +import policy from "./policy"; +import { Group, User } from "../models"; +import { AdminRequiredError } from "../errors"; const { allow } = policy; -allow(User, ['create'], Group, actor => { +allow(User, ["create"], Group, actor => { if (actor.isAdmin) return true; throw new AdminRequiredError(); }); -allow(User, ['update', 'delete'], Group, (actor, group) => { +allow(User, ["update", "delete"], Group, (actor, group) => { if (!group || actor.teamId !== group.teamId) return false; if (actor.isAdmin) return true; throw new AdminRequiredError(); }); -allow(User, ['read'], Group, (actor, group) => { +allow(User, ["read"], Group, (actor, group) => { if (!group || actor.teamId !== group.teamId) return false; if (actor.isAdmin) return true; if (group.groupMemberships.filter(gm => gm.userId === actor.id).length) { diff --git a/server/policies/index.js b/server/policies/index.js index 8f390a4e..351fe148 100644 --- a/server/policies/index.js +++ b/server/policies/index.js @@ -1,15 +1,15 @@ // @flow -import { Team, User, Collection, Document, Group } from '../models'; -import policy from './policy'; -import './apiKey'; -import './collection'; -import './document'; -import './integration'; -import './notificationSetting'; -import './share'; -import './user'; -import './team'; -import './group'; +import { Team, User, Collection, Document, Group } from "../models"; +import policy from "./policy"; +import "./apiKey"; +import "./collection"; +import "./document"; +import "./integration"; +import "./notificationSetting"; +import "./share"; +import "./user"; +import "./team"; +import "./group"; const { can, abilities } = policy; diff --git a/server/policies/index.test.js b/server/policies/index.test.js index 7b4fa9cb..cb88194d 100644 --- a/server/policies/index.test.js +++ b/server/policies/index.test.js @@ -1,11 +1,11 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import { flushdb } from '../test/support'; -import { buildUser } from '../test/factories'; -import { serialize } from './index'; +import { flushdb } from "../test/support"; +import { buildUser } from "../test/factories"; +import { serialize } from "./index"; beforeEach(flushdb); -it('should serialize policy', async () => { +it("should serialize policy", async () => { const user = await buildUser(); const response = serialize(user, user); expect(response.update).toEqual(true); diff --git a/server/policies/integration.js b/server/policies/integration.js index 5a94ed89..4917e848 100644 --- a/server/policies/integration.js +++ b/server/policies/integration.js @@ -1,20 +1,20 @@ // @flow -import policy from './policy'; -import { Integration, User } from '../models'; -import { AdminRequiredError } from '../errors'; +import policy from "./policy"; +import { Integration, User } from "../models"; +import { AdminRequiredError } from "../errors"; const { allow } = policy; -allow(User, 'create', Integration); +allow(User, "create", Integration); allow( User, - 'read', + "read", Integration, (user, integration) => user.teamId === integration.teamId ); -allow(User, ['update', 'delete'], Integration, (user, integration) => { +allow(User, ["update", "delete"], Integration, (user, integration) => { if (!integration || user.teamId !== integration.teamId) return false; if (user.isAdmin) return true; throw new AdminRequiredError(); diff --git a/server/policies/notificationSetting.js b/server/policies/notificationSetting.js index 3b02db5e..8a03c2b6 100644 --- a/server/policies/notificationSetting.js +++ b/server/policies/notificationSetting.js @@ -1,14 +1,14 @@ // @flow -import policy from './policy'; -import { NotificationSetting, User } from '../models'; +import policy from "./policy"; +import { NotificationSetting, User } from "../models"; const { allow } = policy; -allow(User, 'create', NotificationSetting); +allow(User, "create", NotificationSetting); allow( User, - ['read', 'update', 'delete'], + ["read", "update", "delete"], NotificationSetting, (user, setting) => user && user.id === setting.userId ); diff --git a/server/policies/policy.js b/server/policies/policy.js index a9535176..ca48f792 100644 --- a/server/policies/policy.js +++ b/server/policies/policy.js @@ -1,3 +1,3 @@ // @flow -import CanCan from 'cancan'; +import CanCan from "cancan"; export default new CanCan(); diff --git a/server/policies/share.js b/server/policies/share.js index 62151ed0..19c82ea1 100644 --- a/server/policies/share.js +++ b/server/policies/share.js @@ -1,13 +1,13 @@ // @flow -import policy from './policy'; -import { Share, User } from '../models'; -import { AdminRequiredError } from '../errors'; +import policy from "./policy"; +import { Share, User } from "../models"; +import { AdminRequiredError } from "../errors"; const { allow } = policy; -allow(User, ['read'], Share, (user, share) => user.teamId === share.teamId); -allow(User, ['update'], Share, (user, share) => false); -allow(User, ['revoke'], Share, (user, share) => { +allow(User, ["read"], Share, (user, share) => user.teamId === share.teamId); +allow(User, ["update"], Share, (user, share) => false); +allow(User, ["revoke"], Share, (user, share) => { if (!share || user.teamId !== share.teamId) return false; if (user.id === share.userId) return true; if (user.isAdmin) return true; diff --git a/server/policies/team.js b/server/policies/team.js index c8c34d57..c7b04885 100644 --- a/server/policies/team.js +++ b/server/policies/team.js @@ -1,34 +1,34 @@ // @flow -import policy from './policy'; -import { Team, User } from '../models'; -import { AdminRequiredError } from '../errors'; +import policy from "./policy"; +import { Team, User } from "../models"; +import { AdminRequiredError } from "../errors"; const { allow } = policy; -allow(User, 'read', Team, (user, team) => team && user.teamId === team.id); +allow(User, "read", Team, (user, team) => team && user.teamId === team.id); -allow(User, 'share', Team, (user, team) => { +allow(User, "share", Team, (user, team) => { if (!team || user.teamId !== team.id) return false; return team.sharing; }); -allow(User, 'auditLog', Team, user => { +allow(User, "auditLog", Team, user => { if (user.isAdmin) return true; return false; }); -allow(User, 'invite', Team, user => { +allow(User, "invite", Team, user => { if (user.isAdmin) return true; return false; }); // ??? policy for creating new groups, I don't know how to do this other than on the team level -allow(User, 'group', Team, user => { +allow(User, "group", Team, user => { if (user.isAdmin) return true; throw new AdminRequiredError(); }); -allow(User, ['update', 'export'], Team, (user, team) => { +allow(User, ["update", "export"], Team, (user, team) => { if (!team || user.teamId !== team.id) return false; if (user.isAdmin) return true; throw new AdminRequiredError(); diff --git a/server/policies/user.js b/server/policies/user.js index 1fa3eeb1..43e78cf8 100644 --- a/server/policies/user.js +++ b/server/policies/user.js @@ -1,28 +1,28 @@ // @flow -import policy from './policy'; -import { User } from '../models'; -import { AdminRequiredError } from '../errors'; +import policy from "./policy"; +import { User } from "../models"; +import { AdminRequiredError } from "../errors"; const { allow } = policy; allow( User, - 'read', + "read", User, (actor, user) => user && user.teamId === actor.teamId ); -allow(User, 'invite', User, actor => { +allow(User, "invite", User, actor => { return true; }); -allow(User, 'update', User, (actor, user) => { +allow(User, "update", User, (actor, user) => { if (!user || user.teamId !== actor.teamId) return false; if (user.id === actor.id) return true; throw new AdminRequiredError(); }); -allow(User, 'delete', User, (actor, user) => { +allow(User, "delete", User, (actor, user) => { if (!user || user.teamId !== actor.teamId) return false; if (user.id === actor.id) return true; if (actor.isAdmin && !user.lastActiveAt) return true; @@ -31,7 +31,7 @@ allow(User, 'delete', User, (actor, user) => { allow( User, - ['promote', 'demote', 'activate', 'suspend'], + ["promote", "demote", "activate", "suspend"], User, (actor, user) => { if (!user || user.teamId !== actor.teamId) return false; diff --git a/server/presenters/apiKey.js b/server/presenters/apiKey.js index 212acca1..8ef7dfc2 100644 --- a/server/presenters/apiKey.js +++ b/server/presenters/apiKey.js @@ -1,5 +1,5 @@ // @flow -import { ApiKey } from '../models'; +import { ApiKey } from "../models"; export default function present(key: ApiKey) { return { diff --git a/server/presenters/collection.js b/server/presenters/collection.js index 2f88ee8c..91372cff 100644 --- a/server/presenters/collection.js +++ b/server/presenters/collection.js @@ -1,6 +1,6 @@ // @flow -import { Collection } from '../models'; -import naturalSort from '../../shared/utils/naturalSort'; +import { Collection } from "../models"; +import naturalSort from "../../shared/utils/naturalSort"; type Document = { children: Document[], @@ -10,7 +10,7 @@ type Document = { }; const sortDocuments = (documents: Document[]): Document[] => { - const orderedDocs = naturalSort(documents, 'title'); + const orderedDocs = naturalSort(documents, "title"); return orderedDocs.map(document => ({ ...document, @@ -25,7 +25,7 @@ export default function present(collection: Collection) { name: collection.name, description: collection.description, icon: collection.icon, - color: collection.color || '#4E5C6E', + color: collection.color || "#4E5C6E", type: collection.type, private: collection.private, createdAt: collection.createdAt, @@ -34,7 +34,7 @@ export default function present(collection: Collection) { documents: undefined, }; - if (collection.type === 'atlas') { + if (collection.type === "atlas") { // Force alphabetical sorting data.documents = sortDocuments(collection.documentStructure); } diff --git a/server/presenters/collectionGroupMembership.js b/server/presenters/collectionGroupMembership.js index abefe773..d9213929 100644 --- a/server/presenters/collectionGroupMembership.js +++ b/server/presenters/collectionGroupMembership.js @@ -1,5 +1,5 @@ // @flow -import { CollectionGroup } from '../models'; +import { CollectionGroup } from "../models"; type Membership = { id: string, diff --git a/server/presenters/document.js b/server/presenters/document.js index 764249be..82b3c6d7 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -1,8 +1,8 @@ // @flow -import { takeRight } from 'lodash'; -import { User, Document, Attachment } from '../models'; -import { getSignedImageUrl } from '../utils/s3'; -import presentUser from './user'; +import { takeRight } from "lodash"; +import { User, Document, Attachment } from "../models"; +import { getSignedImageUrl } from "../utils/s3"; +import presentUser from "./user"; type Options = { isPublic?: boolean, diff --git a/server/presenters/event.js b/server/presenters/event.js index 67e59640..32fafbb5 100644 --- a/server/presenters/event.js +++ b/server/presenters/event.js @@ -1,6 +1,6 @@ // @flow -import { Event } from '../models'; -import presentUser from './user'; +import { Event } from "../models"; +import presentUser from "./user"; export default function present(event: Event, auditLog: boolean = false) { let data = { diff --git a/server/presenters/group.js b/server/presenters/group.js index e60cccb8..79410c79 100644 --- a/server/presenters/group.js +++ b/server/presenters/group.js @@ -1,5 +1,5 @@ // @flow -import { Group } from '../models'; +import { Group } from "../models"; export default function present(group: Group) { return { diff --git a/server/presenters/groupMembership.js b/server/presenters/groupMembership.js index ac1b038e..0c89df65 100644 --- a/server/presenters/groupMembership.js +++ b/server/presenters/groupMembership.js @@ -1,6 +1,6 @@ // @flow -import { GroupUser } from '../models'; -import { presentUser } from '.'; +import { GroupUser } from "../models"; +import { presentUser } from "."; type GroupMembership = { id: string, diff --git a/server/presenters/index.js b/server/presenters/index.js index 370214ac..270be632 100644 --- a/server/presenters/index.js +++ b/server/presenters/index.js @@ -1,21 +1,21 @@ // @flow -import presentUser from './user'; -import presentView from './view'; -import presentDocument from './document'; -import presentEvent from './event'; -import presentRevision from './revision'; -import presentCollection from './collection'; -import presentApiKey from './apiKey'; -import presentShare from './share'; -import presentTeam from './team'; -import presentIntegration from './integration'; -import presentMembership from './membership'; -import presentNotificationSetting from './notificationSetting'; -import presentSlackAttachment from './slackAttachment'; -import presentPolicies from './policy'; -import presentGroup from './group'; -import presentGroupMembership from './groupMembership'; -import presentCollectionGroupMembership from './collectionGroupMembership'; +import presentUser from "./user"; +import presentView from "./view"; +import presentDocument from "./document"; +import presentEvent from "./event"; +import presentRevision from "./revision"; +import presentCollection from "./collection"; +import presentApiKey from "./apiKey"; +import presentShare from "./share"; +import presentTeam from "./team"; +import presentIntegration from "./integration"; +import presentMembership from "./membership"; +import presentNotificationSetting from "./notificationSetting"; +import presentSlackAttachment from "./slackAttachment"; +import presentPolicies from "./policy"; +import presentGroup from "./group"; +import presentGroupMembership from "./groupMembership"; +import presentCollectionGroupMembership from "./collectionGroupMembership"; export { presentUser, diff --git a/server/presenters/integration.js b/server/presenters/integration.js index 1cb49e9e..f1829078 100644 --- a/server/presenters/integration.js +++ b/server/presenters/integration.js @@ -1,5 +1,5 @@ // @flow -import { Integration } from '../models'; +import { Integration } from "../models"; export default function present(integration: Integration) { return { diff --git a/server/presenters/membership.js b/server/presenters/membership.js index 88c46f8d..17302a30 100644 --- a/server/presenters/membership.js +++ b/server/presenters/membership.js @@ -1,5 +1,5 @@ // @flow -import { CollectionUser } from '../models'; +import { CollectionUser } from "../models"; type Membership = { id: string, diff --git a/server/presenters/notificationSetting.js b/server/presenters/notificationSetting.js index f1063e8f..84c3f775 100644 --- a/server/presenters/notificationSetting.js +++ b/server/presenters/notificationSetting.js @@ -1,5 +1,5 @@ // @flow -import { NotificationSetting } from '../models'; +import { NotificationSetting } from "../models"; export default function present(setting: NotificationSetting) { return { diff --git a/server/presenters/policy.js b/server/presenters/policy.js index 46a87701..11160310 100644 --- a/server/presenters/policy.js +++ b/server/presenters/policy.js @@ -1,10 +1,10 @@ // @flow -import { User } from '../models'; +import { User } from "../models"; type Policy = { id: string, abilities: { [key: string]: boolean } }; export default function present(user: User, objects: Object[]): Policy[] { - const { serialize } = require('../policies'); + const { serialize } = require("../policies"); return objects.map(object => ({ id: object.id, diff --git a/server/presenters/revision.js b/server/presenters/revision.js index 04e364b2..3c4a4df6 100644 --- a/server/presenters/revision.js +++ b/server/presenters/revision.js @@ -1,6 +1,6 @@ // @flow -import { Revision } from '../models'; -import presentUser from './user'; +import { Revision } from "../models"; +import presentUser from "./user"; export default async function present(revision: Revision) { await revision.migrateVersion(); diff --git a/server/presenters/share.js b/server/presenters/share.js index a8e089eb..4d9d1c0f 100644 --- a/server/presenters/share.js +++ b/server/presenters/share.js @@ -1,6 +1,6 @@ // @flow -import { Share } from '../models'; -import { presentUser } from '.'; +import { Share } from "../models"; +import { presentUser } from "."; export default function present(share: Share) { return { diff --git a/server/presenters/slackAttachment.js b/server/presenters/slackAttachment.js index 18eef625..70b1de31 100644 --- a/server/presenters/slackAttachment.js +++ b/server/presenters/slackAttachment.js @@ -1,5 +1,5 @@ // @flow -import { Document, Collection, Team } from '../models'; +import { Document, Collection, Team } from "../models"; type Action = { type: string, @@ -18,7 +18,7 @@ export default function present( // the context contains tags around search terms, we convert them here // to the markdown format that slack expects to receive. const text = context - ? context.replace(/<\/?b>/g, '*').replace('\n', '') + ? context.replace(/<\/?b>/g, "*").replace("\n", "") : document.getSummary(); return { diff --git a/server/presenters/team.js b/server/presenters/team.js index e981ba97..c0b2b369 100644 --- a/server/presenters/team.js +++ b/server/presenters/team.js @@ -1,5 +1,5 @@ // @flow -import { Team } from '../models'; +import { Team } from "../models"; export default function present(team: Team) { return { diff --git a/server/presenters/user.js b/server/presenters/user.js index 24e49240..a0bf2071 100644 --- a/server/presenters/user.js +++ b/server/presenters/user.js @@ -1,5 +1,5 @@ // @flow -import { User } from '../models'; +import { User } from "../models"; type Options = { includeDetails?: boolean, diff --git a/server/presenters/user.test.js b/server/presenters/user.test.js index 9a888f03..fece314c 100644 --- a/server/presenters/user.test.js +++ b/server/presenters/user.test.js @@ -1,24 +1,24 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import presentUser from './user'; +import presentUser from "./user"; -it('presents a user', async () => { +it("presents a user", async () => { const user = presentUser({ - id: '123', - name: 'Test User', - username: 'testuser', + id: "123", + name: "Test User", + username: "testuser", slackData: { - image_192: 'http://example.com/avatar.png', + image_192: "http://example.com/avatar.png", }, }); expect(user).toMatchSnapshot(); }); -it('presents a user without slack data', async () => { +it("presents a user without slack data", async () => { const user = presentUser({ - id: '123', - name: 'Test User', - username: 'testuser', + id: "123", + name: "Test User", + username: "testuser", slackData: null, }); diff --git a/server/presenters/view.js b/server/presenters/view.js index ac6c8baa..3b210d77 100644 --- a/server/presenters/view.js +++ b/server/presenters/view.js @@ -1,6 +1,6 @@ // @flow -import { View } from '../models'; -import { presentUser } from '../presenters'; +import { View } from "../models"; +import { presentUser } from "../presenters"; export default function present(view: View) { return { diff --git a/server/redis.js b/server/redis.js index 3cc60c3a..2b58feb0 100644 --- a/server/redis.js +++ b/server/redis.js @@ -1,5 +1,5 @@ // @flow -import Redis from 'ioredis'; +import Redis from "ioredis"; const client = new Redis(process.env.REDIS_URL); const subscriber = new Redis(process.env.REDIS_URL); diff --git a/server/routes.js b/server/routes.js index ec03a562..0fdffa37 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1,69 +1,69 @@ // @flow -import * as React from 'react'; -import path from 'path'; -import Koa from 'koa'; -import Router from 'koa-router'; -import sendfile from 'koa-sendfile'; -import serve from 'koa-static'; -import apexRedirect from './middlewares/apexRedirect'; -import renderpage from './utils/renderpage'; -import { isCustomSubdomain, parseDomain } from '../shared/utils/domains'; -import { robotsResponse } from './utils/robots'; -import { opensearchResponse } from './utils/opensearch'; -import { NotFoundError } from './errors'; -import { Team } from './models'; +import * as React from "react"; +import path from "path"; +import Koa from "koa"; +import Router from "koa-router"; +import sendfile from "koa-sendfile"; +import serve from "koa-static"; +import apexRedirect from "./middlewares/apexRedirect"; +import renderpage from "./utils/renderpage"; +import { isCustomSubdomain, parseDomain } from "../shared/utils/domains"; +import { robotsResponse } from "./utils/robots"; +import { opensearchResponse } from "./utils/opensearch"; +import { NotFoundError } from "./errors"; +import { Team } from "./models"; -import Home from './pages/Home'; -import Developers from './pages/developers'; -import Api from './pages/developers/Api'; -import SubdomainSignin from './pages/SubdomainSignin'; +import Home from "./pages/Home"; +import Developers from "./pages/developers"; +import Api from "./pages/developers/Api"; +import SubdomainSignin from "./pages/SubdomainSignin"; -const isProduction = process.env.NODE_ENV === 'production'; +const isProduction = process.env.NODE_ENV === "production"; const koa = new Koa(); const router = new Router(); const renderapp = async ctx => { if (isProduction) { - await sendfile(ctx, path.join(__dirname, '../dist/index.html')); + await sendfile(ctx, path.join(__dirname, "../dist/index.html")); } else { - await sendfile(ctx, path.join(__dirname, './static/dev.html')); + await sendfile(ctx, path.join(__dirname, "./static/dev.html")); } }; // serve static assets koa.use( - serve(path.resolve(__dirname, '../public'), { + serve(path.resolve(__dirname, "../public"), { maxage: 60 * 60 * 24 * 30 * 1000, }) ); -router.get('/_health', ctx => (ctx.body = 'OK')); +router.get("/_health", ctx => (ctx.body = "OK")); -if (process.env.NODE_ENV === 'production') { - router.get('/static/*', async ctx => { +if (process.env.NODE_ENV === "production") { + router.get("/static/*", async ctx => { ctx.set({ - 'Cache-Control': `max-age=${356 * 24 * 60 * 60}`, + "Cache-Control": `max-age=${356 * 24 * 60 * 60}`, }); await sendfile( ctx, - path.join(__dirname, '../dist/', ctx.path.substring(8)) + path.join(__dirname, "../dist/", ctx.path.substring(8)) ); }); } // static pages -router.get('/developers', ctx => renderpage(ctx, )); -router.get('/developers/api', ctx => renderpage(ctx, )); +router.get("/developers", ctx => renderpage(ctx, )); +router.get("/developers/api", ctx => renderpage(ctx, )); // home page -router.get('/', async ctx => { - const lastSignedIn = ctx.cookies.get('lastSignedIn'); - const accessToken = ctx.cookies.get('accessToken'); +router.get("/", async ctx => { + const lastSignedIn = ctx.cookies.get("lastSignedIn"); + const accessToken = ctx.cookies.get("accessToken"); // Because we render both the signed in and signed out views depending // on a cookie it's important that the browser does not render from cache. - ctx.set('Cache-Control', 'no-cache'); + ctx.set("Cache-Control", "no-cache"); // If we have an accessToken we can just go ahead and render the app – if // the accessToken turns out to be invalid the user will be redirected. @@ -74,7 +74,7 @@ router.get('/', async ctx => { // If we're on a custom subdomain then we display a slightly different signed // out view that includes the teams basic information. if ( - process.env.SUBDOMAINS_ENABLED === 'true' && + process.env.SUBDOMAINS_ENABLED === "true" && isCustomSubdomain(ctx.request.hostname) ) { const domain = parseDomain(ctx.request.hostname); @@ -113,18 +113,18 @@ router.get('/', async ctx => { ); }); -router.get('/robots.txt', ctx => { +router.get("/robots.txt", ctx => { ctx.body = robotsResponse(ctx); }); -router.get('/opensearch.xml', ctx => { - ctx.type = 'text/xml'; +router.get("/opensearch.xml", ctx => { + ctx.type = "text/xml"; ctx.body = opensearchResponse(); }); // catch all for react app -router.get('*', async (ctx, next) => { - if (ctx.request.path === '/realtime/') return next(); +router.get("*", async (ctx, next) => { + if (ctx.request.path === "/realtime/") return next(); await renderapp(ctx); if (!ctx.status) ctx.throw(new NotFoundError()); diff --git a/server/routes.test.js b/server/routes.test.js index 3fdf84dc..ce572e07 100644 --- a/server/routes.test.js +++ b/server/routes.test.js @@ -1,24 +1,24 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import TestServer from 'fetch-test-server'; -import app from './app'; -import { flushdb } from './test/support'; +import TestServer from "fetch-test-server"; +import app from "./app"; +import { flushdb } from "./test/support"; const server = new TestServer(app.callback()); beforeEach(flushdb); afterAll(server.close); -describe('#index', async () => { - it('should render homepage', async () => { - const res = await server.get('/'); +describe("#index", async () => { + it("should render homepage", async () => { + const res = await server.get("/"); const html = await res.text(); expect(res.status).toEqual(200); - expect(html.includes('Our team’s knowledge base')).toEqual(true); + expect(html.includes("Our team’s knowledge base")).toEqual(true); }); - it('should render app if there is an accessToken', async () => { - const res = await server.get('/', { - headers: { Cookie: ['accessToken=12345667'] }, + it("should render app if there is an accessToken", async () => { + const res = await server.get("/", { + headers: { Cookie: ["accessToken=12345667"] }, }); const html = await res.text(); expect(res.status).toEqual(200); diff --git a/server/sequelize.js b/server/sequelize.js index fbcf4975..c064f901 100644 --- a/server/sequelize.js +++ b/server/sequelize.js @@ -1,7 +1,7 @@ // @flow -import Sequelize from 'sequelize'; -import EncryptedField from 'sequelize-encrypted'; -import debug from 'debug'; +import Sequelize from "sequelize"; +import EncryptedField from "sequelize-encrypted"; +import debug from "debug"; export const encryptedFields = EncryptedField( Sequelize, @@ -13,6 +13,6 @@ export const Op = Sequelize.Op; export const sequelize = new Sequelize(process.env.DATABASE_URL, { // logging: console.log, - logging: debug('sql'), + logging: debug("sql"), typeValidation: true, }); diff --git a/server/services/backlinks.js b/server/services/backlinks.js index 5acf7ca6..20398160 100644 --- a/server/services/backlinks.js +++ b/server/services/backlinks.js @@ -1,14 +1,14 @@ // @flow -import { difference } from 'lodash'; -import type { DocumentEvent } from '../events'; -import { Document, Revision, Backlink } from '../models'; -import parseDocumentIds from '../../shared/utils/parseDocumentIds'; -import slugify from '../utils/slugify'; +import { difference } from "lodash"; +import type { DocumentEvent } from "../events"; +import { Document, Revision, Backlink } from "../models"; +import parseDocumentIds from "../../shared/utils/parseDocumentIds"; +import slugify from "../utils/slugify"; export default class Backlinks { async on(event: DocumentEvent) { switch (event.name) { - case 'documents.publish': { + case "documents.publish": { const document = await Document.findByPk(event.documentId); const linkIds = parseDocumentIds(document.text); @@ -31,7 +31,7 @@ export default class Backlinks { break; } - case 'documents.update': { + case "documents.update": { // no-op for now if (event.data.autosave) return; @@ -41,7 +41,7 @@ export default class Backlinks { const [currentRevision, previousRevision] = await Revision.findAll({ where: { documentId: event.documentId }, - order: [['createdAt', 'desc']], + order: [["createdAt", "desc"]], limit: 2, }); const previousLinkIds = previousRevision @@ -98,7 +98,7 @@ export default class Backlinks { where: { documentId: event.documentId, }, - include: [{ model: Document, as: 'reverseDocument' }], + include: [{ model: Document, as: "reverseDocument" }], }); await Promise.all( @@ -123,7 +123,7 @@ export default class Backlinks { break; } - case 'documents.delete': { + case "documents.delete": { await Backlink.destroy({ where: { reverseDocumentId: event.documentId, diff --git a/server/services/backlinks.test.js b/server/services/backlinks.test.js index a2520aab..e5a193bf 100644 --- a/server/services/backlinks.test.js +++ b/server/services/backlinks.test.js @@ -1,23 +1,23 @@ /* eslint-disable flowtype/require-valid-file-annotation */ -import { flushdb } from '../test/support'; -import BacklinksService from './backlinks'; -import { buildDocument } from '../test/factories'; -import Backlink from '../models/Backlink'; +import { flushdb } from "../test/support"; +import BacklinksService from "./backlinks"; +import { buildDocument } from "../test/factories"; +import Backlink from "../models/Backlink"; const Backlinks = new BacklinksService(); beforeEach(flushdb); beforeEach(jest.resetAllMocks); -describe('documents.update', () => { - test('should not fail on a document with no previous revisions', async () => { +describe("documents.update", () => { + test("should not fail on a document with no previous revisions", async () => { const otherDocument = await buildDocument(); const document = await buildDocument({ text: `[this is a link](${otherDocument.url})`, }); await Backlinks.on({ - name: 'documents.update', + name: "documents.update", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -32,7 +32,7 @@ describe('documents.update', () => { expect(backlinks.length).toBe(1); }); - test('should create new backlink records', async () => { + test("should create new backlink records", async () => { const otherDocument = await buildDocument(); const document = await buildDocument(); @@ -40,7 +40,7 @@ describe('documents.update', () => { await document.save(); await Backlinks.on({ - name: 'documents.update', + name: "documents.update", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -55,14 +55,14 @@ describe('documents.update', () => { expect(backlinks.length).toBe(1); }); - test('should destroy removed backlink records', async () => { + test("should destroy removed backlink records", async () => { const otherDocument = await buildDocument(); const document = await buildDocument({ text: `[this is a link](${otherDocument.url})`, }); await Backlinks.on({ - name: 'documents.publish', + name: "documents.publish", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -70,11 +70,11 @@ describe('documents.update', () => { data: { autosave: false }, }); - document.text = 'Link is gone'; + document.text = "Link is gone"; await document.save(); await Backlinks.on({ - name: 'documents.update', + name: "documents.update", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -89,8 +89,8 @@ describe('documents.update', () => { expect(backlinks.length).toBe(0); }); - test('should update titles in backlinked documents', async () => { - const newTitle = 'test'; + test("should update titles in backlinked documents", async () => { + const newTitle = "test"; const document = await buildDocument(); const otherDocument = await buildDocument(); @@ -100,7 +100,7 @@ describe('documents.update', () => { // ensure the backlinks are created await Backlinks.on({ - name: 'documents.update', + name: "documents.update", documentId: document.id, collectionId: document.collectionId, teamId: document.teamId, @@ -114,7 +114,7 @@ describe('documents.update', () => { // does the text get updated with the new title await Backlinks.on({ - name: 'documents.update', + name: "documents.update", documentId: otherDocument.id, collectionId: otherDocument.collectionId, teamId: otherDocument.teamId, diff --git a/server/services/index.js b/server/services/index.js index e4ff4049..eca6537c 100644 --- a/server/services/index.js +++ b/server/services/index.js @@ -1,22 +1,22 @@ // @flow -import debug from 'debug'; -import fs from 'fs-extra'; -import path from 'path'; +import debug from "debug"; +import fs from "fs-extra"; +import path from "path"; -const log = debug('services'); +const log = debug("services"); const services = {}; fs .readdirSync(__dirname) .filter( file => - file.indexOf('.') !== 0 && + file.indexOf(".") !== 0 && file !== path.basename(__filename) && - !file.includes('.test') + !file.includes(".test") ) .forEach(fileName => { const servicePath = path.join(__dirname, fileName); - const name = path.basename(servicePath.replace(/\.js$/, '')); + const name = path.basename(servicePath.replace(/\.js$/, "")); // $FlowIssue const Service = require(servicePath).default; services[name] = new Service(); diff --git a/server/services/notifications.js b/server/services/notifications.js index badc321a..21f6dad5 100644 --- a/server/services/notifications.js +++ b/server/services/notifications.js @@ -1,22 +1,22 @@ // @flow -import { Op } from '../sequelize'; -import type { DocumentEvent, CollectionEvent, Event } from '../events'; +import { Op } from "../sequelize"; +import type { DocumentEvent, CollectionEvent, Event } from "../events"; import { Document, Team, Collection, User, NotificationSetting, -} from '../models'; -import mailer from '../mailer'; +} from "../models"; +import mailer from "../mailer"; export default class Notifications { async on(event: Event) { switch (event.name) { - case 'documents.publish': - case 'documents.update': + case "documents.publish": + case "documents.update": return this.documentUpdated(event); - case 'collections.create': + case "collections.create": return this.collectionCreated(event); default: } @@ -50,20 +50,20 @@ export default class Notifications { { model: User, required: true, - as: 'user', + as: "user", }, ], }); const eventName = - event.name === 'documents.publish' ? 'published' : 'updated'; + event.name === "documents.publish" ? "published" : "updated"; notificationSettings.forEach(setting => { // For document updates we only want to send notifications if // the document has been edited by the user with this notification setting // This could be replaced with ability to "follow" in the future if ( - event.name === 'documents.update' && + event.name === "documents.update" && !document.collaboratorIds.includes(setting.userId) ) { return; @@ -87,7 +87,7 @@ export default class Notifications { { model: User, required: true, - as: 'user', + as: "user", }, ], }); @@ -106,7 +106,7 @@ export default class Notifications { { model: User, required: true, - as: 'user', + as: "user", }, ], }); @@ -114,7 +114,7 @@ export default class Notifications { notificationSettings.forEach(setting => mailer.collectionNotification({ to: setting.user.email, - eventName: 'created', + eventName: "created", collection, actor: collection.user, unsubscribeUrl: setting.unsubscribeUrl, diff --git a/server/services/slack.js b/server/services/slack.js index 1649dede..7fdcb5f7 100644 --- a/server/services/slack.js +++ b/server/services/slack.js @@ -1,15 +1,15 @@ // @flow -import type { DocumentEvent, IntegrationEvent, Event } from '../events'; -import { Document, Integration, Collection, Team } from '../models'; -import { presentSlackAttachment } from '../presenters'; +import type { DocumentEvent, IntegrationEvent, Event } from "../events"; +import { Document, Integration, Collection, Team } from "../models"; +import { presentSlackAttachment } from "../presenters"; export default class Slack { async on(event: Event) { switch (event.name) { - case 'documents.publish': - case 'documents.update': + case "documents.publish": + case "documents.update": return this.documentUpdated(event); - case 'integrations.create': + case "integrations.create": return this.integrationCreated(event); default: } @@ -19,14 +19,14 @@ export default class Slack { const integration = await Integration.findOne({ where: { id: event.modelId, - service: 'slack', - type: 'post', + service: "slack", + type: "post", }, include: [ { model: Collection, required: true, - as: 'collection', + as: "collection", }, ], }); @@ -36,9 +36,9 @@ export default class Slack { if (!collection) return; await fetch(integration.settings.url, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ text: `👋 Hey there! When documents are published or updated in the *${ @@ -59,7 +59,7 @@ export default class Slack { async documentUpdated(event: DocumentEvent) { // lets not send a notification on every autosave update if ( - event.name === 'documents.update' && + event.name === "documents.update" && event.data && event.data.autosave ) { @@ -67,7 +67,7 @@ export default class Slack { } // lets not send a notification on every CMD+S update - if (event.name === 'documents.update' && event.data && !event.data.done) { + if (event.name === "documents.update" && event.data && !event.data.done) { return; } @@ -81,8 +81,8 @@ export default class Slack { where: { teamId: document.teamId, collectionId: document.collectionId, - service: 'slack', - type: 'post', + service: "slack", + type: "post", }, }); if (!integration) return; @@ -91,14 +91,14 @@ export default class Slack { let text = `${document.createdBy.name} published a new document`; - if (event.name === 'documents.update') { + if (event.name === "documents.update") { text = `${document.updatedBy.name} updated a document`; } await fetch(integration.settings.url, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ text, diff --git a/server/services/websockets.js b/server/services/websockets.js index b35484f2..95adbef1 100644 --- a/server/services/websockets.js +++ b/server/services/websockets.js @@ -1,32 +1,32 @@ // @flow -import type { Event } from '../events'; +import type { Event } from "../events"; import { Document, Collection, Group, CollectionGroup, GroupUser, -} from '../models'; -import { socketio } from '../'; -import { Op } from '../sequelize'; -import subHours from 'date-fns/sub_hours'; +} from "../models"; +import { socketio } from "../"; +import { Op } from "../sequelize"; +import subHours from "date-fns/sub_hours"; export default class Websockets { async on(event: Event) { - if (process.env.WEBSOCKETS_ENABLED !== 'true' || !socketio) return; + if (process.env.WEBSOCKETS_ENABLED !== "true" || !socketio) return; switch (event.name) { - case 'documents.publish': - case 'documents.restore': - case 'documents.archive': - case 'documents.unarchive': { + case "documents.publish": + case "documents.restore": + case "documents.archive": + case "documents.unarchive": { const document = await Document.findByPk(event.documentId, { paranoid: false, }); return socketio .to(`collection-${document.collectionId}`) - .emit('entities', { + .emit("entities", { event: event.name, documentIds: [ { @@ -41,13 +41,13 @@ export default class Websockets { ], }); } - case 'documents.delete': { + case "documents.delete": { const document = await Document.findByPk(event.documentId, { paranoid: false, }); if (!document.publishedAt) { - return socketio.to(`user-${document.createdById}`).emit('entities', { + return socketio.to(`user-${document.createdById}`).emit("entities", { event: event.name, documentIds: [ { @@ -60,7 +60,7 @@ export default class Websockets { return socketio .to(`collection-${document.collectionId}`) - .emit('entities', { + .emit("entities", { event: event.name, documentIds: [ { @@ -75,16 +75,16 @@ export default class Websockets { ], }); } - case 'documents.pin': - case 'documents.unpin': - case 'documents.update': { + case "documents.pin": + case "documents.unpin": + case "documents.update": { const document = await Document.findByPk(event.documentId, { paranoid: false, }); return socketio .to(`collection-${document.collectionId}`) - .emit('entities', { + .emit("entities", { event: event.name, documentIds: [ { @@ -94,10 +94,10 @@ export default class Websockets { ], }); } - case 'documents.create': { + case "documents.create": { const document = await Document.findByPk(event.documentId); - return socketio.to(`user-${event.actorId}`).emit('entities', { + return socketio.to(`user-${event.actorId}`).emit("entities", { event: event.name, documentIds: [ { @@ -112,13 +112,13 @@ export default class Websockets { ], }); } - case 'documents.star': - case 'documents.unstar': { + case "documents.star": + case "documents.unstar": { return socketio.to(`user-${event.actorId}`).emit(event.name, { documentId: event.documentId, }); } - case 'documents.move': { + case "documents.move": { const documents = await Document.findAll({ where: { id: event.data.documentIds, @@ -126,7 +126,7 @@ export default class Websockets { paranoid: false, }); documents.forEach(document => { - socketio.to(`collection-${document.collectionId}`).emit('entities', { + socketio.to(`collection-${document.collectionId}`).emit("entities", { event: event.name, documentIds: [ { @@ -137,14 +137,14 @@ export default class Websockets { }); }); event.data.collectionIds.forEach(collectionId => { - socketio.to(`collection-${collectionId}`).emit('entities', { + socketio.to(`collection-${collectionId}`).emit("entities", { event: event.name, collectionIds: [{ id: collectionId }], }); }); return; } - case 'collections.create': { + case "collections.create": { const collection = await Collection.findByPk(event.collectionId, { paranoid: false, }); @@ -155,7 +155,7 @@ export default class Websockets { ? `collection-${collection.id}` : `team-${collection.teamId}` ) - .emit('entities', { + .emit("entities", { event: event.name, collectionIds: [ { @@ -170,18 +170,18 @@ export default class Websockets { ? `collection-${collection.id}` : `team-${collection.teamId}` ) - .emit('join', { + .emit("join", { event: event.name, collectionId: collection.id, }); } - case 'collections.update': - case 'collections.delete': { + case "collections.update": + case "collections.delete": { const collection = await Collection.findByPk(event.collectionId, { paranoid: false, }); - return socketio.to(`team-${collection.teamId}`).emit('entities', { + return socketio.to(`team-${collection.teamId}`).emit("entities", { event: event.name, collectionIds: [ { @@ -191,7 +191,7 @@ export default class Websockets { ], }); } - case 'collections.add_user': { + case "collections.add_user": { // the user being added isn't yet in the websocket channel for the collection // so they need to be notified separately socketio.to(`user-${event.userId}`).emit(event.name, { @@ -208,12 +208,12 @@ export default class Websockets { }); // tell any user clients to connect to the websocket channel for the collection - return socketio.to(`user-${event.userId}`).emit('join', { + return socketio.to(`user-${event.userId}`).emit("join", { event: event.name, collectionId: event.collectionId, }); } - case 'collections.remove_user': { + case "collections.remove_user": { const membershipUserIds = await Collection.membershipUserIds( event.collectionId ); @@ -222,8 +222,8 @@ export default class Websockets { // Even though we just removed a user from the collection // the user still has access through some means // treat this like an add, so that the client re-syncs policies - socketio.to(`user-${event.userId}`).emit('collections.add_user', { - event: 'collections.add_user', + socketio.to(`user-${event.userId}`).emit("collections.add_user", { + event: "collections.add_user", userId: event.userId, collectionId: event.collectionId, }); @@ -231,21 +231,21 @@ export default class Websockets { // let everyone with access to the collection know a user was removed socketio .to(`collection-${event.collectionId}`) - .emit('collections.remove_user', { + .emit("collections.remove_user", { event: event.name, userId: event.userId, collectionId: event.collectionId, }); // tell any user clients to disconnect from the websocket channel for the collection - socketio.to(`user-${event.userId}`).emit('leave', { + socketio.to(`user-${event.userId}`).emit("leave", { event: event.name, collectionId: event.collectionId, }); } return; } - case 'collections.add_group': { + case "collections.add_group": { const group = await Group.findByPk(event.data.groupId); // the users being added are not yet in the websocket channel for the collection @@ -253,21 +253,21 @@ export default class Websockets { for (const groupMembership of group.groupMemberships) { socketio .to(`user-${groupMembership.userId}`) - .emit('collections.add_user', { + .emit("collections.add_user", { event: event.name, userId: groupMembership.userId, collectionId: event.collectionId, }); // tell any user clients to connect to the websocket channel for the collection - socketio.to(`user-${groupMembership.userId}`).emit('join', { + socketio.to(`user-${groupMembership.userId}`).emit("join", { event: event.name, collectionId: event.collectionId, }); } return; } - case 'collections.remove_group': { + case "collections.remove_group": { const group = await Group.findByPk(event.data.groupId); const membershipUserIds = await Collection.membershipUserIds( event.collectionId @@ -279,7 +279,7 @@ export default class Websockets { // treat this like an add, so that the client re-syncs policies socketio .to(`user-${groupMembership.userId}`) - .emit('collections.add_user', { + .emit("collections.add_user", { event: event.name, userId: groupMembership.userId, collectionId: event.collectionId, @@ -288,14 +288,14 @@ export default class Websockets { // let users in the channel know they were removed socketio .to(`user-${groupMembership.userId}`) - .emit('collections.remove_user', { + .emit("collections.remove_user", { event: event.name, userId: groupMembership.userId, collectionId: event.collectionId, }); // tell any user clients to disconnect to the websocket channel for the collection - socketio.to(`user-${groupMembership.userId}`).emit('leave', { + socketio.to(`user-${groupMembership.userId}`).emit("leave", { event: event.name, collectionId: event.collectionId, }); @@ -303,13 +303,13 @@ export default class Websockets { } return; } - case 'groups.create': - case 'groups.update': { + case "groups.create": + case "groups.update": { const group = await Group.findByPk(event.modelId, { paranoid: false, }); - return socketio.to(`team-${group.teamId}`).emit('entities', { + return socketio.to(`team-${group.teamId}`).emit("entities", { event: event.name, groupIds: [ { @@ -319,7 +319,7 @@ export default class Websockets { ], }); } - case 'groups.add_user': { + case "groups.add_user": { // do an add user for every collection that the group is a part of const collectionGroupMemberships = await CollectionGroup.findAll({ where: { groupId: event.modelId }, @@ -328,7 +328,7 @@ export default class Websockets { for (const collectionGroup of collectionGroupMemberships) { // the user being added isn't yet in the websocket channel for the collection // so they need to be notified separately - socketio.to(`user-${event.userId}`).emit('collections.add_user', { + socketio.to(`user-${event.userId}`).emit("collections.add_user", { event: event.name, userId: event.userId, collectionId: collectionGroup.collectionId, @@ -337,21 +337,21 @@ export default class Websockets { // let everyone with access to the collection know a user was added socketio .to(`collection-${collectionGroup.collectionId}`) - .emit('collections.add_user', { + .emit("collections.add_user", { event: event.name, userId: event.userId, collectionId: collectionGroup.collectionId, }); // tell any user clients to connect to the websocket channel for the collection - return socketio.to(`user-${event.userId}`).emit('join', { + return socketio.to(`user-${event.userId}`).emit("join", { event: event.name, collectionId: collectionGroup.collectionId, }); } return; } - case 'groups.remove_user': { + case "groups.remove_user": { const collectionGroupMemberships = await CollectionGroup.findAll({ where: { groupId: event.modelId }, }); @@ -360,7 +360,7 @@ export default class Websockets { // if the user has any memberships remaining on the collection // we need to emit add instead of remove const collection = await Collection.scope({ - method: ['withMembership', event.userId], + method: ["withMembership", event.userId], }).findByPk(collectionGroup.collectionId); const hasMemberships = @@ -370,7 +370,7 @@ export default class Websockets { if (hasMemberships) { // the user still has access through some means... // treat this like an add, so that the client re-syncs policies - socketio.to(`user-${event.userId}`).emit('collections.add_user', { + socketio.to(`user-${event.userId}`).emit("collections.add_user", { event: event.name, userId: event.userId, collectionId: collectionGroup.collectionId, @@ -379,14 +379,14 @@ export default class Websockets { // let everyone with access to the collection know a user was removed socketio .to(`collection-${collectionGroup.collectionId}`) - .emit('collections.remove_user', { + .emit("collections.remove_user", { event: event.name, userId: event.userId, collectionId: collectionGroup.collectionId, }); // tell any user clients to disconnect from the websocket channel for the collection - socketio.to(`user-${event.userId}`).emit('leave', { + socketio.to(`user-${event.userId}`).emit("leave", { event: event.name, collectionId: collectionGroup.collectionId, }); @@ -394,12 +394,12 @@ export default class Websockets { } return; } - case 'groups.delete': { + case "groups.delete": { const group = await Group.findByPk(event.modelId, { paranoid: false, }); - socketio.to(`team-${group.teamId}`).emit('entities', { + socketio.to(`team-${group.teamId}`).emit("entities", { event: event.name, groupIds: [ { @@ -443,7 +443,7 @@ export default class Websockets { // treat this like an add, so that the client re-syncs policies socketio .to(`user-${groupUser.userId}`) - .emit('collections.add_user', { + .emit("collections.add_user", { event: event.name, userId: groupUser.userId, collectionId: collectionGroup.collectionId, @@ -452,14 +452,14 @@ export default class Websockets { // let everyone with access to the collection know a user was removed socketio .to(`collection-${collectionGroup.collectionId}`) - .emit('collections.remove_user', { + .emit("collections.remove_user", { event: event.name, userId: groupUser.userId, collectionId: collectionGroup.collectionId, }); // tell any user clients to disconnect from the websocket channel for the collection - socketio.to(`user-${groupUser.userId}`).emit('leave', { + socketio.to(`user-${groupUser.userId}`).emit("leave", { event: event.name, collectionId: collectionGroup.collectionId, }); diff --git a/server/slack.js b/server/slack.js index d33ebf48..91f9a7f6 100644 --- a/server/slack.js +++ b/server/slack.js @@ -1,9 +1,9 @@ // @flow -import fetch from 'isomorphic-fetch'; -import querystring from 'querystring'; -import { InvalidRequestError } from './errors'; +import fetch from "isomorphic-fetch"; +import querystring from "querystring"; +import { InvalidRequestError } from "./errors"; -const SLACK_API_URL = 'https://slack.com/api'; +const SLACK_API_URL = "https://slack.com/api"; export async function post(endpoint: string, body: Object) { let data; @@ -11,10 +11,10 @@ export async function post(endpoint: string, body: Object) { const token = body.token; try { const response = await fetch(`${SLACK_API_URL}/${endpoint}`, { - method: 'POST', + method: "POST", headers: { Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(body), }); @@ -44,9 +44,9 @@ export async function request(endpoint: string, body: Object) { export async function oauthAccess( code: string, - redirect_uri: string = `${process.env.URL || ''}/auth/slack.callback` + redirect_uri: string = `${process.env.URL || ""}/auth/slack.callback` ) { - return request('oauth.access', { + return request("oauth.access", { client_id: process.env.SLACK_KEY, client_secret: process.env.SLACK_SECRET, redirect_uri, diff --git a/server/test/factories.js b/server/test/factories.js index f0493224..2709adb4 100644 --- a/server/test/factories.js +++ b/server/test/factories.js @@ -9,8 +9,8 @@ import { Group, GroupUser, Attachment, -} from '../models'; -import uuid from 'uuid'; +} from "../models"; +import uuid from "uuid"; let count = 0; @@ -39,8 +39,8 @@ export function buildTeam(overrides: Object = {}) { export function buildEvent(overrides: Object = {}) { return Event.create({ - name: 'documents.publish', - ip: '127.0.0.1', + name: "documents.publish", + ip: "127.0.0.1", ...overrides, }); } @@ -57,10 +57,10 @@ export async function buildUser(overrides: Object = {}) { email: `user${count}@example.com`, username: `user${count}`, name: `User ${count}`, - service: 'slack', + service: "slack", serviceId: uuid.v4(), - createdAt: new Date('2018-01-01T00:00:00.000Z'), - lastActiveAt: new Date('2018-01-01T00:00:00.000Z'), + createdAt: new Date("2018-01-01T00:00:00.000Z"), + lastActiveAt: new Date("2018-01-01T00:00:00.000Z"), ...overrides, }); } @@ -80,9 +80,9 @@ export async function buildCollection(overrides: Object = {}) { return Collection.create({ name: `Test Collection ${count}`, - description: 'Test collection description', + description: "Test collection description", creatorId: overrides.userId, - type: 'atlas', + type: "atlas", ...overrides, }); } @@ -146,7 +146,7 @@ export async function buildDocument(overrides: Object = {}) { return Document.create({ title: `Document ${count}`, - text: 'This is the text in an example document', + text: "This is the text in an example document", publishedAt: new Date(), lastModifiedById: overrides.userId, createdById: overrides.userId, @@ -180,11 +180,11 @@ export async function buildAttachment(overrides: Object = {}) { return Attachment.create({ key: `uploads/key/to/file ${count}.png`, url: `https://redirect.url.com/uploads/key/to/file ${count}.png`, - contentType: 'image/png', + contentType: "image/png", size: 100, - acl: 'public-read', - createdAt: new Date('2018-01-02T00:00:00.000Z'), - updatedAt: new Date('2018-01-02T00:00:00.000Z'), + acl: "public-read", + createdAt: new Date("2018-01-02T00:00:00.000Z"), + updatedAt: new Date("2018-01-02T00:00:00.000Z"), ...overrides, }); } diff --git a/server/test/helper.js b/server/test/helper.js index c7436c5d..eb6bc197 100644 --- a/server/test/helper.js +++ b/server/test/helper.js @@ -1,26 +1,26 @@ // @flow /* global jest */ -require('dotenv').config({ silent: true }); +require("dotenv").config({ silent: true }); // test environment variables process.env.DATABASE_URL = process.env.DATABASE_URL_TEST; -process.env.NODE_ENV = 'test'; +process.env.NODE_ENV = "test"; -const Sequelize = require('sequelize'); -const sequelize = require('../sequelize').sequelize; -const Umzug = require('umzug'); +const Sequelize = require("sequelize"); +const sequelize = require("../sequelize").sequelize; +const Umzug = require("umzug"); const queryInterface = sequelize.getQueryInterface(); function runMigrations() { const umzug = new Umzug({ - storage: 'sequelize', + storage: "sequelize", storageOptions: { sequelize, }, migrations: { params: [queryInterface, Sequelize], - path: './server/migrations', + path: "./server/migrations", }, }); return umzug.up(); @@ -30,4 +30,4 @@ runMigrations(); // This is needed for the relative manual mock to be picked up // $FlowFixMe -jest.mock('../events'); +jest.mock("../events"); diff --git a/server/test/support.js b/server/test/support.js index dcac37d9..2e1a0139 100644 --- a/server/test/support.js +++ b/server/test/support.js @@ -1,66 +1,66 @@ // @flow -import { User, Document, Collection, Team } from '../models'; -import { sequelize } from '../sequelize'; +import { User, Document, Collection, Team } from "../models"; +import { sequelize } from "../sequelize"; export function flushdb() { const sql = sequelize.getQueryInterface(); const tables = Object.keys(sequelize.models).map(model => { const n = sequelize.models[model].getTableName(); - return sql.quoteTable(typeof n === 'string' ? n : n.tableName); + return sql.quoteTable(typeof n === "string" ? n : n.tableName); }); - const query = `TRUNCATE ${tables.join(', ')} CASCADE`; + const query = `TRUNCATE ${tables.join(", ")} CASCADE`; return sequelize.query(query); } const seed = async () => { const team = await Team.create({ - id: '86fde1d4-0050-428f-9f0b-0bf77f8bdf61', - name: 'Team', - slackId: 'T2399UF2P', + id: "86fde1d4-0050-428f-9f0b-0bf77f8bdf61", + name: "Team", + slackId: "T2399UF2P", slackData: { - id: 'T2399UF2P', + id: "T2399UF2P", }, }); const admin = await User.create({ - id: 'fa952cff-fa64-4d42-a6ea-6955c9689046', - email: 'admin@example.com', - username: 'admin', - name: 'Admin User', + id: "fa952cff-fa64-4d42-a6ea-6955c9689046", + email: "admin@example.com", + username: "admin", + name: "Admin User", teamId: team.id, isAdmin: true, - service: 'slack', - serviceId: 'U2399UF1P', + service: "slack", + serviceId: "U2399UF1P", slackData: { - id: 'U2399UF1P', - image_192: 'http://example.com/avatar.png', + id: "U2399UF1P", + image_192: "http://example.com/avatar.png", }, - createdAt: new Date('2018-01-01T00:00:00.000Z'), + createdAt: new Date("2018-01-01T00:00:00.000Z"), }); const user = await User.create({ - id: '46fde1d4-0050-428f-9f0b-0bf77f4bdf61', - email: 'user1@example.com', - username: 'user1', - name: 'User 1', + id: "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", + email: "user1@example.com", + username: "user1", + name: "User 1", teamId: team.id, - service: 'slack', - serviceId: 'U2399UF2P', + service: "slack", + serviceId: "U2399UF2P", slackData: { - id: 'U2399UF2P', - image_192: 'http://example.com/avatar.png', + id: "U2399UF2P", + image_192: "http://example.com/avatar.png", }, - createdAt: new Date('2018-01-02T00:00:00.000Z'), + createdAt: new Date("2018-01-02T00:00:00.000Z"), }); const collection = await Collection.create({ - id: '26fde1d4-0050-428f-9f0b-0bf77f8bdf62', - name: 'Collection', - urlId: 'collection', + id: "26fde1d4-0050-428f-9f0b-0bf77f8bdf62", + name: "Collection", + urlId: "collection", teamId: team.id, creatorId: user.id, - type: 'atlas', + type: "atlas", }); const document = await Document.create({ @@ -70,8 +70,8 @@ const seed = async () => { userId: collection.creatorId, lastModifiedById: collection.creatorId, createdById: collection.creatorId, - title: 'First ever document', - text: '# Much test support', + title: "First ever document", + text: "# Much test support", }); await document.publish(); await collection.reload(); diff --git a/server/utils/jwt.js b/server/utils/jwt.js index d10cab2e..08a30c9f 100644 --- a/server/utils/jwt.js +++ b/server/utils/jwt.js @@ -1,19 +1,19 @@ // @flow -import JWT from 'jsonwebtoken'; -import subMinutes from 'date-fns/sub_minutes'; -import { AuthenticationError } from '../errors'; -import { User } from '../models'; +import JWT from "jsonwebtoken"; +import subMinutes from "date-fns/sub_minutes"; +import { AuthenticationError } from "../errors"; +import { User } from "../models"; function getJWTPayload(token) { let payload; try { payload = JWT.decode(token); } catch (err) { - throw new AuthenticationError('Unable to decode JWT token'); + throw new AuthenticationError("Unable to decode JWT token"); } if (!payload) { - throw new AuthenticationError('Invalid token'); + throw new AuthenticationError("Invalid token"); } return payload; } @@ -25,7 +25,7 @@ export async function getUserForJWT(token: string) { try { JWT.verify(token, user.jwtSecret); } catch (err) { - throw new AuthenticationError('Invalid token'); + throw new AuthenticationError("Invalid token"); } return user; @@ -37,7 +37,7 @@ export async function getUserForEmailSigninToken(token: string) { // check the token is within it's expiration time if (payload.createdAt) { if (new Date(payload.createdAt) < subMinutes(new Date(), 10)) { - throw new AuthenticationError('Expired token'); + throw new AuthenticationError("Expired token"); } } @@ -46,13 +46,13 @@ export async function getUserForEmailSigninToken(token: string) { // if user has signed in at all since the token was created then // it's no longer valid, they'll need a new one. if (user.lastSignedInAt > payload.createdAt) { - throw new AuthenticationError('Token has already been used'); + throw new AuthenticationError("Token has already been used"); } try { JWT.verify(token, user.jwtSecret); } catch (err) { - throw new AuthenticationError('Invalid token'); + throw new AuthenticationError("Invalid token"); } return user; diff --git a/server/utils/prefetchTags.js b/server/utils/prefetchTags.js index 29a488b7..e2c5ba99 100644 --- a/server/utils/prefetchTags.js +++ b/server/utils/prefetchTags.js @@ -1,8 +1,8 @@ // @flow -import * as React from 'react'; -import fs from 'fs'; -import path from 'path'; -import webpackConfig from '../../webpack.config'; +import * as React from "react"; +import fs from "fs"; +import path from "path"; +import webpackConfig from "../../webpack.config"; const PUBLIC_PATH = webpackConfig.output.publicPath; @@ -16,13 +16,13 @@ const prefetchTags = [ try { const manifest = fs.readFileSync( - path.join(__dirname, '../../dist/manifest.json'), - 'utf8' + path.join(__dirname, "../../dist/manifest.json"), + "utf8" ); const manifestData = JSON.parse(manifest); Object.values(manifestData).forEach(filename => { - if (typeof filename !== 'string') return; - if (filename.endsWith('.js')) { + if (typeof filename !== "string") return; + if (filename.endsWith(".js")) { prefetchTags.push( ); - } else if (filename.endsWith('.css')) { + } else if (filename.endsWith(".css")) { prefetchTags.push( (head += helmet[key].toString())); ctx.body = `\n${html}` - .replace('{{CSS}}', sheet.getStyleTags()) - .replace('{{HEAD}}', head); + .replace("{{CSS}}", sheet.getStyleTags()) + .replace("{{HEAD}}", head); } diff --git a/server/utils/robots.js b/server/utils/robots.js index f7e08c8e..9905335c 100644 --- a/server/utils/robots.js +++ b/server/utils/robots.js @@ -1,9 +1,9 @@ // @flow -import { type Context } from 'koa'; +import { type Context } from "koa"; const DISALLOW_ROBOTS = `User-agent: * Disallow: /`; export const robotsResponse = (ctx: Context): ?string => { - if (ctx.headers.host.indexOf('getoutline.com') < 0) return DISALLOW_ROBOTS; + if (ctx.headers.host.indexOf("getoutline.com") < 0) return DISALLOW_ROBOTS; }; diff --git a/server/utils/s3.js b/server/utils/s3.js index 77903335..ee4245f4 100644 --- a/server/utils/s3.js +++ b/server/utils/s3.js @@ -1,11 +1,11 @@ // @flow -import crypto from 'crypto'; -import addHours from 'date-fns/add_hours'; -import format from 'date-fns/format'; -import AWS from 'aws-sdk'; -import invariant from 'invariant'; -import fetch from 'isomorphic-fetch'; -import * as Sentry from '@sentry/node'; +import crypto from "crypto"; +import addHours from "date-fns/add_hours"; +import format from "date-fns/format"; +import AWS from "aws-sdk"; +import invariant from "invariant"; +import fetch from "isomorphic-fetch"; +import * as Sentry from "@sentry/node"; const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; @@ -17,24 +17,24 @@ const s3 = new AWS.S3({ accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, endpoint: new AWS.Endpoint(process.env.AWS_S3_UPLOAD_BUCKET_URL), - signatureVersion: 'v4', + signatureVersion: "v4", }); const hmac = (key: string, message: string, encoding: any) => { return crypto - .createHmac('sha256', key) - .update(message, 'utf8') + .createHmac("sha256", key) + .update(message, "utf8") .digest(encoding); }; export const makeCredential = () => { const credential = AWS_ACCESS_KEY_ID + - '/' + - format(new Date(), 'YYYYMMDD') + - '/' + + "/" + + format(new Date(), "YYYYMMDD") + + "/" + AWS_REGION + - '/s3/aws4_request'; + "/s3/aws4_request"; return credential; }; @@ -47,31 +47,31 @@ export const makePolicy = ( const policy = { conditions: [ { bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME }, - ['starts-with', '$key', ''], + ["starts-with", "$key", ""], { acl }, - ['content-length-range', 0, +process.env.AWS_S3_UPLOAD_MAX_SIZE], - ['starts-with', '$Content-Type', 'image'], - ['starts-with', '$Cache-Control', ''], - { 'x-amz-algorithm': 'AWS4-HMAC-SHA256' }, - { 'x-amz-credential': credential }, - { 'x-amz-date': longDate }, + ["content-length-range", 0, +process.env.AWS_S3_UPLOAD_MAX_SIZE], + ["starts-with", "$Content-Type", "image"], + ["starts-with", "$Cache-Control", ""], + { "x-amz-algorithm": "AWS4-HMAC-SHA256" }, + { "x-amz-credential": credential }, + { "x-amz-date": longDate }, ], - expiration: format(tomorrow, 'YYYY-MM-DDTHH:mm:ss\\Z'), + expiration: format(tomorrow, "YYYY-MM-DDTHH:mm:ss\\Z"), }; - return new Buffer(JSON.stringify(policy)).toString('base64'); + return new Buffer(JSON.stringify(policy)).toString("base64"); }; export const getSignature = (policy: any) => { const kDate = hmac( - 'AWS4' + AWS_SECRET_ACCESS_KEY, - format(new Date(), 'YYYYMMDD') + "AWS4" + AWS_SECRET_ACCESS_KEY, + format(new Date(), "YYYYMMDD") ); const kRegion = hmac(kDate, AWS_REGION); - const kService = hmac(kRegion, 's3'); - const kCredentials = hmac(kService, 'aws4_request'); + const kService = hmac(kRegion, "s3"); + const kCredentials = hmac(kService, "aws4_request"); - const signature = hmac(kCredentials, policy, 'hex'); + const signature = hmac(kCredentials, policy, "hex"); return signature; }; @@ -80,11 +80,11 @@ export const publicS3Endpoint = (isServerUpload?: boolean) => { // for access outside of docker containers in local development const isDocker = process.env.AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/); const host = process.env.AWS_S3_UPLOAD_BUCKET_URL.replace( - 's3:', - 'localhost:' - ).replace(/\/$/, ''); + "s3:", + "localhost:" + ).replace(/\/$/, ""); - return `${host}/${isServerUpload && isDocker ? 's3/' : ''}${ + return `${host}/${isServerUpload && isDocker ? "s3/" : ""}${ process.env.AWS_S3_UPLOAD_BUCKET_NAME }`; }; @@ -94,7 +94,7 @@ export const uploadToS3FromUrl = async ( key: string, acl: string ) => { - invariant(AWS_S3_UPLOAD_BUCKET_NAME, 'AWS_S3_UPLOAD_BUCKET_NAME not set'); + invariant(AWS_S3_UPLOAD_BUCKET_NAME, "AWS_S3_UPLOAD_BUCKET_NAME not set"); try { // $FlowIssue https://github.com/facebook/flow/issues/2171 @@ -105,9 +105,9 @@ export const uploadToS3FromUrl = async ( ACL: acl, Bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME, Key: key, - ContentType: res.headers['content-type'], - ContentLength: res.headers['content-length'], - ServerSideEncryption: 'AES256', + ContentType: res.headers["content-type"], + ContentLength: res.headers["content-length"], + ServerSideEncryption: "AES256", Body: buffer, }) .promise(); @@ -133,7 +133,7 @@ export const deleteFromS3 = (key: string) => { }; export const getSignedImageUrl = async (key: string) => { - invariant(AWS_S3_UPLOAD_BUCKET_NAME, 'AWS_S3_UPLOAD_BUCKET_NAME not set'); + invariant(AWS_S3_UPLOAD_BUCKET_NAME, "AWS_S3_UPLOAD_BUCKET_NAME not set"); const isDocker = process.env.AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/); const params = { @@ -144,7 +144,7 @@ export const getSignedImageUrl = async (key: string) => { return isDocker ? `${publicS3Endpoint()}/${key}` - : s3.getSignedUrl('getObject', params); + : s3.getSignedUrl("getObject", params); }; export const getImageByKey = async (key: string) => { diff --git a/server/utils/slugify.js b/server/utils/slugify.js index e8243b7e..a3cfb404 100644 --- a/server/utils/slugify.js +++ b/server/utils/slugify.js @@ -1,7 +1,7 @@ // @flow -import slug from 'slug'; +import slug from "slug"; -slug.defaults.mode = 'rfc3986'; +slug.defaults.mode = "rfc3986"; export default function slugify(text: string): string { return slug(text, { diff --git a/server/utils/updates.js b/server/utils/updates.js index a730dac3..3aaf114c 100644 --- a/server/utils/updates.js +++ b/server/utils/updates.js @@ -1,25 +1,25 @@ // @flow -import crypto from 'crypto'; -import invariant from 'invariant'; -import fetch from 'isomorphic-fetch'; -import { client } from '../redis'; -import packageInfo from '../../package.json'; +import crypto from "crypto"; +import invariant from "invariant"; +import fetch from "isomorphic-fetch"; +import { client } from "../redis"; +import packageInfo from "../../package.json"; -import { User, Team, Collection, Document } from '../models'; +import { User, Team, Collection, Document } from "../models"; -const UPDATES_URL = 'https://updates.getoutline.com'; -const UPDATES_KEY = 'UPDATES_KEY'; +const UPDATES_URL = "https://updates.getoutline.com"; +const UPDATES_KEY = "UPDATES_KEY"; export default async () => { invariant( process.env.SECRET_KEY && process.env.URL, - 'SECRET_KEY or URL env var is not set' + "SECRET_KEY or URL env var is not set" ); const secret = process.env.SECRET_KEY.slice(0, 6) + process.env.URL; const id = crypto - .createHash('sha256') + .createHash("sha256") .update(secret) - .digest('hex'); + .digest("hex"); const [ userCount, @@ -45,14 +45,14 @@ export default async () => { }, }); - await client.del('UPDATES_KEY'); + await client.del("UPDATES_KEY"); try { const response = await fetch(UPDATES_URL, { - method: 'POST', + method: "POST", headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', + Accept: "application/json", + "Content-Type": "application/json", }, body, }); diff --git a/server/utils/zip.js b/server/utils/zip.js index b2713221..3956a0e4 100644 --- a/server/utils/zip.js +++ b/server/utils/zip.js @@ -1,10 +1,10 @@ // @flow -import fs from 'fs'; -import JSZip from 'jszip'; -import tmp from 'tmp'; -import * as Sentry from '@sentry/node'; -import { Attachment, Collection, Document } from '../models'; -import { getImageByKey } from './s3'; +import fs from "fs"; +import JSZip from "jszip"; +import tmp from "tmp"; +import * as Sentry from "@sentry/node"; +import { Attachment, Collection, Document } from "../models"; +import { getImageByKey } from "./s3"; async function addToArchive(zip, documents) { for (const doc of documents) { @@ -20,7 +20,7 @@ async function addToArchive(zip, documents) { text = text.replace(attachment.redirectUrl, encodeURI(attachment.key)); } - zip.file(`${document.title || 'Untitled'}.md`, text); + zip.file(`${document.title || "Untitled"}.md`, text); if (doc.children && doc.children.length) { const folder = zip.folder(document.title); @@ -44,14 +44,14 @@ async function addImageToArchive(zip, key) { async function archiveToPath(zip) { return new Promise((resolve, reject) => { - tmp.file({ prefix: 'export-', postfix: '.zip' }, (err, path) => { + tmp.file({ prefix: "export-", postfix: ".zip" }, (err, path) => { if (err) return reject(err); zip - .generateNodeStream({ type: 'nodebuffer', streamFiles: true }) + .generateNodeStream({ type: "nodebuffer", streamFiles: true }) .pipe(fs.createWriteStream(path)) - .on('finish', () => resolve(path)) - .on('error', reject); + .on("finish", () => resolve(path)) + .on("error", reject); }); }); }