Compare commits

...

67 Commits

Author SHA1 Message Date
Tom Moor cd2cdd025c 0.47.1 2020-09-11 12:26:13 -07:00
Tom Moor d5f5319f80 fix: History sidebar header shrinking when lots of history 2020-09-11 12:10:40 -07:00
Tom Moor c298c73240 fix: Revision skipped after identical previous autosave 2020-09-11 12:06:57 -07:00
Tom Moor 38d1831259 fix: Send events for draft documents down user channel always 2020-09-11 11:19:30 -07:00
Tom Moor 9c9b95741c fix: Unable to edit collection description
closes #1523
2020-09-09 23:23:01 -07:00
Tom Moor cc8db7e991 fix: Save team logo automatically
closes #1521
2020-09-08 21:12:37 -07:00
Tom Moor c5b7d9be13 fix: Flash of sidebar when navigating between documents 2020-09-08 20:48:52 -07:00
Tom Moor be2e46b5d2 References -> Referenced by
Less ambiguous as to the backlink direction
2020-09-08 18:44:19 -07:00
Tom Moor 4b2a766357 fix: Missing toast message when successfully copying code 2020-09-08 07:31:03 -07:00
Tom Moor f264d67862 fix: Two API endpoints where requesting a permenantly deleted document results in server error 2020-09-08 07:29:06 -07:00
Tom Moor b1648ac2aa Update LICENSE 2020-09-07 20:25:16 -07:00
Tom Moor 25423d8c85 0.47.0 2020-09-07 20:24:40 -07:00
Nan Yu e7ab2939d4 fix: Improved handling of delete events from collection and document sockets (#1517)
* handle delete events fron collection and document sockets

* handle collection deletes to documents

* rework semantics to always reload after a delete

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-09-07 19:05:10 -07:00
Tom Moor ceeac9b982 fix: Deeply nested document titles not updated in collection documentStructure cache
closes #1508
2020-09-07 13:41:17 -07:00
Tom Moor 5de2f969e3 fix: Preload membership 2020-09-07 12:17:04 -07:00
Tom Moor b54901d50c fix: CMD+Eneter in title should still publish 2020-09-07 12:06:03 -07:00
Tom Moor 4de3f69474 fix: Documents in deleted collection should appear in trash (#1362)
* fix: Documents from deleted collection should show in trash

* improve messaging

* test: Add documents.move tests
feat: Add ability to restore trashed documents from deleted collection

* update store

* fix

* ui

* lint

* fix: Improve breadcrumb
2020-09-07 11:51:09 -07:00
Tom Moor c5de2da115 Merge branch 'develop' of github.com:outline/outline into develop 2020-09-07 10:43:36 -07:00
Tom Moor 709c3e78bd fix: Occasional render loop in editor toolbar (#1518)
* fix: CMD+S should save when editor title is focused

* fix: Bump RME, fixes various small editor issues
2020-09-07 10:42:51 -07:00
Tom Moor acb04fdf1a fix: CMD+S should save when editor title is focused 2020-09-06 10:33:39 -07:00
Tom Moor f13696dd2a Update yarn.lock 2020-09-05 19:58:17 -07:00
Tom Moor d6f245d67e Bump RME: Fixes inline styles in table cells with newlines 2020-09-05 19:51:12 -07:00
Tom Moor 11d3a5c9b9 fix: Enter in document title should create a newline at the top of editor and focus
closes #1511
2020-09-04 13:23:33 -07:00
Matheus Rocha Vieira cf1e506009 feat: Add ClickUp Embed Service (#1465)
* Add Clickup Embed Service

* Transparency Icon


Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-09-04 13:21:27 -07:00
Tom Moor 6b6d67beb6 fix: Allow disabling SSL for Postgres connection with standard PGSSLMODE env
closes #1501
2020-09-04 12:51:48 -07:00
Tom Moor a98e8ad8df fix: Breadcrumb overflow color is inaccessible
closes #1505
2020-09-04 12:49:20 -07:00
Tom Moor 9049785d98 fix: e shortcut to edit doesn't work when title is selected
closes #1510
2020-09-04 12:42:41 -07:00
Tom Moor e8648d4611 fix: Error in shares.info endpoint when requesting share record for deleted document 2020-09-03 23:22:41 -07:00
Tom Moor dd7436f78c fix: async error in backlinks service when dealing with a deleted document 2020-09-03 22:44:42 -07:00
Tom Moor b93a397ab3 feat: Bump RME 2020-09-03 22:27:30 -07:00
Tom Moor 206160582e chore: Tweak ordering of unpublish menu item 2020-09-02 20:19:36 -07:00
Tom Moor 4bf5926ee3 Bump RME 2020-08-31 21:57:26 -07:00
Tom Moor 82433e02a0 feat: Add custom error state for chunk loading failed 2020-08-31 21:09:23 -07:00
Tom Moor 637a9b5cf9 refactor: Remove unused event handlers 2020-08-31 20:44:19 -07:00
Tom Moor 95b91c466a fix: Disallow creating nested document within document in trash 2020-08-31 20:32:08 -07:00
Tom Moor 4edf90a184 fix: Development warning, missing key on event list items 2020-08-31 20:29:55 -07:00
Tom Moor 31522b0d6f fix: Local shares state not cleared when document is deleted 2020-08-31 20:28:41 -07:00
Tom Moor 759d4a5ac2 fix: Handle error revoking share link on frontend 2020-08-31 20:24:05 -07:00
Tom Moor dd0d51dd9d test 2020-08-31 20:23:51 -07:00
Tom Moor 8550116c6b fix: shares.list should not return shares for deleted documents
fix: shares.info should not return info for revoked shares
closes #1492
2020-08-31 20:15:10 -07:00
Tom Moor 3c7dc93982 Merge branch 'develop' of github.com:outline/outline into develop 2020-08-31 20:06:44 -07:00
Guilherme DIniz 0aa338cccc feat: Allow unpublishing documents (#1467)
* Allow unpublishing documents

* add block unpublish files that has children

* add api tests to new route
2020-08-31 20:03:05 -07:00
Tom Moor de8ac4acf5 fix: Configure mobx-react-lite observer batching
Removes development warning
2020-08-31 18:42:12 -07:00
Tom Moor de59147418 chore: Upgrade Sentry to 5.22.3
closes #1498
2020-08-31 18:36:30 -07:00
Tom Moor cf522cc85f fix: Regression with TOC not showing when navigating directly to document (#1500)
fix: Editing document too wide when TOC visible in read only
2020-08-31 18:31:13 -07:00
Tom Moor 8c7200fa87 chore: yarn deduplicate 2020-08-30 19:44:30 -07:00
Tom Moor f2310be173 Updated Yarn lockfile 2020-08-29 12:11:12 -07:00
Tom Moor 29f4dc9331 Bump RME
Fixes #1107 - It's now possible to use line breaks in table cells with Shift+Enter
Fixes #1253 - Selected content can now be dragged to reorder
2020-08-29 12:00:55 -07:00
Tom Moor 03b6dd62a8 fix: Missing click action to change permissions on a collection
fix: Modals no longer stacking correctly since upgrading react-portal
2020-08-25 21:00:50 -07:00
Tom Moor 7f0c608dbb Merge branch 'guilherme-diniz-feature/document-history-header' into develop 2020-08-25 20:04:02 -07:00
Tom Moor c52fbb944e Styling tidy up 2020-08-25 20:03:52 -07:00
Tom Moor e22e952606 Merge branch 'feature/document-history-header' of git://github.com/guilherme-diniz/outline into guilherme-diniz-feature/document-history-header 2020-08-25 19:44:56 -07:00
Guilherme Diniz 197cdff6c3 fix layout issues 2020-08-25 17:22:13 -03:00
Tom Moor 85d09b2351 fix: Deleting a document should correctly show who deleted (#1488) 2020-08-25 08:51:12 -07:00
Tom Moor 69611638b9 fix: Redirect to parent document when deleting a child document if possible (#1489) 2020-08-25 08:45:04 -07:00
Tom Moor e117d5f103 fix: Unable to view all possible locations when moving document (#1490)
* fix: Remove limit of displayed results on Move dialog

* fix: Filter templates from results

* Show final document location on hover/active, reduces visual noise
2020-08-25 08:44:46 -07:00
Tom Moor 03db975217 Merge branch 'feature/document-history-header' of git://github.com/guilherme-diniz/outline into guilherme-diniz-feature/document-history-header 2020-08-24 23:46:16 -07:00
Tom Moor 76279902f9 chore: Introduce AWS_S3_FORCE_PATH_STYLE option to maintain compatability with Minio et al (#1443)
- Make AWS_S3_UPLOAD_BUCKET_NAME optional
2020-08-24 23:27:10 -07:00
Tom Moor 9b5573c5e2 0.46.1 2020-08-24 20:22:08 -07:00
Tom Moor 70838918c3 fix: Collections not collapsing 2020-08-23 12:51:35 -07:00
Tom Moor ec38f5d79c refactor: Remove old react lifecycle methods (#1480)
* refactor: Remove deprecated APIs

* bump mobx-react for hooks support

* inject -> useStores
https://mobx-react.js.org/recipes-migration\#hooks-to-the-rescue

* chore: React rules of hooks lint
2020-08-23 11:51:56 -07:00
Jonathan Killian 179176c312 fix: Update package.json build script to use yarn instead of npm. (#1476)
* fix: Update package.json build script to yarn.

Update package.json build script to use yarn instead of npm to maintain consistency with the rest of scripts. I was running into an issue with the Dockerfile when using nvm with yarn and this fixed the issue.
2020-08-22 19:56:52 -07:00
Tom Moor c446a91f7d fix: Restore Postgres SSL support on Heroku
https://github.com/brianc/node-postgres/issues/2009
2020-08-22 08:27:42 -07:00
Guilherme Diniz 05f48f054b feat: Add Header to Document History Sidebar 2020-08-21 20:58:57 -03:00
Tom Moor ec55299c8b fix: Improve websocket reliability (#1470)
* check connection on page visibility change

* fix: SocketPresence account for socket changing
2020-08-20 20:37:54 -07:00
Tom Moor 26c574ab58 chore: Upgrade pg and sequelize to support node 14+ (#1462)
* Upgrade pg and sequelize to support node 14+

When Node 14 came out the app was incompatible, we should always have a maximum version defined here until the server code has been tested to prove compatibility

Co-authored-by: Lance Whatley <whatl3y@gmail.com>
2020-08-20 20:19:44 -07:00
Tom Moor 6dd6768f07 feat: Allow moving templates between collections (#1454)
- Allow template move in document policy
- fix: Ensure that document is not added to collection structure in documentMover command
- fix: Moving a template should now show nested documents as options
- fix: Hitting 'm' should not allow moving a draft
- fix: Styling of seperators on move screen
2020-08-20 19:46:29 -07:00
76 changed files with 1778 additions and 902 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ jobs:
build:
working_directory: ~/outline
docker:
- image: circleci/node:12
- image: circleci/node:14
- image: circleci/redis:latest
- image: circleci/postgres:9.6.5-alpine-ram
environment:
+1
View File
@@ -45,6 +45,7 @@ AWS_REGION=xx-xxxx-x
AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569
AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
AWS_S3_UPLOAD_MAX_SIZE=26214400
AWS_S3_FORCE_PATH_STYLE=true
# uploaded s3 objects permission level, default is private
# set to "public-read" to allow public access
AWS_S3_ACL=private
+2 -1
View File
@@ -4,7 +4,8 @@
"react-app",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:flowtype/recommended"
"plugin:flowtype/recommended",
"plugin:react-hooks/recommended"
],
"plugins": [
"prettier",
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.46.0
Licensed Work: Outline 0.47.0
The Licensed Work is (c) 2020 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2023-08-12
Change Date: 2023-09-07
Change License: Apache License, Version 2.0
+5
View File
@@ -92,6 +92,11 @@
"value": "26214400",
"required": false
},
"AWS_S3_FORCE_PATH_STYLE": {
"description": "Use path-style URL's for connecting to S3 instead of subdomain. This is useful for S3-compatible storage.",
"value": "true",
"required": false
},
"AWS_REGION": {
"value": "us-east-1",
"description": "Region in which the above S3 bucket exists",
+75 -32
View File
@@ -1,11 +1,13 @@
// @flow
import { observer, inject } from "mobx-react";
import {
PadlockIcon,
ArchiveIcon,
EditIcon,
GoToIcon,
MoreIcon,
PadlockIcon,
ShapesIcon,
EditIcon,
TrashIcon,
} from "outline-icons";
import * as React from "react";
import { Link } from "react-router-dom";
@@ -25,11 +27,73 @@ type Props = {
onlyText: boolean,
};
const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
const collection = collections.get(document.collectionId);
if (!collection) return <div />;
function Icon({ document }) {
if (document.isDeleted) {
return (
<>
<CollectionName to="/trash">
<TrashIcon color="currentColor" />
&nbsp;
<span>Trash</span>
</CollectionName>
<Slash />
</>
);
}
if (document.isArchived) {
return (
<>
<CollectionName to="/archive">
<ArchiveIcon color="currentColor" />
&nbsp;
<span>Archive</span>
</CollectionName>
<Slash />
</>
);
}
if (document.isDraft) {
return (
<>
<CollectionName to="/drafts">
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
</CollectionName>
<Slash />
</>
);
}
if (document.isTemplate) {
return (
<>
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
</CollectionName>
<Slash />
</>
);
}
return null;
}
const path = collection.pathToDocument(document).slice(0, -1);
const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
let collection = collections.get(document.collectionId);
if (!collection) {
if (!document.deletedAt) return <div />;
collection = {
id: document.collectionId,
name: "Deleted Collection",
color: "currentColor",
};
}
const path = collection.pathToDocument
? collection.pathToDocument(document).slice(0, -1)
: [];
if (onlyText === true) {
return (
@@ -50,34 +114,13 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
);
}
const isTemplate = document.isTemplate;
const isDraft = !document.publishedAt && !isTemplate;
const isNestedDocument = path.length > 1;
const lastPath = path.length ? path[path.length - 1] : undefined;
const menuPath = isNestedDocument ? path.slice(0, -1) : [];
return (
<Wrapper justify="flex-start" align="center">
{isTemplate && (
<>
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
</CollectionName>
<Slash />
</>
)}
{isDraft && (
<>
<CollectionName to="/drafts">
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
</CollectionName>
<Slash />
</>
)}
<Icon document={document} />
<CollectionName to={collectionUrl(collection.id)}>
<CollectionIcon collection={collection} expanded />
&nbsp;
@@ -127,12 +170,12 @@ export const Slash = styled(GoToIcon)`
const Overflow = styled(MoreIcon)`
flex-shrink: 0;
opacity: 0.25;
transition: opacity 100ms ease-in-out;
fill: ${(props) => props.theme.divider};
&:hover,
&:active {
opacity: 1;
&:active,
&:hover {
fill: ${(props) => props.theme.text};
}
`;
+1 -1
View File
@@ -18,7 +18,7 @@ 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" && collection.color !== "currentColor"
? getLuminance(collection.color) > 0.12
? collection.color
: "currentColor"
+1 -1
View File
@@ -14,7 +14,7 @@ export default function DelayedMount({ delay = 250, children }: Props) {
return () => {
clearTimeout(timeout);
};
}, []);
}, [delay]);
if (!isShowing) {
return null;
@@ -1,20 +1,23 @@
// @flow
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { observable, action } from "mobx";
import { observer, inject } from "mobx-react";
import { action, observable } from "mobx";
import { inject, observer } from "mobx-react";
import { CloseIcon } from "outline-icons";
import * as React from "react";
import { type RouterHistory, type Match } from "react-router-dom";
import { type Match, Redirect, type RouterHistory } from "react-router-dom";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DocumentsStore from "stores/DocumentsStore";
import RevisionsStore from "stores/RevisionsStore";
import Button from "components/Button";
import Flex from "components/Flex";
import { ListPlaceholder } from "components/LoadingPlaceholder";
import Revision from "./components/Revision";
import { documentHistoryUrl } from "utils/routeHelpers";
import { documentHistoryUrl, documentUrl } from "utils/routeHelpers";
type Props = {
match: Match,
@@ -29,6 +32,7 @@ class DocumentHistory extends React.Component<Props> {
@observable isFetching: boolean = false;
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
@observable redirectTo: ?string;
async componentDidMount() {
await this.loadMoreResults();
@@ -86,15 +90,34 @@ class DocumentHistory extends React.Component<Props> {
return this.props.revisions.getDocumentRevisions(document.id);
}
onCloseHistory = () => {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
this.redirectTo = documentUrl(document);
};
render() {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
const showLoading = (!this.isLoaded && this.isFetching) || !document;
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
return (
<Sidebar>
<Wrapper column>
<Header>
<Title>History</Title>
<Button
icon={<CloseIcon />}
onClick={this.onCloseHistory}
borderOnHover
neutral
/>
</Header>
{showLoading ? (
<Loading>
<ListPlaceholder count={5} />
@@ -140,10 +163,37 @@ const Wrapper = styled(Flex)`
`;
const Sidebar = styled(Flex)`
display: none;
background: ${(props) => props.theme.background};
min-width: ${(props) => props.theme.sidebarWidth};
border-left: 1px solid ${(props) => props.theme.divider};
z-index: 1;
${breakpoint("tablet")`
display: flex;
`};
`;
const Title = styled(Flex)`
font-size: 16px;
font-weight: 600;
text-align: center;
align-items: center;
justify-content: flex-start;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 0;
flex-grow: 1;
`;
const Header = styled(Flex)`
align-items: center;
position: relative;
padding: 12px;
border-bottom: 1px solid ${(props) => props.theme.divider};
color: ${(props) => props.theme.text};
flex-shrink: 0;
`;
export default inject("documents", "revisions")(DocumentHistory);
@@ -36,7 +36,7 @@ class RevisionListItem extends React.Component<Props> {
{revision.createdBy.name}
</Author>
<Meta>
<Time dateTime={revision.createdAt}>
<Time dateTime={revision.createdAt} tooltipDelay={250}>
{format(revision.createdAt, "MMMM Do, YYYY h:mm a")}
</Time>
</Meta>
+20 -1
View File
@@ -55,7 +55,26 @@ class ErrorBoundary extends React.Component<Props> {
render() {
if (this.error) {
const error = this.error;
const isReported = !!window.Sentry && env.DEPLOYMENT === "hosted";
const isChunkError = this.error.message.match(/chunk/);
if (isChunkError) {
return (
<CenteredContent>
<PageTitle title="Module failed to load" />
<h1>Loading Failed</h1>
<HelpText>
Sorry, part of the application failed to load. This may be because
it was updated since you opened the tab or because of a failed
network request. Please try reloading.
</HelpText>
<p>
<Button onClick={this.handleReload}>Reload</Button>
</p>
</CenteredContent>
);
}
return (
<CenteredContent>
@@ -66,7 +85,7 @@ class ErrorBoundary extends React.Component<Props> {
{isReported && " 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>}
{this.showDetails && <Pre>{error.toString()}</Pre>}
<p>
<Button onClick={this.handleReload}>Reload</Button>{" "}
{this.showDetails ? (
+10 -6
View File
@@ -20,12 +20,7 @@ type Props = {
onClose: () => void,
};
function HoverPreview({ node, documents, onClose, event }: Props) {
// previews only work for internal doc links for now
if (!isInternalUrl(node.href)) {
return null;
}
function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
const slug = parseDocumentSlugFromUrl(node.href);
const [isVisible, setVisible] = React.useState(false);
@@ -131,6 +126,15 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
);
}
function HoverPreview({ node, ...rest }: Props) {
// previews only work for internal doc links for now
if (!isInternalUrl(node.href)) {
return null;
}
return <HoverPreviewInternal {...rest} node={node} />;
}
const Animate = styled.div`
animation: ${fadeAndSlideIn} 150ms ease;
+6 -5
View File
@@ -44,21 +44,22 @@ class Layout extends React.Component<Props> {
@observable redirectTo: ?string;
@observable keyboardShortcutsOpen: boolean = false;
componentWillMount() {
this.updateBackground();
constructor(props) {
super();
this.updateBackground(props);
}
componentDidUpdate() {
this.updateBackground();
this.updateBackground(this.props);
if (this.redirectTo) {
this.redirectTo = undefined;
}
}
updateBackground() {
updateBackground(props) {
// ensure the wider page color always matches the theme
window.document.body.style.background = this.props.theme.background;
window.document.body.style.background = props.theme.background;
}
@keydown("shift+/")
+2 -1
View File
@@ -17,7 +17,8 @@ class Mask extends React.Component<Props> {
return false;
}
componentWillMount() {
constructor() {
super();
this.width = randomInteger(75, 100);
}
+24 -16
View File
@@ -9,6 +9,7 @@ import breakpoint from "styled-components-breakpoint";
import { fadeAndScaleIn } from "shared/styles/animations";
import Flex from "components/Flex";
import NudeButton from "components/NudeButton";
import Scrollable from "components/Scrollable";
ReactModal.setAppElement("#root");
@@ -27,7 +28,8 @@ const GlobalStyles = createGlobalStyle`
}
${breakpoint("tablet")`
.ReactModalPortal + .ReactModalPortal {
.ReactModalPortal + .ReactModalPortal,
.ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 12px;
box-shadow: 0 -2px 10px ${(props) => props.theme.shadow};
@@ -36,13 +38,15 @@ const GlobalStyles = createGlobalStyle`
}
}
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal {
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal,
.ReactModalPortal + .ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 24px;
}
}
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + .ReactModalPortal {
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + .ReactModalPortal,
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 36px;
}
@@ -72,10 +76,11 @@ const Modal = ({
isOpen={isOpen}
{...rest}
>
<Content onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
<Content>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
</Centered>
</Content>
<Back onClick={onRequestClose}>
<BackIcon size={32} color="currentColor" />
@@ -89,10 +94,20 @@ const Modal = ({
);
};
const Content = styled(Flex)`
const Content = styled(Scrollable)`
width: 100%;
padding: 8vh 2rem 2rem;
${breakpoint("tablet")`
padding-top: 13vh;
`};
`;
const Centered = styled(Flex)`
width: 640px;
max-width: 100%;
position: relative;
margin: 0 auto;
`;
const StyledModal = styled(ReactModal)`
@@ -107,16 +122,9 @@ const StyledModal = styled(ReactModal)`
display: flex;
justify-content: center;
align-items: flex-start;
overflow-x: hidden;
overflow-y: auto;
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
padding: 8vh 2rem 2rem;
outline: none;
${breakpoint("tablet")`
padding-top: 13vh;
`};
`;
const Text = styled.span`
@@ -147,7 +155,7 @@ const Close = styled(NudeButton)`
`;
const Back = styled(NudeButton)`
position: fixed;
position: absolute;
display: none;
align-items: center;
top: 2rem;
+14 -4
View File
@@ -42,20 +42,23 @@ class PathToDocument extends React.Component<Props> {
return (
<Component ref={ref} onClick={this.handleClick} href="" selectable>
{collection && <CollectionIcon collection={collection} />}
&nbsp;
{result.path
.map((doc) => <Title key={doc.id}>{doc.title}</Title>)
.reduce((prev, curr) => [prev, <StyledGoToIcon />, curr])}
{document && (
<Flex>
<DocumentTitle>
{" "}
<StyledGoToIcon /> <Title>{document.title}</Title>
</Flex>
</DocumentTitle>
)}
</Component>
);
}
}
const DocumentTitle = styled(Flex)``;
const Title = styled.span`
white-space: nowrap;
overflow: hidden;
@@ -63,7 +66,7 @@ const Title = styled.span`
`;
const StyledGoToIcon = styled(GoToIcon)`
opacity: 0.25;
fill: ${(props) => props.theme.divider};
`;
const ResultWrapper = styled.div`
@@ -79,13 +82,20 @@ const ResultWrapper = styled.div`
const ResultWrapperLink = styled(ResultWrapper.withComponent("a"))`
margin: 0 -8px;
padding: 8px 4px;
border-radius: 8px;
${DocumentTitle} {
display: none;
}
&:hover,
&:active,
&:focus {
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
${DocumentTitle} {
display: flex;
}
}
`;
+35 -40
View File
@@ -1,65 +1,60 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import { CloseIcon, MenuIcon } from "outline-icons";
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 UiStore from "stores/UiStore";
import Fade from "components/Fade";
import Flex from "components/Flex";
import usePrevious from "hooks/usePrevious";
import useStores from "hooks/useStores";
let firstRender = true;
type Props = {
children: React.Node,
location: Location,
ui: UiStore,
};
@observer
class Sidebar extends React.Component<Props> {
componentWillReceiveProps = (nextProps: Props) => {
if (this.props.location !== nextProps.location) {
this.props.ui.hideMobileSidebar();
function Sidebar({ location, children }: Props) {
const { ui } = useStores();
const previousLocation = usePrevious(location);
React.useEffect(() => {
if (location !== previousLocation) {
ui.hideMobileSidebar();
}
};
}, [ui, location]);
toggleSidebar = () => {
this.props.ui.toggleMobileSidebar();
};
render() {
const { children, ui } = this.props;
const content = (
<Container
editMode={ui.editMode}
const content = (
<Container
editMode={ui.editMode}
mobileSidebarVisible={ui.mobileSidebarVisible}
column
>
<Toggle
onClick={ui.toggleMobileSidebar}
mobileSidebarVisible={ui.mobileSidebarVisible}
column
>
<Toggle
onClick={this.toggleSidebar}
mobileSidebarVisible={ui.mobileSidebarVisible}
>
{ui.mobileSidebarVisible ? (
<CloseIcon size={32} />
) : (
<MenuIcon size={32} />
)}
</Toggle>
{children}
</Container>
);
{ui.mobileSidebarVisible ? (
<CloseIcon size={32} />
) : (
<MenuIcon size={32} />
)}
</Toggle>
{children}
</Container>
);
// Fade in the sidebar on first render after page load
if (firstRender) {
firstRender = false;
return <Fade>{content}</Fade>;
}
return content;
// Fade in the sidebar on first render after page load
if (firstRender) {
firstRender = false;
return <Fade>{content}</Fade>;
}
return content;
}
const Container = styled(Flex)`
@@ -117,4 +112,4 @@ const Toggle = styled.a`
`};
`;
export default withRouter(inject("ui")(Sidebar));
export default withRouter(observer(Sidebar));
@@ -1,5 +1,4 @@
// @flow
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
@@ -25,79 +24,80 @@ type Props = {
depth?: number,
};
@observer
class SidebarLink extends React.Component<Props> {
@observable expanded: ?boolean = this.props.expanded;
function SidebarLink({
icon,
children,
onClick,
to,
label,
active,
menu,
menuOpen,
hideDisclosure,
theme,
exact,
href,
depth,
...rest
}: Props) {
const [expanded, setExpanded] = React.useState(rest.expanded);
style = {
paddingLeft: `${(this.props.depth || 0) * 16 + 16}px`,
};
componentWillReceiveProps(nextProps: Props) {
if (nextProps.expanded !== undefined) {
this.expanded = nextProps.expanded;
}
}
@action
handleClick = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.expanded = !this.expanded;
};
@action
handleExpand = () => {
this.expanded = true;
};
render() {
const {
icon,
children,
onClick,
to,
label,
active,
menu,
menuOpen,
hideDisclosure,
exact,
href,
} = this.props;
const showDisclosure = !!children && !hideDisclosure;
const activeStyle = {
color: this.props.theme.text,
background: this.props.theme.sidebarItemBackground,
fontWeight: 600,
...this.style,
const style = React.useMemo(() => {
return {
paddingLeft: `${(depth || 0) * 16 + 16}px`,
};
}, [depth]);
return (
<Wrapper column>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : this.style}
onClick={onClick}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label onClick={this.handleExpand}>
{showDisclosure && (
<Disclosure expanded={this.expanded} onClick={this.handleClick} />
)}
{label}
</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
{this.expanded && children}
</Wrapper>
);
}
React.useEffect(() => {
if (rest.expanded !== undefined) {
setExpanded(rest.expanded);
}
}, [rest.expanded]);
const handleClick = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded(!expanded);
},
[expanded]
);
const handleExpand = React.useCallback(() => {
setExpanded(true);
}, []);
const showDisclosure = !!children && !hideDisclosure;
const activeStyle = {
color: theme.text,
background: theme.sidebarItemBackground,
fontWeight: 600,
...style,
};
return (
<Wrapper column>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label onClick={handleExpand}>
{showDisclosure && (
<Disclosure expanded={expanded} onClick={handleClick} />
)}
{label}
</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
{expanded && children}
</Wrapper>
);
}
// accounts for whitespace around icon
@@ -171,4 +171,4 @@ const Disclosure = styled(CollapsedIcon)`
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
export default withRouter(withTheme(SidebarLink));
export default withRouter(withTheme(observer(SidebarLink)));
+48 -11
View File
@@ -3,7 +3,7 @@ import { find } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import io from "socket.io-client";
import io, { Socket } from "socket.io-client";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
@@ -13,6 +13,7 @@ import MembershipsStore from "stores/MembershipsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import ViewsStore from "stores/ViewsStore";
import { getVisibilityListener, getPageVisible } from "utils/pageVisibility";
export const SocketContext: any = React.createContext();
@@ -31,9 +32,35 @@ type Props = {
@observer
class SocketProvider extends React.Component<Props> {
@observable socket;
@observable socket: Socket;
componentDidMount() {
this.createConnection();
document.addEventListener(getVisibilityListener(), this.checkConnection);
}
componentWillUnmount() {
if (this.socket) {
this.socket.authenticated = false;
this.socket.disconnect();
}
document.removeEventListener(getVisibilityListener(), this.checkConnection);
}
checkConnection = () => {
if (this.socket && this.socket.disconnected && getPageVisible()) {
// null-ifying this reference is important, do not remove. Without it
// references to old sockets are potentially held in context
this.socket.close();
this.socket = null;
this.createConnection();
}
};
createConnection = () => {
this.socket = io(window.location.origin, {
path: "/realtime",
transports: ["websocket"],
@@ -98,6 +125,8 @@ class SocketProvider extends React.Component<Props> {
if (document) {
document.deletedAt = documentDescriptor.updatedAt;
}
policies.remove(documentId);
continue;
}
@@ -145,7 +174,21 @@ class SocketProvider extends React.Component<Props> {
const collection = collections.get(collectionId) || {};
if (event.event === "collections.delete") {
const collection = collections.get(collectionId);
if (collection) {
collection.deletedAt = collectionDescriptor.updatedAt;
}
const deletedDocuments = documents.inCollection(collectionId);
deletedDocuments.forEach((doc) => {
doc.deletedAt = collectionDescriptor.updatedAt;
policies.remove(doc.id);
});
documents.removeCollectionDocuments(collectionId);
memberships.removeCollectionMemberships(collectionId);
collections.remove(collectionId);
policies.remove(collectionId);
continue;
}
@@ -160,9 +203,10 @@ class SocketProvider extends React.Component<Props> {
await collections.fetch(collectionId, { force: true });
} catch (err) {
if (err.statusCode === 404 || err.statusCode === 403) {
collections.remove(collectionId);
documents.removeCollectionDocuments(collectionId);
memberships.removeCollectionMemberships(collectionId);
collections.remove(collectionId);
policies.remove(collectionId);
return;
}
}
@@ -264,14 +308,7 @@ class SocketProvider extends React.Component<Props> {
this.socket.on("user.presence", (event) => {
presence.touch(event.documentId, event.userId, event.isEditing);
});
}
componentWillUnmount() {
if (this.socket) {
this.socket.disconnect();
this.socket.authenticated = false;
}
}
};
render() {
return (
+2
View File
@@ -23,6 +23,7 @@ function eachMinute(fn) {
type Props = {
dateTime: string,
children?: React.Node,
tooltipDelay?: number,
};
class Time extends React.Component<Props> {
@@ -42,6 +43,7 @@ class Time extends React.Component<Props> {
return (
<Tooltip
tooltip={format(this.props.dateTime, "MMMM Do, YYYY h:mm a")}
delay={this.props.tooltipDelay}
placement="bottom"
>
<time dateTime={this.props.dateTime}>
+22
View File
@@ -0,0 +1,22 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https?://share.clickup.com/[a-z]/[a-z]/(.*)/(.*)$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
export default class ClickUp extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return <Frame src={this.props.attrs.href} title="ClickUp Embed" />;
}
}
+17
View File
@@ -0,0 +1,17 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import ClickUp from "./ClickUp";
describe("ClickUp", () => {
const match = ClickUp.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://share.clickup.com/b/h/6-9310960-2/c9d837d74182317".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://share.clickup.com".match(match)).toBe(null);
expect("https://clickup.com/".match(match)).toBe(null);
expect("https://clickup.com/features".match(match)).toBe(null);
});
});
+8
View File
@@ -3,6 +3,7 @@ import * as React from "react";
import styled from "styled-components";
import Abstract from "./Abstract";
import Airtable from "./Airtable";
import ClickUp from "./ClickUp";
import Codepen from "./Codepen";
import Figma from "./Figma";
import Framer from "./Framer";
@@ -57,6 +58,13 @@ export default [
component: Airtable,
matcher: matcher(Airtable),
},
{
title: "ClickUp",
keywords: "project",
icon: () => <Img src="/images/clickup.png" />,
component: ClickUp,
matcher: matcher(ClickUp),
},
{
title: "Codepen",
keywords: "code editor",
+10
View File
@@ -0,0 +1,10 @@
// @flow
import * as React from "react";
export default function usePrevious(value: any) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
+8
View File
@@ -0,0 +1,8 @@
// @flow
import { MobXProviderContext } from "mobx-react";
import * as React from "react";
import RootStore from "stores";
export default function useStores(): typeof RootStore {
return React.useContext(MobXProviderContext);
}
+15 -22
View File
@@ -1,4 +1,5 @@
// @flow
import "mobx-react-lite/batchingForReactDom";
import { Provider } from "mobx-react";
import * as React from "react";
import { render } from "react-dom";
@@ -12,32 +13,24 @@ import Toasts from "components/Toasts";
import Routes from "./routes";
import env from "env";
let DevTools;
if (process.env.NODE_ENV !== "production") {
DevTools = require("mobx-react-devtools").default; // eslint-disable-line global-require
}
const element = document.getElementById("root");
if (element) {
render(
<>
<ErrorBoundary>
<Provider {...stores}>
<Theme>
<Router>
<>
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
</>
</Router>
</Theme>
</Provider>
</ErrorBoundary>
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
</>,
<ErrorBoundary>
<Provider {...stores}>
<Theme>
<Router>
<>
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
</>
</Router>
</Theme>
</Provider>
</ErrorBoundary>,
element
);
}
+1
View File
@@ -127,6 +127,7 @@ class CollectionMenu extends React.Component<Props> {
collection={collection}
onSubmit={this.handleMembersModalClose}
handleEditCollectionOpen={this.handleEditCollectionOpen}
onEdit={this.handleEditCollectionOpen}
/>
</Modal>
<DropdownMenu onOpen={onOpen} onClose={onClose} position={position}>
+61 -8
View File
@@ -3,7 +3,6 @@ import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { Redirect } from "react-router-dom";
import AuthStore from "stores/AuthStore";
import CollectionStore from "stores/CollectionsStore";
import PoliciesStore from "stores/PoliciesStore";
@@ -12,13 +11,18 @@ import Document from "models/Document";
import DocumentDelete from "scenes/DocumentDelete";
import DocumentShare from "scenes/DocumentShare";
import DocumentTemplatize from "scenes/DocumentTemplatize";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import CollectionIcon from "components/CollectionIcon";
import {
DropdownMenu,
DropdownMenuItem,
Header,
} from "components/DropdownMenu";
import Modal from "components/Modal";
import {
documentUrl,
documentMoveUrl,
editDocumentUrl,
documentHistoryUrl,
documentMoveUrl,
documentUrl,
editDocumentUrl,
newDocumentUrl,
} from "utils/routeHelpers";
@@ -101,11 +105,19 @@ class DocumentMenu extends React.Component<Props> {
this.props.ui.showToast("Document archived");
};
handleRestore = async (ev: SyntheticEvent<>) => {
await this.props.document.restore();
handleRestore = async (
ev: SyntheticEvent<>,
options?: { collectionId: string }
) => {
await this.props.document.restore(options);
this.props.ui.showToast("Document restored");
};
handleUnpublish = async (ev: SyntheticEvent<>) => {
await this.props.document.unpublish();
this.props.ui.showToast("Document unpublished");
};
handlePin = (ev: SyntheticEvent<>) => {
this.props.document.pin();
};
@@ -150,6 +162,7 @@ class DocumentMenu extends React.Component<Props> {
showPrint,
showPin,
auth,
collections,
onOpen,
onClose,
} = this.props;
@@ -157,6 +170,7 @@ class DocumentMenu extends React.Component<Props> {
const can = policies.abilities(document.id);
const canShareDocuments = can.share && auth.team && auth.team.sharing;
const canViewHistory = can.read && !can.restore;
const collection = collections.get(document.collectionId);
return (
<>
@@ -166,11 +180,45 @@ class DocumentMenu extends React.Component<Props> {
onOpen={onOpen}
onClose={onClose}
>
{(can.unarchive || can.restore) && (
{can.unarchive && (
<DropdownMenuItem onClick={this.handleRestore}>
Restore
</DropdownMenuItem>
)}
{can.restore &&
(collection ? (
<DropdownMenuItem onClick={this.handleRestore}>
Restore
</DropdownMenuItem>
) : (
<DropdownMenu
label={<DropdownMenuItem>Restore</DropdownMenuItem>}
style={{
left: -170,
position: "relative",
top: -40,
}}
hover
>
<Header>Choose a collection</Header>
{collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);
return (
<DropdownMenuItem
key={collection.id}
onClick={(ev) =>
this.handleRestore(ev, { collectionId: collection.id })
}
disabled={!can.update}
>
<CollectionIcon collection={collection} />
&nbsp;{collection.name}
</DropdownMenuItem>
);
})}
</DropdownMenu>
))}
{showPin &&
(document.pinned
? can.unpin && (
@@ -230,6 +278,11 @@ class DocumentMenu extends React.Component<Props> {
Create template
</DropdownMenuItem>
)}
{can.unpublish && (
<DropdownMenuItem onClick={this.handleUnpublish}>
Unpublish
</DropdownMenuItem>
)}
{can.update && (
<DropdownMenuItem onClick={this.handleEdit}>Edit</DropdownMenuItem>
)}
+1 -1
View File
@@ -24,7 +24,7 @@ type Props = {
class RevisionMenu extends React.Component<Props> {
handleRestore = async (ev: SyntheticEvent<>) => {
ev.preventDefault();
await this.props.document.restore(this.props.revision);
await this.props.document.restore({ revisionId: this.props.revision.id });
this.props.ui.showToast("Document restored");
this.props.history.push(this.props.document.url);
};
+8 -3
View File
@@ -31,10 +31,15 @@ class ShareMenu extends React.Component<Props> {
this.redirectTo = this.props.share.documentUrl;
};
handleRevoke = (ev: SyntheticEvent<>) => {
handleRevoke = async (ev: SyntheticEvent<>) => {
ev.preventDefault();
this.props.shares.revoke(this.props.share);
this.props.ui.showToast("Share link revoked");
try {
await this.props.shares.revoke(this.props.share);
this.props.ui.showToast("Share link revoked");
} catch (err) {
this.props.ui.showToast(err.message);
}
};
handleCopy = () => {
+7 -4
View File
@@ -1,12 +1,11 @@
// @flow
import addDays from "date-fns/add_days";
import invariant from "invariant";
import { action, set, observable, computed } from "mobx";
import { action, computed, observable, set } from "mobx";
import parseTitle from "shared/utils/parseTitle";
import unescape from "shared/utils/unescape";
import DocumentsStore from "stores/DocumentsStore";
import BaseModel from "models/BaseModel";
import Revision from "models/Revision";
import User from "models/User";
type SaveOptions = {
@@ -141,8 +140,12 @@ export default class Document extends BaseModel {
return this.store.archive(this);
};
restore = (revision: Revision) => {
return this.store.restore(this, revision);
restore = (options) => {
return this.store.restore(this, options);
};
unpublish = () => {
return this.store.unpublish(this);
};
@action
+28 -12
View File
@@ -36,6 +36,7 @@ import Tab from "components/Tab";
import Tabs from "components/Tabs";
import Tooltip from "components/Tooltip";
import CollectionMenu from "menus/CollectionMenu";
import { AuthorizationError } from "utils/errors";
import { newDocumentUrl, collectionUrl } from "utils/routeHelpers";
type Props = {
@@ -62,10 +63,19 @@ class CollectionScene extends React.Component<Props> {
}
}
componentWillReceiveProps(nextProps) {
const { id } = nextProps.match.params;
componentDidUpdate(prevProps) {
const { id } = this.props.match.params;
if (id && id !== this.props.match.params.id) {
if (this.collection) {
const { collection } = this;
const policy = this.props.policies.get(collection.id);
if (!policy) {
this.loadContent(collection.id);
}
}
if (id && id !== prevProps.match.params.id) {
this.loadContent(id);
}
}
@@ -75,18 +85,24 @@ class CollectionScene extends React.Component<Props> {
}
loadContent = async (id: string) => {
const collection = await this.props.collections.fetch(id);
try {
const collection = await this.props.collections.fetch(id);
if (collection) {
this.props.ui.setActiveCollection(collection);
this.collection = collection;
if (collection) {
this.props.ui.setActiveCollection(collection);
this.collection = collection;
await this.props.documents.fetchPinned({
collectionId: id,
});
await this.props.documents.fetchPinned({
collectionId: id,
});
}
} catch (error) {
if (error instanceof AuthorizationError) {
this.collection = null;
}
} finally {
this.isFetching = false;
}
this.isFetching = false;
};
onNewDocument = (ev: SyntheticEvent<>) => {
+3 -2
View File
@@ -46,8 +46,9 @@ class CollectionDelete extends React.Component<Props> {
<form onSubmit={this.handleSubmit}>
<HelpText>
Are you sure about that? Deleting the{" "}
<strong>{collection.name}</strong> collection is permanent and will
also delete all of the documents within it, so be extra careful.
<strong>{collection.name}</strong> collection is permanent and
cannot be restored, however documents within will be moved to the
trash.
</HelpText>
<Button type="submit" disabled={this.isDeleting} autoFocus danger>
{this.isDeleting ? "Deleting…" : "Im sure  Delete"}
+5 -13
View File
@@ -20,20 +20,12 @@ type Props = {
@observer
class CollectionEdit extends React.Component<Props> {
@observable name: string;
@observable description: string = "";
@observable icon: string = "";
@observable color: string = "#4E5C6E";
@observable name: string = this.props.collection.name;
@observable description: string = this.props.collection.description;
@observable icon: string = this.props.collection.icon;
@observable color: string = this.props.collection.color || "#4E5C6E";
@observable private: boolean = this.props.collection.private;
@observable isSaving: boolean;
@observable private: boolean = false;
componentDidMount() {
this.name = this.props.collection.name;
this.description = this.props.collection.description;
this.icon = this.props.collection.icon;
this.color = this.props.collection.color;
this.private = this.props.collection.private;
}
handleSubmit = async (ev: SyntheticEvent<*>) => {
ev.preventDefault();
@@ -132,7 +132,7 @@ class CollectionMembers extends React.Component<Props> {
collection. You can make this collection visible to the entire
team by{" "}
<a role="button" onClick={this.props.onEdit}>
changing its visibility
changing the visibility
</a>
.
</HelpText>
+7 -1
View File
@@ -50,7 +50,8 @@ class DataLoader extends React.Component<Props> {
// reload from the server otherwise the UI will not know which authorizations
// the user has
if (this.document) {
const policy = this.props.policies.get(this.document.id);
const document = this.document;
const policy = this.props.policies.get(document.id);
if (!policy && !this.error) {
this.loadDocument();
@@ -101,6 +102,11 @@ class DataLoader extends React.Component<Props> {
loadDocument = async () => {
const { shareId, documentSlug, revisionId } = this.props.match.params;
// sets the document as active in the sidebar if we already have it loaded
if (this.document) {
this.props.ui.setActiveDocument(this.document);
}
try {
this.document = await this.props.documents.fetch(documentSlug, {
shareId,
+48 -45
View File
@@ -18,6 +18,7 @@ import Branding from "components/Branding";
import ErrorBoundary from "components/ErrorBoundary";
import Flex from "components/Flex";
import LoadingIndicator from "components/LoadingIndicator";
import LoadingPlaceholder from "components/LoadingPlaceholder";
import Notice from "components/Notice";
import PageTitle from "components/PageTitle";
import Time from "components/Time";
@@ -67,7 +68,7 @@ type Props = {
@observer
class DocumentScene extends React.Component<Props> {
@observable editor: ?any;
@observable editor = React.createRef();
@observable isUploading: boolean = false;
@observable isSaving: boolean = false;
@observable isPublishing: boolean = false;
@@ -133,7 +134,7 @@ class DocumentScene extends React.Component<Props> {
ev.preventDefault();
const { document, abilities } = this.props;
if (abilities.update) {
if (abilities.move) {
this.props.history.push(documentMoveUrl(document));
}
}
@@ -380,7 +381,7 @@ class DocumentScene extends React.Component<Props> {
)}
<MaxWidth
archived={document.isArchived}
tocVisible={ui.tocVisible}
tocVisible={ui.tocVisible && readOnly}
column
auto
>
@@ -412,50 +413,52 @@ class DocumentScene extends React.Component<Props> {
)}
</Notice>
)}
<Flex auto={!readOnly}>
{ui.tocVisible && readOnly && (
<Contents
headings={this.editor ? this.editor.getHeadings() : []}
<React.Suspense fallback={<LoadingPlaceholder />}>
<Flex auto={!readOnly}>
{ui.tocVisible && readOnly && (
<Contents
headings={
this.editor.current
? this.editor.current.getHeadings()
: []
}
/>
)}
<Editor
id={document.id}
innerRef={this.editor}
isShare={isShare}
isDraft={document.isDraft}
template={document.isTemplate}
key={[injectTemplate, disableEmbeds].join("-")}
title={revision ? revision.title : this.title}
document={document}
value={readOnly ? value : undefined}
defaultValue={value}
disableEmbeds={disableEmbeds}
onImageUploadStart={this.onImageUploadStart}
onImageUploadStop={this.onImageUploadStop}
onSearchLink={this.props.onSearchLink}
onCreateLink={this.props.onCreateLink}
onChangeTitle={this.onChangeTitle}
onChange={this.onChange}
onSave={this.onSave}
onPublish={this.onPublish}
onCancel={this.goBack}
readOnly={readOnly}
readOnlyWriteCheckboxes={readOnly && abilities.update}
ui={this.props.ui}
/>
</Flex>
{readOnly && !isShare && !revision && (
<>
<MarkAsViewed document={document} />
<ReferencesWrapper isOnlyTitle={document.isOnlyTitle}>
<References document={document} />
</ReferencesWrapper>
</>
)}
<Editor
id={document.id}
ref={(ref) => {
if (ref) {
this.editor = ref;
}
}}
isShare={isShare}
isDraft={document.isDraft}
template={document.isTemplate}
key={[injectTemplate, disableEmbeds].join("-")}
title={revision ? revision.title : this.title}
document={document}
value={readOnly ? value : undefined}
defaultValue={value}
disableEmbeds={disableEmbeds}
onImageUploadStart={this.onImageUploadStart}
onImageUploadStop={this.onImageUploadStop}
onSearchLink={this.props.onSearchLink}
onCreateLink={this.props.onCreateLink}
onChangeTitle={this.onChangeTitle}
onChange={this.onChange}
onSave={this.onSave}
onPublish={this.onPublish}
onCancel={this.goBack}
readOnly={readOnly}
readOnlyWriteCheckboxes={readOnly && abilities.update}
ui={this.props.ui}
/>
</Flex>
{readOnly && !isShare && !revision && (
<>
<MarkAsViewed document={document} />
<ReferencesWrapper isOnlyTitle={document.isOnlyTitle}>
<References document={document} />
</ReferencesWrapper>
</>
)}
</React.Suspense>
</MaxWidth>
</Container>
</Background>
+91 -44
View File
@@ -13,13 +13,11 @@ import DocumentsStore from "stores/DocumentsStore";
import UiStore from "stores/UiStore";
import Document from "models/Document";
import Flex from "components/Flex";
import Input from "components/Input";
import { Outline } from "components/Input";
import Labeled from "components/Labeled";
import Modal from "components/Modal";
import PathToDocument from "components/PathToDocument";
const MAX_RESULTS = 8;
type Props = {|
document: Document,
documents: DocumentsStore,
@@ -36,14 +34,19 @@ class DocumentMove extends React.Component<Props> {
@computed
get searchIndex() {
const { collections } = this.props;
const { collections, documents } = this.props;
const paths = collections.pathsToDocuments;
const index = new Search("id");
index.addIndex("title");
// Build index
const indexeableDocuments = [];
paths.forEach((path) => indexeableDocuments.push(path));
paths.forEach((path) => {
const doc = documents.get(path.id);
if (!doc || !doc.isTemplate) {
indexeableDocuments.push(path);
}
});
index.addDocuments(indexeableDocuments);
return index;
@@ -52,6 +55,7 @@ class DocumentMove extends React.Component<Props> {
@computed
get results(): DocumentPath[] {
const { document, collections } = this.props;
const onlyShowCollections = document.isTemplate;
let results = [];
if (collections.isLoaded) {
@@ -62,17 +66,23 @@ class DocumentMove extends React.Component<Props> {
}
}
// Exclude root from search results if document is already at the root
if (!document.parentDocumentId) {
results = results.filter((result) => result.id !== document.collectionId);
}
if (onlyShowCollections) {
results = results.filter((result) => result.type === "collection");
} else {
// Exclude root from search results if document is already at the root
if (!document.parentDocumentId) {
results = results.filter(
(result) => result.id !== document.collectionId
);
}
// Exclude document if on the path to result, or the same result
results = results.filter(
(result) =>
!result.path.map((doc) => doc.id).includes(document.id) &&
last(result.path.map((doc) => doc.id)) !== document.parentDocumentId
);
// Exclude document if on the path to result, or the same result
results = results.filter(
(result) =>
!result.path.map((doc) => doc.id).includes(document.id) &&
last(result.path.map((doc) => doc.id)) !== document.parentDocumentId
);
}
return results;
}
@@ -129,35 +139,41 @@ class DocumentMove extends React.Component<Props> {
</Section>
<Section column>
<Labeled label="Choose a new location">
<Input
type="search"
placeholder="Search collections & documents…"
onKeyDown={this.handleKeyDown}
onChange={this.handleFilter}
required
autoFocus
/>
</Labeled>
<Flex column>
<StyledArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{this.results.slice(0, MAX_RESULTS).map((result, index) => (
<PathToDocument
key={result.id}
result={result}
document={document}
collection={collections.get(result.collectionId)}
ref={(ref) =>
index === 0 && this.setFirstDocumentRef(ref)
}
onSuccess={this.handleSuccess}
/>
))}
</StyledArrowKeyNavigation>
</Flex>
<Labeled label="Choose a new location" />
<NewLocation>
<InputWrapper>
<Input
type="search"
placeholder="Search collections & documents…"
onKeyDown={this.handleKeyDown}
onChange={this.handleFilter}
required
autoFocus
/>
</InputWrapper>
<Results>
<Flex column>
<StyledArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{this.results.map((result, index) => (
<PathToDocument
key={result.id}
result={result}
document={document}
collection={collections.get(result.collectionId)}
ref={(ref) =>
index === 0 && this.setFirstDocumentRef(ref)
}
onSuccess={this.handleSuccess}
/>
))}
</StyledArrowKeyNavigation>
</Flex>
</Results>
</NewLocation>
</Section>
</Flex>
)}
@@ -166,6 +182,37 @@ class DocumentMove extends React.Component<Props> {
}
}
const InputWrapper = styled("div")`
padding: 8px;
width: 100%;
`;
const Input = styled("input")`
width: 100%;
outline: none;
background: none;
border-radius: 4px;
height: 30px;
border: 0;
color: ${(props) => props.theme.text};
&::placeholder {
color: ${(props) => props.theme.placeholder};
}
`;
const NewLocation = styled(Outline)`
flex-direction: column;
`;
const Results = styled(Flex)`
display: block;
width: 100%;
max-height: 40vh;
overflow-y: auto;
padding: 8px;
`;
const Section = styled(Flex)`
margin-bottom: 24px;
`;
+58 -48
View File
@@ -11,7 +11,6 @@ import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
import Flex from "components/Flex";
import HoverPreview from "components/HoverPreview";
import LoadingPlaceholder from "components/LoadingPlaceholder";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {
@@ -22,37 +21,50 @@ type Props = {
isDraft: boolean,
isShare: boolean,
readOnly?: boolean,
onSave: () => mixed,
innerRef: { current: any },
};
@observer
class DocumentEditor extends React.Component<Props> {
@observable activeLinkEvent: ?MouseEvent;
editor = React.createRef<any>();
focusAtStart = () => {
if (this.editor.current) {
this.editor.current.focusAtStart();
if (this.props.innerRef.current) {
this.props.innerRef.current.focusAtStart();
}
};
focusAtEnd = () => {
if (this.editor.current) {
this.editor.current.focusAtEnd();
if (this.props.innerRef.current) {
this.props.innerRef.current.focusAtEnd();
}
};
getHeadings = () => {
if (this.editor.current) {
return this.editor.current.getHeadings();
insertParagraph = () => {
if (this.props.innerRef.current) {
const { view } = this.props.innerRef.current;
const { dispatch, state } = view;
dispatch(state.tr.insert(0, state.schema.nodes.paragraph.create()));
}
return [];
};
handleTitleKeyDown = (event: SyntheticKeyboardEvent<>) => {
if (event.key === "Enter" || event.key === "Tab") {
if (event.key === "Enter" && !event.metaKey) {
event.preventDefault();
this.insertParagraph();
this.focusAtStart();
return;
}
if (event.key === "Tab" || event.key === "ArrowDown") {
event.preventDefault();
this.focusAtStart();
return;
}
if (event.key === "s" && event.metaKey) {
event.preventDefault();
this.props.onSave();
return;
}
};
@@ -72,49 +84,47 @@ class DocumentEditor extends React.Component<Props> {
isDraft,
isShare,
readOnly,
innerRef,
} = this.props;
const { emoji } = parseTitle(title);
const startsWithEmojiAndSpace = !!(emoji && title.startsWith(`${emoji} `));
return (
<Flex auto column>
<React.Suspense fallback={<LoadingPlaceholder />}>
<Title
type="text"
onChange={onChangeTitle}
onKeyDown={this.handleTitleKeyDown}
placeholder={document.placeholder}
value={!title && readOnly ? document.titleWithDefault : title}
style={
startsWithEmojiAndSpace ? { marginLeft: "-1.2em" } : undefined
}
readOnly={readOnly}
autoFocus={!title}
maxLength={100}
<Title
type="text"
onChange={onChangeTitle}
onKeyDown={this.handleTitleKeyDown}
placeholder={document.placeholder}
value={!title && readOnly ? document.titleWithDefault : title}
style={startsWithEmojiAndSpace ? { marginLeft: "-1.2em" } : undefined}
readOnly={readOnly}
disabled={readOnly}
autoFocus={!title}
maxLength={100}
/>
<DocumentMetaWithViews
isDraft={isDraft}
document={document}
to={documentHistoryUrl(document)}
/>
<Editor
ref={innerRef}
autoFocus={title && !this.props.defaultValue}
placeholder="…the rest is up to you"
onHoverLink={this.handleLinkActive}
scrollTo={window.location.hash}
grow
{...this.props}
/>
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
{this.activeLinkEvent && !isShare && readOnly && (
<HoverPreview
node={this.activeLinkEvent.target}
event={this.activeLinkEvent}
onClose={this.handleLinkInactive}
/>
<DocumentMetaWithViews
isDraft={isDraft}
document={document}
to={documentHistoryUrl(document)}
/>
<Editor
ref={this.editor}
autoFocus={title && !this.props.defaultValue}
placeholder="…the rest is up to you"
onHoverLink={this.handleLinkActive}
scrollTo={window.location.hash}
grow
{...this.props}
/>
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
{this.activeLinkEvent && !isShare && readOnly && (
<HoverPreview
node={this.activeLinkEvent.target}
event={this.activeLinkEvent}
onClose={this.handleLinkInactive}
/>
)}
</React.Suspense>
)}
</Flex>
);
}
+1 -1
View File
@@ -47,7 +47,7 @@ class References extends React.Component<Props> {
)}
{showBacklinks && (
<Tab to="#backlinks" isActive={() => isBacklinksTab}>
References
Referenced by
</Tab>
)}
</Tabs>
@@ -41,7 +41,7 @@ export default class SocketPresence extends React.Component<Props> {
}
setupOnce = () => {
if (this.context && !this.previousContext) {
if (this.context && this.context !== this.previousContext) {
this.previousContext = this.context;
if (this.context.authenticated) {
+18 -6
View File
@@ -9,7 +9,7 @@ import Document from "models/Document";
import Button from "components/Button";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import { collectionUrl } from "utils/routeHelpers";
import { collectionUrl, documentUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
@@ -24,15 +24,27 @@ class DocumentDelete extends React.Component<Props> {
@observable isDeleting: boolean;
handleSubmit = async (ev: SyntheticEvent<>) => {
const { documents, document } = this.props;
ev.preventDefault();
this.isDeleting = true;
try {
await this.props.document.delete();
if (this.props.ui.activeDocumentId === this.props.document.id) {
this.props.history.push(
collectionUrl(this.props.document.collectionId)
);
await document.delete();
// only redirect if we're currently viewing the document that's deleted
if (this.props.ui.activeDocumentId === document.id) {
// If the document has a parent and it's available in the store then
// redirect to it
if (document.parentDocumentId) {
const parent = documents.get(document.parentDocumentId);
if (parent) {
this.props.history.push(documentUrl(parent));
return;
}
}
// otherwise, redirect to the collection home
this.props.history.push(collectionUrl(document.collectionId));
}
this.props.onSubmit();
} catch (err) {
+5 -2
View File
@@ -41,8 +41,10 @@ class Details extends React.Component<Props> {
clearTimeout(this.timeout);
}
handleSubmit = async (ev: SyntheticEvent<>) => {
ev.preventDefault();
handleSubmit = async (event: ?SyntheticEvent<>) => {
if (event) {
event.preventDefault();
}
try {
await this.props.auth.updateTeam({
@@ -66,6 +68,7 @@ class Details extends React.Component<Props> {
handleAvatarUpload = (avatarUrl: string) => {
this.avatarUrl = avatarUrl;
this.handleSubmit();
};
handleAvatarError = (error: ?string) => {
+1 -1
View File
@@ -86,7 +86,7 @@ class Events extends React.Component<Props> {
) : (
<>
{events.orderedData.map((event) => (
<EventListItem event={event} />
<EventListItem event={event} key={event.id} />
))}
{this.allowLoadMore && (
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
@@ -12,6 +12,8 @@ import LoadingIndicator from "components/LoadingIndicator";
import Modal from "components/Modal";
import { uploadFile, dataUrlToBlob } from "utils/uploadFile";
const EMPTY_OBJECT = {};
type Props = {
children?: React.Node,
onSuccess: (string) => void | Promise<void>,
@@ -123,10 +125,8 @@ class ImageUpload extends React.Component<Props> {
<Dropzone
accept="image/png, image/jpeg"
onDropAccepted={this.onDropAccepted}
style={{}}
style={EMPTY_OBJECT}
disablePreview
onSuccess={this.props.onSuccess}
onError={this.props.onError}
>
{this.props.children}
</Dropzone>
+26 -4
View File
@@ -12,11 +12,9 @@ import {
} from "lodash";
import { observable, action, computed, runInAction } from "mobx";
import naturalSort from "shared/utils/naturalSort";
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 { client } from "utils/ApiClient";
@@ -435,6 +433,7 @@ export default class DocumentsStore extends BaseStore<Document> {
res.data.documents.forEach(this.add);
res.data.collections.forEach(this.rootStore.collections.add);
this.addPolicies(res.policies);
};
@action
@@ -500,6 +499,13 @@ export default class DocumentsStore extends BaseStore<Document> {
this.recentlyViewedIds = without(this.recentlyViewedIds, document.id);
});
// check to see if we have any shares related to this document already
// loaded in local state. If so we can go ahead and remove those too.
const share = this.rootStore.shares.getByDocumentId(document.id);
if (share) {
this.rootStore.shares.remove(share.id);
}
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
}
@@ -520,10 +526,10 @@ export default class DocumentsStore extends BaseStore<Document> {
};
@action
restore = async (document: Document, revision?: Revision) => {
restore = async (document: Document, options = {}) => {
const res = await client.post("/documents.restore", {
id: document.id,
revisionId: revision ? revision.id : undefined,
...options,
});
runInAction("Document#restore", () => {
invariant(res && res.data, "Data should be available");
@@ -535,6 +541,22 @@ export default class DocumentsStore extends BaseStore<Document> {
if (collection) collection.refresh();
};
@action
unpublish = async (document: Document) => {
const res = await client.post("/documents.unpublish", {
id: document.id,
});
runInAction("Document#unpublish", () => {
invariant(res && res.data, "Data should be available");
document.updateFromJson(res.data);
this.addPolicies(res.policies);
});
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
};
pin = (document: Document) => {
return client.post("/documents.pin", { id: document.id });
};
+12 -12
View File
@@ -109,34 +109,34 @@ class UiStore {
};
@action
enableEditMode() {
enableEditMode = () => {
this.editMode = true;
}
};
@action
disableEditMode() {
disableEditMode = () => {
this.editMode = false;
}
};
@action
enableProgressBar() {
enableProgressBar = () => {
this.progressBarVisible = true;
}
};
@action
disableProgressBar() {
disableProgressBar = () => {
this.progressBarVisible = false;
}
};
@action
toggleMobileSidebar() {
toggleMobileSidebar = () => {
this.mobileSidebarVisible = !this.mobileSidebarVisible;
}
};
@action
hideMobileSidebar() {
hideMobileSidebar = () => {
this.mobileSidebarVisible = false;
}
};
@action
showToast = (
+25
View File
@@ -0,0 +1,25 @@
// @flow
let hidden = "hidden";
let visibilityChange = "visibilitychange";
if ("hidden" in document) {
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if ("mozHidden" in document) {
// Firefox up to v17
hidden = "mozHidden";
visibilityChange = "mozvisibilitychange";
} else if ("webkitHidden" in document) {
// Chrome up to v32, Android up to v4.4, Blackberry up to v10
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
export function getVisibilityListener(): string {
return visibilityChange;
}
export function getPageVisible(): boolean {
// $FlowFixMe
return !document[hidden];
}
+1 -2
View File
@@ -17,9 +17,8 @@ if (process.env.AWS_ACCESS_KEY_ID) {
"AWS_REGION",
"AWS_SECRET_ACCESS_KEY",
"AWS_S3_UPLOAD_BUCKET_URL",
"AWS_S3_UPLOAD_BUCKET_NAME",
"AWS_S3_UPLOAD_MAX_SIZE",
].forEach(key => {
].forEach((key) => {
if (!process.env[key]) {
console.error(`The ${key} env variable must be set when using AWS`);
// $FlowFixMe
+15 -15
View File
@@ -6,16 +6,16 @@
"clean": "rimraf dist",
"build:webpack": "NODE_ENV=production webpack --config webpack.config.prod.js",
"build:analyze": "NODE_ENV=production webpack --config webpack.config.prod.js --json > stats.json",
"build": "npm run clean && npm run build:webpack",
"build": "yarn clean && yarn build:webpack",
"start": "NODE_ENV=production node index.js",
"dev": "NODE_ENV=development nodemon --watch server index.js",
"lint": "eslint app server shared",
"flow": "flow",
"deploy": "git push heroku master",
"heroku-postbuild": "npm run build && npm run sequelize:migrate",
"heroku-postbuild": "yarn build && yarn sequelize:migrate",
"sequelize:create-migration": "sequelize migration:create",
"sequelize:migrate": "sequelize db:migrate",
"test": "npm run test:app && npm run test:server",
"test": "yarn test:app && yarn test:server",
"test:app": "jest",
"test:server": "jest --config=server/.jestconfig.json --runInBand --forceExit",
"test:watch": "jest --config=server/.jestconfig.json --runInBand --forceExit --watchAll"
@@ -48,7 +48,7 @@
]
},
"engines": {
"node": ">= 12 <14"
"node": ">= 12 <15"
},
"repository": {
"type": "git",
@@ -66,7 +66,7 @@
"@babel/preset-react": "^7.10.4",
"@babel/register": "^7.10.5",
"@rehooks/window-scroll-position": "^1.0.1",
"@sentry/node": "^5.12.2",
"@sentry/node": "^5.22.3",
"@tippy.js/react": "^2.2.2",
"@tommoor/remove-markdown": "0.3.1",
"autotrack": "^2.4.1",
@@ -115,13 +115,13 @@
"koa-static": "^4.0.1",
"lodash": "^4.17.19",
"mobx": "4.6.0",
"mobx-react": "^5.4.2",
"mobx-react": "^6.2.5",
"natural-sort": "^1.0.0",
"nodemailer": "^4.4.0",
"outline-icons": "^1.21.0-6",
"oy-vey": "^0.10.0",
"pg": "^6.1.5",
"pg-hstore": "2.3.2",
"pg": "^8.3.0",
"pg-hstore": "^2.3.3",
"polished": "3.6.5",
"query-string": "^4.3.4",
"randomstring": "1.1.5",
@@ -138,11 +138,11 @@
"react-portal": "^4.0.0",
"react-router-dom": "^5.1.2",
"react-waypoint": "^9.0.2",
"rich-markdown-editor": "^10.6.1",
"rich-markdown-editor": "^11.0.0-1",
"semver": "^7.3.2",
"sequelize": "^5.21.1",
"sequelize-cli": "^5.5.0",
"sequelize-encrypted": "^0.1.0",
"sequelize": "^6.3.4",
"sequelize-cli": "^6.2.0",
"sequelize-encrypted": "^1.0.0",
"slate": "0.45.0",
"slate-md-serializer": "5.5.4",
"slug": "^1.0.0",
@@ -170,13 +170,13 @@
"eslint-plugin-jsx-a11y": "^6.1.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.1.0",
"fetch-test-server": "^1.1.0",
"flow-bin": "^0.104.0",
"html-webpack-plugin": "3.2.0",
"jest-cli": "^26.0.0",
"koa-webpack-dev-middleware": "^1.4.5",
"koa-webpack-hot-middleware": "^1.0.3",
"mobx-react-devtools": "^6.0.3",
"nodemon": "^1.19.4",
"prettier": "^2.0.5",
"rimraf": "^2.5.4",
@@ -190,5 +190,5 @@
"dot-prop": "^5.2.0",
"js-yaml": "^3.13.1"
},
"version": "0.46.0"
}
"version": "0.47.1"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+68 -13
View File
@@ -2,23 +2,23 @@
import Router from "koa-router";
import Sequelize from "sequelize";
import documentMover from "../commands/documentMover";
import { InvalidRequestError } from "../errors";
import { NotFoundError, InvalidRequestError } from "../errors";
import auth from "../middlewares/authentication";
import {
Backlink,
Collection,
Document,
Event,
Revision,
Share,
Star,
View,
Revision,
Backlink,
User,
View,
} from "../models";
import policy from "../policies";
import {
presentDocument,
presentCollection,
presentDocument,
presentPolicies,
} from "../presenters";
import { sequelize } from "../sequelize";
@@ -210,7 +210,7 @@ router.post("documents.deleted", auth(), pagination(), async (ctx) => {
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const collectionIds = await user.collectionIds();
const collectionIds = await user.collectionIds({ paranoid: false });
const collectionScope = { method: ["withCollection", user.id] };
const documents = await Document.scope(collectionScope).findAll({
@@ -407,11 +407,19 @@ async function loadDocument({ id, shareId, user }) {
authorize(user, "read", document);
}
} else {
document = await Document.findByPk(
id,
user ? { userId: user.id } : undefined
);
authorize(user, "read", document);
document = await Document.findByPk(id, {
userId: user ? user.id : undefined,
paranoid: false,
});
if (!document) {
throw new NotFoundError();
}
if (document.deletedAt) {
authorize(user, "restore", document);
} else {
authorize(user, "read", document);
}
}
return document;
@@ -444,7 +452,7 @@ router.post("documents.export", auth({ required: false }), async (ctx) => {
});
router.post("documents.restore", auth(), async (ctx) => {
const { id, revisionId } = ctx.body;
const { id, collectionId, revisionId } = ctx.body;
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
@@ -452,6 +460,21 @@ router.post("documents.restore", auth(), async (ctx) => {
userId: user.id,
paranoid: false,
});
if (!document) {
throw new NotFoundError();
}
if (collectionId) {
ctx.assertUuid(collectionId, "collectionId must be a uuid");
authorize(user, "restore", document);
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collectionId);
authorize(user, "update", collection);
document.collectionId = collectionId;
}
if (document.deletedAt) {
authorize(user, "restore", document);
@@ -938,6 +961,11 @@ router.post("documents.move", auth(), async (ctx) => {
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, "move", document);
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collectionId);
authorize(user, "update", collection);
if (parentDocumentId) {
const parent = await Document.findByPk(parentDocumentId, {
userId: user.id,
@@ -1001,7 +1029,7 @@ router.post("documents.delete", auth(), async (ctx) => {
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, "delete", document);
await document.delete();
await document.delete(user.id);
await Event.create({
name: "documents.delete",
@@ -1018,4 +1046,31 @@ router.post("documents.delete", auth(), async (ctx) => {
};
});
router.post("documents.unpublish", auth(), async (ctx) => {
const { id } = ctx.body;
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, "unpublish", document);
await document.unpublish();
await Event.create({
name: "documents.unpublish",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
actorId: user.id,
data: { title: document.title },
ip: ctx.request.ip,
});
ctx.body = {
data: await presentDocument(document),
policies: presentPolicies(user, [document]),
};
});
export default router;
+182
View File
@@ -185,6 +185,15 @@ describe("#documents.info", () => {
expect(body.data.id).toEqual(document.id);
});
it("should not error if document doesn't exist", async () => {
const user = await buildUser();
const res = await server.post("/api/documents.info", {
body: { token: user.getJwtToken(), id: "test" },
});
expect(res.status).toEqual(404);
});
it("should require authorization without token", async () => {
const { document } = await seed();
const res = await server.post("/api/documents.info", {
@@ -1137,7 +1146,109 @@ describe("#documents.pin", () => {
});
});
describe("#documents.move", () => {
it("should move the document", async () => {
const { user, document } = await seed();
const collection = await buildCollection({ teamId: user.teamId });
const res = await server.post("/api/documents.move", {
body: {
token: user.getJwtToken(),
id: document.id,
collectionId: collection.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.documents[0].collectionId).toEqual(collection.id);
});
it("should not allow moving the document to a collection the user cannot access", async () => {
const { user, document } = await seed();
const collection = await buildCollection();
const res = await server.post("/api/documents.move", {
body: {
token: user.getJwtToken(),
id: document.id,
collectionId: collection.id,
},
});
expect(res.status).toEqual(403);
});
it("should require authentication", async () => {
const res = await server.post("/api/documents.move");
expect(res.status).toEqual(401);
});
it("should require authorization", async () => {
const { document, collection } = await seed();
const user = await buildUser();
const res = await server.post("/api/documents.move", {
body: {
token: user.getJwtToken(),
id: document.id,
collectionId: collection.id,
},
});
expect(res.status).toEqual(403);
});
});
describe("#documents.restore", () => {
it("should allow restore of trashed documents", async () => {
const { user, document } = await seed();
await document.destroy(user.id);
const res = await server.post("/api/documents.restore", {
body: { token: user.getJwtToken(), id: document.id },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.deletedAt).toEqual(null);
});
it("should allow restore of trashed documents with collectionId", async () => {
const { user, document } = await seed();
const collection = await buildCollection({
userId: user.id,
teamId: user.teamId,
});
await document.destroy(user.id);
const res = await server.post("/api/documents.restore", {
body: {
token: user.getJwtToken(),
id: document.id,
collectionId: collection.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.deletedAt).toEqual(null);
expect(body.data.collectionId).toEqual(collection.id);
});
it("should now allow restore of trashed documents to collection user cannot access", async () => {
const { user, document } = await seed();
const collection = await buildCollection();
await document.destroy(user.id);
const res = await server.post("/api/documents.restore", {
body: {
token: user.getJwtToken(),
id: document.id,
collectionId: collection.id,
},
});
expect(res.status).toEqual(403);
});
it("should allow restore of archived documents", async () => {
const { user, document } = await seed();
await document.archive(user.id);
@@ -1146,6 +1257,8 @@ describe("#documents.restore", () => {
body: { token: user.getJwtToken(), id: document.id },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.archivedAt).toEqual(null);
});
@@ -1164,6 +1277,8 @@ describe("#documents.restore", () => {
body: { token: user.getJwtToken(), id: childDocument.id },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.parentDocumentId).toEqual(undefined);
expect(body.data.archivedAt).toEqual(null);
});
@@ -1184,6 +1299,8 @@ describe("#documents.restore", () => {
body: { token: user.getJwtToken(), id: document.id, revisionId },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.text).toEqual(previousText);
});
@@ -1201,6 +1318,15 @@ describe("#documents.restore", () => {
expect(res.status).toEqual(403);
});
it("should not error if document doesn't exist", async () => {
const user = await buildUser();
const res = await server.post("/api/documents.restore", {
body: { token: user.getJwtToken(), id: "test" },
});
expect(res.status).toEqual(404);
});
it("should require authentication", async () => {
const res = await server.post("/api/documents.restore");
const body = await res.json();
@@ -1736,3 +1862,59 @@ describe("#documents.delete", () => {
expect(body).toMatchSnapshot();
});
});
describe("#documents.unpublish", () => {
it("should unpublish a document", async () => {
const { user, document } = await seed();
const res = await server.post("/api/documents.unpublish", {
body: { token: user.getJwtToken(), id: document.id },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.id).toEqual(document.id);
expect(body.data.publishedAt).toBeNull();
});
it("should fail to unpublish a draft document", async () => {
const { user, document } = await seed();
document.publishedAt = null;
await document.save();
const res = await server.post("/api/documents.unpublish", {
body: { token: user.getJwtToken(), id: document.id },
});
expect(res.status).toEqual(403);
});
it("should fail to unpublish a deleted document", async () => {
const { user, document } = await seed();
await document.delete();
const res = await server.post("/api/documents.unpublish", {
body: { token: user.getJwtToken(), id: document.id },
});
expect(res.status).toEqual(403);
});
it("should fail to unpublish a archived document", async () => {
const { user, document } = await seed();
await document.archive();
const res = await server.post("/api/documents.unpublish", {
body: { token: user.getJwtToken(), id: document.id },
});
expect(res.status).toEqual(403);
});
it("should require authentication", async () => {
const { document } = await seed();
const res = await server.post("/api/documents.unpublish", {
body: { id: document.id },
});
expect(res.status).toEqual(401);
});
});
+1 -2
View File
@@ -16,8 +16,7 @@ router.post("events.list", auth(), pagination(), async (ctx) => {
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const paranoid = false;
const collectionIds = await user.collectionIds(paranoid);
const collectionIds = await user.collectionIds({ paranoid: false });
let where = {
name: Event.ACTIVITY_EVENTS,
+9 -3
View File
@@ -21,13 +21,15 @@ router.post("shares.info", auth(), async (ctx) => {
where: id
? {
id,
revokedAt: { [Op.eq]: null },
}
: {
documentId,
userId: user.id,
revokedAt: { [Op.eq]: null },
},
});
if (!share) {
if (!share || !share.document) {
throw new NotFoundError();
}
@@ -63,6 +65,7 @@ router.post("shares.list", auth(), pagination(), async (ctx) => {
{
model: Document,
required: true,
paranoid: true,
as: "document",
where: {
collectionId: collectionIds,
@@ -168,9 +171,12 @@ router.post("shares.revoke", auth(), async (ctx) => {
const share = await Share.findByPk(id);
authorize(user, "revoke", share);
await share.revoke(user.id);
const document = await Document.findByPk(share.documentId);
if (!document) {
throw new NotFoundError();
}
await share.revoke(user.id);
await Event.create({
name: "shares.revoke",
+63
View File
@@ -70,6 +70,25 @@ describe("#shares.list", () => {
expect(body.data.length).toEqual(0);
});
it("should not return shares to deleted documents", async () => {
const { user, document } = await seed();
await buildShare({
documentId: document.id,
teamId: user.teamId,
userId: user.id,
});
await document.delete(user.id);
const res = await server.post("/api/shares.list", {
body: { token: user.getJwtToken() },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(0);
});
it("admins should return shares created by all users", async () => {
const { user, admin, document } = await seed();
const share = await buildShare({
@@ -268,6 +287,34 @@ describe("#shares.info", () => {
expect(res.status).toEqual(404);
});
it("should not find revoked share", async () => {
const { user, document } = await seed();
const share = await buildShare({
documentId: document.id,
teamId: user.teamId,
userId: user.id,
});
await share.revoke();
const res = await server.post("/api/shares.info", {
body: { token: user.getJwtToken(), documentId: document.id },
});
expect(res.status).toEqual(404);
});
it("should not find share for deleted document", async () => {
const { user, document } = await seed();
await buildShare({
documentId: document.id,
teamId: user.teamId,
userId: user.id,
});
await document.delete(user.id);
const res = await server.post("/api/shares.info", {
body: { token: user.getJwtToken(), documentId: document.id },
});
expect(res.status).toEqual(404);
});
it("should require authentication", async () => {
const { user, document } = await seed();
const share = await buildShare({
@@ -382,6 +429,22 @@ describe("#shares.revoke", () => {
expect(res.status).toEqual(200);
});
it("should 404 if shares document is deleted", async () => {
const { user, document } = await seed();
const share = await buildShare({
documentId: document.id,
teamId: user.teamId,
userId: user.id,
});
await document.delete(user.id);
const res = await server.post("/api/shares.revoke", {
body: { token: user.getJwtToken(), id: share.id },
});
expect(res.status).toEqual(404);
});
it("should allow admin to revoke a share", async () => {
const { user, admin, document } = await seed();
const share = await buildShare({
+84 -66
View File
@@ -14,7 +14,7 @@ export default async function documentMover({
user: Context,
document: Document,
collectionId: string,
parentDocumentId: string,
parentDocumentId?: string,
index?: number,
ip: string,
}) {
@@ -22,80 +22,98 @@ export default async function documentMover({
const result = { collections: [], documents: [] };
const collectionChanged = collectionId !== document.collectionId;
try {
transaction = await sequelize.transaction();
if (document.template) {
if (!collectionChanged) {
return result;
}
// remove from original collection
const collection = await document.getCollection({ transaction });
const documentJson = await collection.removeDocumentInStructure(document, {
save: false,
});
// if the collection is the same then it will get saved below, this
// line prevents a pointless intermediate save from occurring.
if (collectionChanged) await collection.save({ transaction });
// add to new collection (may be the same)
document.collectionId = collectionId;
document.parentDocumentId = parentDocumentId;
document.parentDocumentId = null;
const newCollection: Collection = collectionChanged
? await Collection.findByPk(collectionId, { transaction })
: collection;
await newCollection.addDocumentToStructure(document, index, {
documentJson,
});
result.collections.push(collection);
// if collection does not remain the same loop through children and change their
// collectionId too. This includes archived children, otherwise their collection
// would be wrong once restored.
if (collectionChanged) {
result.collections.push(newCollection);
const loopChildren = async (documentId) => {
const childDocuments = await Document.findAll({
where: { parentDocumentId: documentId },
});
await Promise.all(
childDocuments.map(async (child) => {
await loopChildren(child.id);
await child.update({ collectionId }, { transaction });
child.collection = newCollection;
result.documents.push(child);
})
);
};
await loopChildren(document.id);
}
await document.save({ transaction });
await document.save();
result.documents.push(document);
} else {
try {
transaction = await sequelize.transaction();
await transaction.commit();
// remove from original collection
const collection = await Collection.findByPk(document.collectionId, {
transaction,
paranoid: false,
});
const documentJson = await collection.removeDocumentInStructure(
document,
{
save: false,
}
);
await Event.create({
name: "documents.move",
actorId: user.id,
documentId: document.id,
collectionId,
teamId: document.teamId,
data: {
title: document.title,
collectionIds: result.collections.map((c) => c.id),
documentIds: result.documents.map((d) => d.id),
},
ip,
});
} catch (err) {
if (transaction) {
await transaction.rollback();
// if the collection is the same then it will get saved below, this
// line prevents a pointless intermediate save from occurring.
if (collectionChanged) await collection.save({ transaction });
// add to new collection (may be the same)
document.collectionId = collectionId;
document.parentDocumentId = parentDocumentId;
const newCollection: Collection = collectionChanged
? await Collection.findByPk(collectionId, { transaction })
: collection;
await newCollection.addDocumentToStructure(document, index, {
documentJson,
});
result.collections.push(collection);
// if collection does not remain the same loop through children and change their
// collectionId too. This includes archived children, otherwise their collection
// would be wrong once restored.
if (collectionChanged) {
result.collections.push(newCollection);
const loopChildren = async (documentId) => {
const childDocuments = await Document.findAll({
where: { parentDocumentId: documentId },
});
await Promise.all(
childDocuments.map(async (child) => {
await loopChildren(child.id);
await child.update({ collectionId }, { transaction });
child.collection = newCollection;
result.documents.push(child);
})
);
};
await loopChildren(document.id);
}
await document.save({ transaction });
result.documents.push(document);
await transaction.commit();
} catch (err) {
if (transaction) {
await transaction.rollback();
}
throw err;
}
throw err;
}
await Event.create({
name: "documents.move",
actorId: user.id,
documentId: document.id,
collectionId,
teamId: document.teamId,
data: {
title: document.title,
collectionIds: result.collections.map((c) => c.id),
documentIds: result.documents.map((d) => d.id),
},
ip,
});
// we need to send all updated models back to the client
return result;
}
+6 -1
View File
@@ -9,6 +9,11 @@
},
"production": {
"use_env_variable": "DATABASE_URL",
"dialect": "postgres"
"dialect": "postgres",
"dialectOptions": {
"ssl": {
"rejectUnauthorized": false
}
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ const Authentication = sequelize.define("authentication", {
},
service: DataTypes.STRING,
scopes: DataTypes.ARRAY(DataTypes.STRING),
token: encryptedFields.vault("token"),
token: encryptedFields().vault("token"),
});
Authentication.associate = (models) => {
+14 -6
View File
@@ -250,11 +250,13 @@ Collection.prototype.addDocumentToStructure = async function (
}
// Sequelize doesn't seem to set the value with splice on JSONB field
this.documentStructure = this.documentStructure;
// https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937
this.changed("documentStructure", true);
if (options.save !== false) {
await this.save({
...options,
fields: ["documentStructure"],
transaction,
});
if (transaction) {
@@ -302,7 +304,12 @@ Collection.prototype.updateDocument = async function (
};
this.documentStructure = updateChildren(this.documentStructure);
await this.save({ transaction });
// Sequelize doesn't seem to set the value with splice on JSONB field
// https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937
this.changed("documentStructure", true);
await this.save({ fields: ["documentStructure"], transaction });
await transaction.commit();
} catch (err) {
if (transaction) {
@@ -355,10 +362,11 @@ Collection.prototype.removeDocumentInStructure = async function (
document.id
);
await this.save({
...options,
transaction,
});
// Sequelize doesn't seem to set the value with splice on JSONB field
// https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937
this.changed("documentStructure", true);
await this.save({ ...options, fields: ["documentStructure"], transaction });
await transaction.commit();
} catch (err) {
if (transaction) {
+7 -3
View File
@@ -144,7 +144,9 @@ describe("#updateDocument", () => {
await collection.updateDocument(newDocument);
expect(collection.documentStructure[0].children[0].title).toBe(
const reloaded = await Collection.findByPk(collection.id);
expect(reloaded.documentStructure[0].children[0].title).toBe(
"Updated title"
);
});
@@ -224,8 +226,10 @@ describe("#removeDocument", () => {
// Remove the document
await collection.deleteDocument(newDocument);
expect(collection.documentStructure.length).toBe(1);
expect(collection.documentStructure[0].children.length).toBe(0);
const reloaded = await Collection.findByPk(collection.id);
expect(reloaded.documentStructure.length).toBe(1);
expect(reloaded.documentStructure[0].children.length).toBe(0);
const collectionDocuments = await Document.findAndCountAll({
where: {
+39 -25
View File
@@ -1,10 +1,9 @@
// @flow
import removeMarkdown from "@tommoor/remove-markdown";
import { map, find, compact, uniq } from "lodash";
import { compact, find, map, uniq } from "lodash";
import randomstring from "randomstring";
import Sequelize, { type Transaction } from "sequelize";
import Sequelize, { Transaction } from "sequelize";
import MarkdownSerializer from "slate-md-serializer";
import isUUID from "validator/lib/isUUID";
import parseTitle from "../../shared/utils/parseTitle";
import unescape from "../../shared/utils/unescape";
@@ -19,15 +18,14 @@ const serializer = new MarkdownSerializer();
export const DOCUMENT_VERSION = 2;
const createRevision = (doc, options = {}) => {
const createRevision = async (doc, options = {}) => {
// we don't create revisions for autosaves
if (options.autosave) return;
const previous = await Revision.findLatest(doc.id);
// we don't create revisions if identical to previous
if (
doc.text === doc.previous("text") &&
doc.title === doc.previous("title")
) {
if (previous && doc.text === previous.text && doc.title === previous.title) {
return;
}
@@ -556,6 +554,18 @@ Document.prototype.publish = async function (options) {
return this;
};
Document.prototype.unpublish = async function (options) {
if (!this.publishedAt) return this;
const collection = await this.getCollection();
await collection.removeDocumentInStructure(this);
this.publishedAt = null;
await this.save(options);
return this;
};
// Moves a document from being visible to the team within a collection
// to the archived area, where it can be subsequently restored.
Document.prototype.archive = async function (userId) {
@@ -570,7 +580,7 @@ Document.prototype.archive = async function (userId) {
};
// Restore an archived document back to being visible to the team
Document.prototype.unarchive = async function (userId) {
Document.prototype.unarchive = async function (userId: string) {
const collection = await this.getCollection();
// check to see if the documents parent hasn't been archived also
@@ -602,23 +612,27 @@ Document.prototype.unarchive = async function (userId) {
};
// Delete a document, archived or otherwise.
Document.prototype.delete = function (options) {
return sequelize.transaction(async (transaction: Transaction): Promise<*> => {
if (!this.archivedAt) {
// delete any children and remove from the document structure
const collection = await this.getCollection();
if (collection) await collection.deleteDocument(this, { transaction });
Document.prototype.delete = function (userId: string) {
return sequelize.transaction(
async (transaction: Transaction): Promise<Document> => {
if (!this.archivedAt && !this.template) {
// delete any children and remove from the document structure
const collection = await this.getCollection();
if (collection) await collection.deleteDocument(this, { transaction });
}
await Revision.destroy({
where: { documentId: this.id },
transaction,
});
this.lastModifiedById = userId;
this.deletedAt = new Date();
await this.save({ transaction });
return this;
}
await Revision.destroy({
where: { documentId: this.id },
transaction,
});
await this.destroy({ transaction, ...options });
return this;
});
);
};
Document.prototype.getTimestamp = function () {
+51 -2
View File
@@ -1,11 +1,47 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import { Document } from "../models";
import { buildDocument, buildCollection, buildTeam } from "../test/factories";
import { Document, Revision } from "../models";
import {
buildDocument,
buildCollection,
buildTeam,
buildUser,
} from "../test/factories";
import { flushdb } from "../test/support";
beforeEach(() => flushdb());
beforeEach(jest.resetAllMocks);
describe("#createRevision", () => {
test("should create revision on document creation", async () => {
const document = await buildDocument();
document.title = "Changed";
await document.save({ autosave: true });
const amount = await Revision.count({ where: { documentId: document.id } });
expect(amount).toBe(1);
});
test("should create revision on document update identical to previous autosave", async () => {
const document = await buildDocument();
document.title = "Changed";
await document.save({ autosave: true });
document.title = "Changed";
await document.save();
const amount = await Revision.count({ where: { documentId: document.id } });
expect(amount).toBe(2);
});
test("should not create revision if autosave", async () => {
const document = await buildDocument();
const amount = await Revision.count({ where: { documentId: document.id } });
expect(amount).toBe(1);
});
});
describe("#getSummary", () => {
test("should strip markdown", async () => {
const document = await buildDocument({
@@ -192,3 +228,16 @@ describe("#searchForTeam", () => {
expect(results.length).toBe(0);
});
});
describe("#delete", () => {
test("should soft delete and set last modified", async () => {
let document = await buildDocument();
let user = await buildUser();
await document.delete(user.id);
document = await Document.findByPk(document.id, { paranoid: false });
expect(document.lastModifiedById).toBe(user.id);
expect(document.deletedAt).toBeTruthy();
});
});
+9
View File
@@ -40,6 +40,15 @@ Revision.associate = (models) => {
);
};
Revision.findLatest = function (documentId) {
return Revision.findOne({
where: {
documentId,
},
order: [["createdAt", "DESC"]],
});
};
Revision.prototype.migrateVersion = function () {
let migrated = false;
+27
View File
@@ -0,0 +1,27 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import { Revision } from "../models";
import { buildDocument } from "../test/factories";
import { flushdb } from "../test/support";
beforeEach(() => flushdb());
beforeEach(jest.resetAllMocks);
describe("#findLatest", () => {
test("should return latest revision", async () => {
const document = await buildDocument({
title: "Title",
text: "Content",
});
document.title = "Changed 1";
await document.save();
document.title = "Changed 2";
await document.save();
const revision = await Revision.findLatest(document.id);
expect(revision.title).toBe("Changed 2");
expect(revision.text).toBe("Content");
});
});
+4 -3
View File
@@ -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,
@@ -71,13 +71,14 @@ User.associate = (models) => {
};
// Instance methods
User.prototype.collectionIds = async function (paranoid: boolean = true) {
User.prototype.collectionIds = async function (options = {}) {
const collectionStubs = await Collection.scope({
method: ["withMembership", this.id],
}).findAll({
attributes: ["id", "private"],
where: { teamId: this.teamId },
paranoid,
paranoid: true,
...options,
});
return collectionStubs
+44 -6
View File
@@ -58,7 +58,7 @@ allow(User, "update", Document, (user, document) => {
allow(User, "createChildDocument", Document, (user, document) => {
if (document.archivedAt) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
if (document.template) return false;
if (!document.publishedAt) return false;
@@ -71,7 +71,21 @@ allow(User, "createChildDocument", Document, (user, document) => {
return user.teamId === document.teamId;
});
allow(User, ["move", "pin", "unpin"], Document, (user, document) => {
allow(User, "move", 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?"
);
if (cannot(user, "update", document.collection)) return false;
return user.teamId === document.teamId;
});
allow(User, ["pin", "unpin"], Document, (user, document) => {
if (document.archivedAt) return false;
if (document.deletedAt) return false;
if (document.template) return false;
@@ -112,16 +126,16 @@ allow(User, "restore", Document, (user, document) => {
});
allow(User, "archive", Document, (user, document) => {
if (!document.publishedAt) return false;
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?"
);
if (cannot(user, "update", document.collection)) return false;
if (!document.publishedAt) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
return user.teamId === document.teamId;
});
@@ -143,3 +157,27 @@ allow(
Revision,
(document, revision) => document.id === revision.documentId
);
allow(User, "unpublish", Document, (user, document) => {
invariant(
document.collection,
"collection is missing, did you forget to include in the query scope?"
);
if (!document.publishedAt || !!document.deletedAt || !!document.archivedAt)
return false;
if (cannot(user, "update", document.collection)) return false;
const documentID = document.id;
const hasChild = (documents) =>
documents.some((doc) => {
if (doc.id === documentID) return doc.children.length > 0;
return hasChild(doc.children);
});
return (
!hasChild(document.collection.documentStructure) &&
user.teamId === document.teamId
);
});
+7 -5
View File
@@ -72,11 +72,13 @@ export default async function present(document: Document, options: ?Options) {
data.updatedBy = presentUser(document.updatedBy);
// TODO: This could be further optimized
data.collaborators = await User.findAll({
where: {
id: takeRight(document.collaboratorIds, 10) || [],
},
}).map(presentUser);
data.collaborators = (
await User.findAll({
where: {
id: takeRight(document.collaboratorIds, 10) || [],
},
})
).map(presentUser);
}
return data;
+14 -5
View File
@@ -3,16 +3,25 @@ import debug from "debug";
import Sequelize from "sequelize";
import EncryptedField from "sequelize-encrypted";
export const encryptedFields = EncryptedField(
Sequelize,
process.env.SECRET_KEY
);
const isProduction = process.env.NODE_ENV === "production";
const isSSLDisabled = process.env.PGSSLMODE === "disable";
export const encryptedFields = () =>
EncryptedField(Sequelize, process.env.SECRET_KEY);
export const DataTypes = Sequelize;
export const Op = Sequelize.Op;
export const sequelize = new Sequelize(process.env.DATABASE_URL, {
// logging: console.log,
logging: debug("sql"),
typeValidation: true,
dialectOptions: {
ssl:
isProduction && !isSSLDisabled
? {
// Ref.: https://github.com/brianc/node-postgres/issues/2009
rejectUnauthorized: false,
}
: false,
},
});
+3 -1
View File
@@ -55,7 +55,9 @@ export default class Backlinks {
await Promise.all(
addedLinkIds.map(async (linkId) => {
const linkedDocument = await Document.findByPk(linkId);
if (linkedDocument.id === event.documentId) return;
if (!linkedDocument || linkedDocument.id === event.documentId) {
return;
}
await Backlink.findOrCreate({
where: {
+31 -27
View File
@@ -26,22 +26,24 @@ export default class Websockets {
paranoid: false,
});
return socketio
.to(`collection-${document.collectionId}`)
.emit("entities", {
event: event.name,
documentIds: [
{
id: document.id,
updatedAt: document.updatedAt,
},
],
collectionIds: [
{
id: document.collectionId,
},
],
});
const channel = document.publishedAt
? `collection-${document.collectionId}`
: `user-${event.actorId}`;
return socketio.to(channel).emit("entities", {
event: event.name,
documentIds: [
{
id: document.id,
updatedAt: document.updatedAt,
},
],
collectionIds: [
{
id: document.collectionId,
},
],
});
}
case "documents.delete": {
const document = await Document.findByPk(event.documentId, {
@@ -84,17 +86,19 @@ export default class Websockets {
paranoid: false,
});
return socketio
.to(`collection-${document.collectionId}`)
.emit("entities", {
event: event.name,
documentIds: [
{
id: document.id,
updatedAt: document.updatedAt,
},
],
});
const channel = document.publishedAt
? `collection-${document.collectionId}`
: `user-${event.actorId}`;
return socketio.to(channel).emit("entities", {
event: event.name,
documentIds: [
{
id: document.id,
updatedAt: document.updatedAt,
},
],
});
}
case "documents.create": {
const document = await Document.findByPk(event.documentId);
+3 -2
View File
@@ -31,8 +31,8 @@
<body>
<div id="root"></div>
<script>//inject-env//</script>
<script src="https://browser.sentry-cdn.com/5.12.1/bundle.min.js"
integrity="sha384-y+an4eARFKvjzOivf/Z7JtMJhaN6b+lLQ5oFbBbUwZNNVir39cYtkjW1r6Xjbxg3" crossorigin="anonymous">
<script src="https://browser.sentry-cdn.com/5.22.3/bundle.min.js"
integrity="sha384-A1qzcXXJWl+bzYr+r8AdFzSaLbdcbYRFmG37MEDKr4EYjtraUyoZ6UiMw31jHcV9" crossorigin="anonymous">
</script>
<script>
if ('//inject-sentry-dsn//') {
@@ -44,6 +44,7 @@
'NotFoundError',
'OfflineError',
'UpdateRequiredError',
'ChunkLoadError'
],
});
}
-1
View File
@@ -28,5 +28,4 @@ function runMigrations() {
runMigrations();
// This is needed for the relative manual mock to be picked up
// $FlowFixMe
jest.mock("../events");
+3 -1
View File
@@ -6,7 +6,9 @@ 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.queryGenerator.quoteTable(
typeof n === "string" ? n : n.tableName
);
});
const query = `TRUNCATE ${tables.join(", ")} CASCADE`;
+12 -15
View File
@@ -4,18 +4,18 @@ import * as Sentry from "@sentry/node";
import AWS from "aws-sdk";
import addHours from "date-fns/add_hours";
import format from "date-fns/format";
import invariant from "invariant";
import fetch from "isomorphic-fetch";
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
const AWS_REGION = process.env.AWS_REGION;
const AWS_S3_UPLOAD_BUCKET_NAME = process.env.AWS_S3_UPLOAD_BUCKET_NAME;
const AWS_S3_UPLOAD_BUCKET_NAME = process.env.AWS_S3_UPLOAD_BUCKET_NAME || "";
const AWS_S3_FORCE_PATH_STYLE = process.env.AWS_S3_FORCE_PATH_STYLE !== "false";
const s3 = new AWS.S3({
s3ForcePathStyle: true,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
s3ForcePathStyle: AWS_S3_FORCE_PATH_STYLE,
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
endpoint: new AWS.Endpoint(process.env.AWS_S3_UPLOAD_BUCKET_URL),
signatureVersion: "v4",
});
@@ -84,9 +84,9 @@ export const publicS3Endpoint = (isServerUpload?: boolean) => {
"localhost:"
).replace(/\/$/, "");
return `${host}/${isServerUpload && isDocker ? "s3/" : ""}${
process.env.AWS_S3_UPLOAD_BUCKET_NAME
}`;
return `${host}/${
isServerUpload && isDocker ? "s3/" : ""
}${AWS_S3_UPLOAD_BUCKET_NAME}`;
};
export const uploadToS3FromUrl = async (
@@ -94,8 +94,6 @@ export const uploadToS3FromUrl = async (
key: string,
acl: string
) => {
invariant(AWS_S3_UPLOAD_BUCKET_NAME, "AWS_S3_UPLOAD_BUCKET_NAME not set");
try {
// $FlowIssue https://github.com/facebook/flow/issues/2171
const res = await fetch(url);
@@ -103,7 +101,7 @@ export const uploadToS3FromUrl = async (
await s3
.putObject({
ACL: acl,
Bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME,
Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
Key: key,
ContentType: res.headers["content-type"],
ContentLength: res.headers["content-length"],
@@ -126,18 +124,17 @@ export const uploadToS3FromUrl = async (
export const deleteFromS3 = (key: string) => {
return s3
.deleteObject({
Bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME,
Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
Key: key,
})
.promise();
};
export const getSignedImageUrl = async (key: string) => {
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 = {
Bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME,
Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
Key: key,
Expires: 60,
};
@@ -149,7 +146,7 @@ export const getSignedImageUrl = async (key: string) => {
export const getImageByKey = async (key: string) => {
const params = {
Bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME,
Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
Key: key,
};
+213 -264
View File
@@ -2,7 +2,7 @@
# yarn lockfile v1
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.8.3":
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
@@ -40,7 +40,7 @@
semver "^5.4.1"
source-map "^0.5.0"
"@babel/generator@^7.11.0", "@babel/generator@^7.8.6":
"@babel/generator@^7.11.0":
version "7.11.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c"
integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==
@@ -130,7 +130,7 @@
"@babel/traverse" "^7.10.4"
"@babel/types" "^7.10.4"
"@babel/helper-function-name@^7.10.4", "@babel/helper-function-name@^7.8.3":
"@babel/helper-function-name@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
@@ -139,7 +139,7 @@
"@babel/template" "^7.10.4"
"@babel/types" "^7.10.4"
"@babel/helper-get-function-arity@^7.10.4", "@babel/helper-get-function-arity@^7.8.3":
"@babel/helper-get-function-arity@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
@@ -235,14 +235,14 @@
dependencies:
"@babel/types" "^7.11.0"
"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.8.3":
"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0":
version "7.11.0"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f"
integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==
dependencies:
"@babel/types" "^7.11.0"
"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.9.0":
"@babel/helper-validator-identifier@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
@@ -266,7 +266,7 @@
"@babel/traverse" "^7.10.4"
"@babel/types" "^7.10.4"
"@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4", "@babel/highlight@^7.8.3":
"@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143"
integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==
@@ -275,7 +275,7 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.7.0", "@babel/parser@^7.8.6":
"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.7.0":
version "7.11.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9"
integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA==
@@ -964,7 +964,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.8.3":
"@babel/template@^7.10.4", "@babel/template@^7.3.3":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==
@@ -988,7 +988,7 @@
globals "^11.1.0"
lodash "^4.17.19"
"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.8.7":
"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
version "7.11.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d"
integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==
@@ -1019,10 +1019,10 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@emotion/is-prop-valid@^0.8.3":
version "0.8.7"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.7.tgz#803449993f436f9a6c67752251ea3fc492a1044c"
integrity sha512-OPkKzUeiid0vEKjZqnGcy2mzxjIlCffin+L2C02pdz/bVlt5zZZE2VzO0D3XOPnH0NEeF21QNKSXiZphjr4xiQ==
"@emotion/is-prop-valid@^0.8.8":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
dependencies:
"@emotion/memoize" "0.7.4"
@@ -1367,83 +1367,72 @@
execa "^4.0.0"
java-properties "^1.0.0"
"@sentry/apm@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.15.4.tgz#59af766d2bb4c9d98eda5ddba7a32a79ecc807a2"
integrity sha512-gcW225Jls1ShyBXMWN6zZyuVJwBOIQ63sI+URI2NSFsdpBpdpZ8yennIm+oMlSfb25Nzt9SId7TRSjPhlSbTZQ==
"@sentry/core@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.3.tgz#030f435f2b518f282ba8bd954dac90cd70888bd7"
integrity sha512-eGL5uUarw3o4i9QUb9JoFHnhriPpWCaqeaIBB06HUpdcvhrjoowcKZj1+WPec5lFg5XusE35vez7z/FPzmJUDw==
dependencies:
"@sentry/browser" "5.15.4"
"@sentry/hub" "5.15.4"
"@sentry/minimal" "5.15.4"
"@sentry/types" "5.15.4"
"@sentry/utils" "5.15.4"
"@sentry/hub" "5.22.3"
"@sentry/minimal" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/browser@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.4.tgz#5a7e7bad088556665ed8e69bceb0e18784e4f6c7"
integrity sha512-l/auT1HtZM3KxjCGQHYO/K51ygnlcuOrM+7Ga8gUUbU9ZXDYw6jRi0+Af9aqXKmdDw1naNxr7OCSy6NBrLWVZw==
"@sentry/hub@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.3.tgz#08309a70d2ea8d5e313d05840c1711f34f2fffe5"
integrity sha512-INo47m6N5HFEs/7GMP9cqxOIt7rmRxdERunA3H2L37owjcr77MwHVeeJ9yawRS6FMtbWXplgWTyTIWIYOuqVbw==
dependencies:
"@sentry/core" "5.15.4"
"@sentry/types" "5.15.4"
"@sentry/utils" "5.15.4"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/core@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.15.4.tgz#08b617e093a636168be5aebad141d1f744217085"
integrity sha512-9KP4NM4SqfV5NixpvAymC7Nvp36Zj4dU2fowmxiq7OIbzTxGXDhwuN/t0Uh8xiqlkpkQqSECZ1OjSFXrBldetQ==
"@sentry/minimal@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.3.tgz#706e4029ae5494123d3875c658ba8911aa5cc440"
integrity sha512-HoINpYnVYCpNjn2XIPIlqH5o4BAITpTljXjtAftOx6Hzj+Opjg8tR8PWliyKDvkXPpc4kXK9D6TpEDw8MO0wZA==
dependencies:
"@sentry/hub" "5.15.4"
"@sentry/minimal" "5.15.4"
"@sentry/types" "5.15.4"
"@sentry/utils" "5.15.4"
"@sentry/hub" "5.22.3"
"@sentry/types" "5.22.3"
tslib "^1.9.3"
"@sentry/hub@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.15.4.tgz#cb64473725a60eec63b0be58ed1143eaaf894bee"
integrity sha512-1XJ1SVqadkbUT4zLS0TVIVl99si7oHizLmghR8LMFl5wOkGEgehHSoOydQkIAX2C7sJmaF5TZ47ORBHgkqclUg==
"@sentry/node@^5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.22.3.tgz#adea622eae6811e11edc8f34209c912caed91336"
integrity sha512-TCCKO7hJKiQi1nGmJcQfvbbqv98P08LULh7pb/NaO5pV20t1FtICfGx8UMpORRDehbcAiYq/f7rPOF6X/Xl5iw==
dependencies:
"@sentry/types" "5.15.4"
"@sentry/utils" "5.15.4"
tslib "^1.9.3"
"@sentry/minimal@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.15.4.tgz#113f01fefb86b7830994c3dfa7ad4889ba7b2003"
integrity sha512-GL4GZ3drS9ge+wmxkHBAMEwulaE7DMvAEfKQPDAjg2p3MfcCMhAYfuY4jJByAC9rg9OwBGGehz7UmhWMFjE0tw==
dependencies:
"@sentry/hub" "5.15.4"
"@sentry/types" "5.15.4"
tslib "^1.9.3"
"@sentry/node@^5.12.2":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.15.4.tgz#e7bc3962d321a12b633743200165ca5f1757cb68"
integrity sha512-OfdhNEvOJZ55ZkCUcVgctjaZkOw7rmLzO5VyDTSgevA4uLsPaTNXSAeK2GSQBXc5J0KdRpNz4sSIyuxOS4Z7Vg==
dependencies:
"@sentry/apm" "5.15.4"
"@sentry/core" "5.15.4"
"@sentry/hub" "5.15.4"
"@sentry/types" "5.15.4"
"@sentry/utils" "5.15.4"
cookie "^0.3.1"
https-proxy-agent "^4.0.0"
"@sentry/core" "5.22.3"
"@sentry/hub" "5.22.3"
"@sentry/tracing" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
cookie "^0.4.1"
https-proxy-agent "^5.0.0"
lru_map "^0.3.3"
tslib "^1.9.3"
"@sentry/types@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.15.4.tgz#37f30e35b06e8e12ad1101f1beec3e9b88ca1aab"
integrity sha512-quPHPpeAuwID48HLPmqBiyXE3xEiZLZ5D3CEbU3c3YuvvAg8qmfOOTI6z4Z3Eedi7flvYpnx3n7N3dXIEz30Eg==
"@sentry/utils@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.15.4.tgz#02865ab3c9b745656cea0ab183767ec26c96f6e6"
integrity sha512-lO8SLBjrUDGADl0LOkd55R5oL510d/1SaI08/IBHZCxCUwI4TiYo5EPECq8mrj3XGfgCyq9osw33bymRlIDuSQ==
"@sentry/tracing@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.22.3.tgz#9b5a376e3164c007a22e8642ec094104468cac0c"
integrity sha512-Zp59kMCk5v56ZAyErqjv/QvGOWOQ5fRltzeVQVp8unIDTk6gEFXfhwPsYHOokJe1mfkmrgPDV6xAkYgtL3KCDQ==
dependencies:
"@sentry/types" "5.15.4"
"@sentry/hub" "5.22.3"
"@sentry/minimal" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/types@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.3.tgz#d1d547b30ee8bd7771fa893af74c4f3d71f0fd18"
integrity sha512-cv+VWK0YFgCVDvD1/HrrBWOWYG3MLuCUJRBTkV/Opdy7nkdNjhCAJQrEyMM9zX0sac8FKWKOHT0sykNh8KgmYw==
"@sentry/utils@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.3.tgz#e3bda3e789239eb16d436f768daa12829f33d18f"
integrity sha512-AHNryXMBvIkIE+GQxTlmhBXD0Ksh+5w1SwM5qi6AttH+1qjWLvV6WB4+4pvVvEoS8t5F+WaVUZPQLmCCWp6zKw==
dependencies:
"@sentry/types" "5.22.3"
tslib "^1.9.3"
"@sindresorhus/is@^0.7.0":
@@ -1803,11 +1792,6 @@ after@0.8.2:
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
agent-base@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
agent-base@6:
version "6.0.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a"
@@ -2496,7 +2480,7 @@ blob@0.0.5:
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
bluebird@^3.5.0, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.2:
bluebird@^3.5.5, bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@@ -2722,10 +2706,10 @@ buffer-indexof-polyfill@~1.0.0:
resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz#a9fb806ce8145d5428510ce72f278bb363a638bf"
integrity sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8=
buffer-writer@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-1.0.1.tgz#22a936901e3029afcd7547eb4487ceb697a3bf08"
integrity sha1-Iqk2kB4wKa/NdUfrRIfOtpejvwg=
buffer-writer@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
buffer-xor@^1.0.3:
version "1.0.3"
@@ -3190,14 +3174,6 @@ cloneable-readable@^1.0.0:
process-nextick-args "^2.0.0"
readable-stream "^2.3.5"
cls-bluebird@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cls-bluebird/-/cls-bluebird-2.1.0.tgz#37ef1e080a8ffb55c2f4164f536f1919e7968aee"
integrity sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=
dependencies:
is-bluebird "^1.0.2"
shimmer "^1.1.0"
cluster-key-slot@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
@@ -3426,11 +3402,16 @@ convert-units@^2.3.4:
lodash.foreach "2.3.x"
lodash.keys "2.3.x"
cookie@0.3.1, cookie@^0.3.1:
cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
cookie@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
cookies@~0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
@@ -4391,6 +4372,11 @@ eslint-plugin-prettier@^3.1.0:
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-react-hooks@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.1.0.tgz#6323fbd5e650e84b2987ba76370523a60f4e7925"
integrity sha512-36zilUcDwDReiORXmcmTc6rRumu9JIM3WjSvV0nclHoUQ0CNrX866EwONvLR/UqaeqFutbAnVu8PEmctdo2SRQ==
eslint-plugin-react@^7.20.0:
version "7.20.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.5.tgz#29480f3071f64a04b2c3d99d9b460ce0f76fb857"
@@ -5075,11 +5061,6 @@ gcp-metadata@^3.4.0:
gaxios "^2.1.0"
json-bigint "^0.3.0"
generic-pool@2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.4.3.tgz#780c36f69dfad05a5a045dd37be7adca11a4f6ff"
integrity sha1-eAw29p360FpaBF3Te+etyhGk9v8=
gensync@^1.0.0-beta.1:
version "1.0.0-beta.1"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
@@ -5675,14 +5656,6 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
https-proxy-agent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b"
integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==
dependencies:
agent-base "5"
debug "4"
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
@@ -5943,11 +5916,6 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
is-bluebird@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-bluebird/-/is-bluebird-1.0.2.tgz#096439060f4aa411abee19143a84d6a55346d6e2"
integrity sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=
is-buffer@^1.1.5, is-buffer@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -6712,11 +6680,6 @@ js-search@^1.4.2:
resolved "https://registry.yarnpkg.com/js-search/-/js-search-1.4.3.tgz#23a86d7e064ca53a473930edc48615b6b1c1954a"
integrity sha512-Sny5pf00kX1sM1KzvUC9nGYWXOvBfy30rmvZWeRktpg+esQKedIXrXNee/I2CAnsouCyaTjitZpRflDACx4toA==
js-string-escape@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -7513,7 +7476,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
"lodash@>=3.5 <5", lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5:
"lodash@>=3.5 <5", lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5:
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
@@ -7632,11 +7595,6 @@ markdown-it-container@^3.0.0:
resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz#1d19b06040a020f9a827577bb7dbf67aa5de9a5b"
integrity sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==
markdown-it-mark@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/markdown-it-mark/-/markdown-it-mark-3.0.0.tgz#27c3e39ef3cc310b2dde5375082c9fa912983cda"
integrity sha512-HqMWeKfMMOu4zBO0emmxsoMWmbf2cPKZY1wP6FsTbKmicFfp5y4L3KXAsNeO1rM6NTJVOrNlLKMPjWzriBGspw==
markdown-it@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc"
@@ -7900,35 +7858,34 @@ mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mobx-react-devtools@^6.0.3:
version "6.1.1"
resolved "https://registry.yarnpkg.com/mobx-react-devtools/-/mobx-react-devtools-6.1.1.tgz#a462b944085cf11ff96fc937d12bf31dab4c8984"
integrity sha512-nc5IXLdEUFLn3wZal65KF3/JFEFd+mbH4KTz/IG5BOPyw7jo8z29w/8qm7+wiCyqVfUIgJ1gL4+HVKmcXIOgqA==
mobx-react-lite@>=2.0.6:
version "2.0.7"
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-2.0.7.tgz#1bfb3b4272668e288047cf0c7940b14e91cba284"
integrity sha512-YKAh2gThC6WooPnVZCoC+rV1bODAKFwkhxikzgH18wpBjkgTkkR9Sb0IesQAH5QrAEH/JQVmy47jcpQkf2Au3Q==
mobx-react@^5.4.2:
version "5.4.4"
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-5.4.4.tgz#b3de9c6eabcd0ed8a40036888cb0221ab9568b80"
integrity sha512-2mTzpyEjVB/RGk2i6KbcmP4HWcAUFox5ZRCrGvSyz49w20I4C4qql63grPpYrS9E9GKwgydBHQlA4y665LuRCQ==
mobx-react@^6.2.5:
version "6.2.5"
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.2.5.tgz#9020a17b79cc6dc3d124ad89ab36eb9ea540a45b"
integrity sha512-LxtXXW0GkOAO6VOIg2m/6WL6ZuKlzOWwESIFdrWelI0ZMIvtKCMZVUuulcO5GAWSDsH0ApaMkGLoaPqKjzyziQ==
dependencies:
hoist-non-react-statics "^3.0.0"
react-lifecycles-compat "^3.0.2"
mobx-react-lite ">=2.0.6"
mobx@4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-4.6.0.tgz#88a8ed21ff81b8861778c4b0d38e3dcdd1a7ddde"
integrity sha512-qoX4BsUpA37yLzYAmNsApPiRlpMr8uwt+KYVTJNv9IhpD8NQVMno/K3EqZKAsiWo9IEvPOuQ0B6z7Z7ISjULpA==
moment-timezone@^0.5.21, moment-timezone@^0.5.25:
version "0.5.28"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338"
integrity sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw==
moment-timezone@^0.5.25, moment-timezone@^0.5.31:
version "0.5.31"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05"
integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0", moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
"moment@>= 2.9.0", moment@^2.26.0:
version "2.27.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
move-concurrently@^1.0.1:
version "1.0.1"
@@ -8212,11 +8169,6 @@ oauth-sign@~0.9.0:
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
object-assign@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
integrity sha1-ejs9DpgGPUP0wD8uiubNUahog6A=
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -8548,10 +8500,10 @@ package-json@^4.0.0:
registry-url "^3.0.3"
semver "^5.1.0"
packet-reader@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-0.3.1.tgz#cd62e60af8d7fea8a705ec4ff990871c46871f27"
integrity sha1-zWLmCvjX/qinBexP+ZCHHEaHHyc=
packet-reader@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74"
integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==
pako@~1.0.2, pako@~1.0.5:
version "1.0.11"
@@ -8598,10 +8550,10 @@ parse-asn1@^5.0.0:
pbkdf2 "^3.0.3"
safe-buffer "^5.1.1"
parse-entities@^1.1.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50"
integrity sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==
parse-entities@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
dependencies:
character-entities "^1.0.0"
character-entities-legacy "^1.0.0"
@@ -8746,15 +8698,15 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pg-connection-string@0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7"
integrity sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=
pg-connection-string@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.3.0.tgz#c13fcb84c298d0bfa9ba12b40dd6c23d946f55d6"
integrity sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w==
pg-hstore@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/pg-hstore/-/pg-hstore-2.3.2.tgz#f7ef053e7b9b892ae986af2f7cbe86432dfcf24f"
integrity sha1-9+8FPnubiSrphq8vfL6GQy388k8=
pg-hstore@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/pg-hstore/-/pg-hstore-2.3.3.tgz#d1978c12a85359830b1388d3b0ff233b88928e96"
integrity sha512-qpeTpdkguFgfdoidtfeTho1Q1zPVPbtMHgs8eQ+Aan05iLmIs3Z3oo5DOZRclPGoQ4i68I1kCtQSJSa7i0ZVYg==
dependencies:
underscore "^1.7.0"
@@ -8763,40 +8715,42 @@ pg-int8@1.0.1:
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
pg-pool@1.*:
version "1.8.0"
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-1.8.0.tgz#f7ec73824c37a03f076f51bfdf70e340147c4f37"
integrity sha1-9+xzgkw3oD8Hb1G/33DjQBR8Tzc=
dependencies:
generic-pool "2.4.3"
object-assign "4.1.0"
pg-pool@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.2.1.tgz#5f4afc0f58063659aeefa952d36af49fa28b30e0"
integrity sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA==
pg-types@1.*:
version "1.13.0"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63"
integrity sha512-lfKli0Gkl/+za/+b6lzENajczwZHc7D5kiUCZfgm914jipD2kIOIvEkAhZ8GrW3/TUoP9w8FHjwpPObBye5KQQ==
pg-protocol@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.2.5.tgz#28a1492cde11646ff2d2d06bdee42a3ba05f126c"
integrity sha512-1uYCckkuTfzz/FCefvavRywkowa6M5FohNMF5OjKrqo9PSR8gYc8poVmwwYQaBxhmQdBjhtP514eXy9/Us2xKg==
pg-types@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
dependencies:
pg-int8 "1.0.1"
postgres-array "~1.0.0"
postgres-array "~2.0.0"
postgres-bytea "~1.0.0"
postgres-date "~1.0.0"
postgres-date "~1.0.4"
postgres-interval "^1.1.0"
pg@^6.1.5:
version "6.4.2"
resolved "https://registry.yarnpkg.com/pg/-/pg-6.4.2.tgz#c364011060eac7a507a2ae063eb857ece910e27f"
integrity sha1-w2QBEGDqx6UHoq4GPrhX7OkQ4n8=
pg@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/pg/-/pg-8.3.0.tgz#941383300d38eef51ecb88a0188cec441ab64d81"
integrity sha512-jQPKWHWxbI09s/Z9aUvoTbvGgoj98AU7FDCcQ7kdejupn/TcNpx56v2gaOTzXkzOajmOEJEdi9eTh9cA2RVAjQ==
dependencies:
buffer-writer "1.0.1"
js-string-escape "1.0.1"
packet-reader "0.3.1"
pg-connection-string "0.1.3"
pg-pool "1.*"
pg-types "1.*"
pgpass "1.*"
buffer-writer "2.0.0"
packet-reader "1.0.0"
pg-connection-string "^2.3.0"
pg-pool "^3.2.1"
pg-protocol "^1.2.5"
pg-types "^2.1.0"
pgpass "1.x"
semver "4.3.2"
pgpass@1.*:
pgpass@1.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306"
integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=
@@ -8873,20 +8827,20 @@ postcss-value-parser@^4.0.2:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d"
integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==
postgres-array@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-1.0.3.tgz#c561fc3b266b21451fc6555384f4986d78ec80f5"
integrity sha512-5wClXrAP0+78mcsNX3/ithQ5exKvCyK5lr5NEEEeGwwM6NJdQgzIJBVxLvRW+huFpX92F2QnZ5CcokH0VhK2qQ==
postgres-array@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
postgres-bytea@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35"
integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=
postgres-date@~1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.5.tgz#710b27de5f27d550f6e80b5d34f7ba189213c2ee"
integrity sha512-pdau6GRPERdAYUQwkBnGKxEfPyhVZXG/JiS44iZWiNdSOWE09N2lUgN6yshuq6fVSon4Pm0VMXd1srUUkLe9iA==
postgres-date@~1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.6.tgz#4925e8085b30c2ba1a06ac91b9a3473954a2ce2d"
integrity sha512-o2a4gxeFcox+CgB3Ig/kNHBP23PiEXHCXx7pcIIsvzoNz4qv+lKTyiSkjOXIMNUl12MO/mOYl2K6wR9X5K6Plg==
postgres-interval@^1.1.0:
version "1.2.0"
@@ -8950,20 +8904,13 @@ pretty-format@^26.2.0:
ansi-styles "^4.0.0"
react-is "^16.12.0"
prismjs@^1.19.0:
prismjs@^1.19.0, prismjs@~1.21.0:
version "1.21.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3"
integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==
optionalDependencies:
clipboard "^2.0.0"
prismjs@~1.17.0:
version "1.17.1"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.17.1.tgz#e669fcbd4cdd873c35102881c33b14d0d68519be"
integrity sha512-PrEDJAFdUGbOP6xK/UsfkC5ghJsPJviKgnQOoxaDbBjwc8op68Quupwt1DeAFoG8GImPhiKXAvvsH7wDSLsu1Q==
optionalDependencies:
clipboard "^2.0.0"
process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -9112,7 +9059,7 @@ prosemirror-tables@^1.0.0:
prosemirror-transform "^1.2.1"
prosemirror-view "^1.13.3"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1:
prosemirror-transform@1.2.5, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1:
version "1.2.5"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.5.tgz#7a3e2c61fcdbaf1d0844a2a3bc34fc3524e9809c"
integrity sha512-eqeIaxWtUfOnpA1ERrXCuSIMzqIJtL9Qrs5uJMCjY5RMSaH5o4pc390SAjn/IDPeIlw6auh0hCCXs3wRvGnQug==
@@ -9372,7 +9319,7 @@ react-keydown@^1.7.3:
dependencies:
core-js "^3.1.2"
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.2:
react-lifecycles-compat@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@@ -9577,14 +9524,14 @@ referrer-policy@1.2.0:
resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e"
integrity sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==
refractor@^2.10.1:
version "2.10.1"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.10.1.tgz#166c32f114ed16fd96190ad21d5193d3afc7d34e"
integrity sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw==
refractor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.1.0.tgz#b05a43c8a1b4fccb30001ffcbd5cd781f7f06f78"
integrity sha512-bN8GvY6hpeXfC4SzWmYNQGLLF2ZakRDNBkgCL0vvl5hnpMrnyURk8Mv61v6pzn4/RBHzSWLp44SzMmVHqMGNww==
dependencies:
hastscript "^5.0.0"
parse-entities "^1.1.2"
prismjs "~1.17.0"
parse-entities "^2.0.0"
prismjs "~1.21.0"
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
@@ -9773,6 +9720,11 @@ require-package-name@^2.0.1:
resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9"
integrity sha1-wR6XJ2tluOKSP3Xav1+y7ww4Qbk=
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -9854,15 +9806,14 @@ retry-as-promised@^3.2.0:
dependencies:
any-promise "^1.3.0"
rich-markdown-editor@^10.6.1:
version "10.6.1"
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-10.6.1.tgz#22099c12b8b1e193f9523c16cb83ef0824c08d58"
integrity sha512-HfjrNXanFwy6iy3dkNqOZF1siwm5tuou8B9y2gj0PcP73KJyQ/FoFPUSEZ/E3ttU45NJCARLvd8X5Qc2wdtLVA==
rich-markdown-editor@^11.0.0-1:
version "11.0.0-1"
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.0.0-1.tgz#d21f0a62153bf8b94dc82652f5c1cbbd861eb5c8"
integrity sha512-XUhsojdDsfKzOMLmIE5E8Pl6lNYWjlHqBpnDEa21oFZE0byFNoDTK84dcJg4ErNrk8OZqFuDjZVBeglgIzrtnQ==
dependencies:
copy-to-clipboard "^3.0.8"
lodash "^4.17.11"
markdown-it-container "^3.0.0"
markdown-it-mark "^3.0.0"
outline-icons "^1.21.0-3"
prismjs "^1.19.0"
prosemirror-commands "^1.1.4"
@@ -9876,15 +9827,16 @@ rich-markdown-editor@^10.6.1:
prosemirror-schema-list "^1.1.2"
prosemirror-state "^1.3.3"
prosemirror-tables "^1.0.0"
prosemirror-transform "1.2.5"
prosemirror-utils "^0.9.6"
prosemirror-view "^1.14.11"
react-medium-image-zoom "^3.0.16"
react-portal "^4.2.1"
refractor "^2.10.1"
refractor "^3.1.0"
resize-observer-polyfill "^1.5.1"
slugify "^1.4.0"
smooth-scroll-into-view-if-needed "^1.1.27"
styled-components "^5.0.0"
typescript "^3.7.5"
typescript "3.7.5"
rimraf@2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1"
@@ -10097,50 +10049,47 @@ semver@^7.2.1, semver@^7.3.2:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
sequelize-cli@^5.5.0:
version "5.5.1"
resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-5.5.1.tgz#0b9c2fc04d082cc8ae0a8fe270b96bb606152bab"
integrity sha512-ZM4kUZvY3y14y+Rq3cYxGH7YDJz11jWHcN2p2x7rhAIemouu4CEXr5ebw30lzTBtyXV4j2kTO+nUjZOqzG7k+Q==
sequelize-cli@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-6.2.0.tgz#fd02bfeae23b8226872f9947f3f8212cc49a4771"
integrity sha512-6WQ2x91hg30dUn66mXHnzvHATZ4pyI1GHSNbS/TNN/vRR4BLRSLijadeMgC8zqmKDsL0VqzVVopJWfJakuP++Q==
dependencies:
bluebird "^3.5.3"
cli-color "^1.4.0"
fs-extra "^7.0.1"
js-beautify "^1.8.8"
lodash "^4.17.5"
resolve "^1.5.0"
umzug "^2.1.0"
umzug "^2.3.0"
yargs "^13.1.0"
sequelize-encrypted@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/sequelize-encrypted/-/sequelize-encrypted-0.1.0.tgz#f9c7a94dc1b4413e1347a49f06cd07b7f3bf9916"
integrity sha1-+cepTcG0QT4TR6SfBs0Ht/O/mRY=
sequelize-encrypted@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/sequelize-encrypted/-/sequelize-encrypted-1.0.0.tgz#49c5f48edcebd3504cc18bcb4876600d53e311d7"
integrity sha1-ScX0jtzr01BMwYvLSHZgDVPjEdc=
sequelize-pool@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-2.3.0.tgz#64f1fe8744228172c474f530604b6133be64993d"
integrity sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA==
sequelize-pool@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-6.1.0.tgz#caaa0c1e324d3c2c3a399fed2c7998970925d668"
integrity sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg==
sequelize@^5.21.1:
version "5.21.6"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.21.6.tgz#5bf9b687336ce9157a3b386ad653fa130a5fa38a"
integrity sha512-RsgEpP2PP7txeoTWxoLLoe3xX8R2WYQAO7LNba2Ok3/pV5EFfKZry4fJXH56DUHJB909msMCHg0CJKDsQVbjcQ==
sequelize@^6.3.4:
version "6.3.4"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.3.4.tgz#dd6f4801ba5e4d7a6f845a5ad3e616f24453694f"
integrity sha512-W6Y96N5QHTgEz5Q37v2GYbKufSXaw0b3v4rCLTPbcCMfIG0MHI42Ozp7IwiyV9bdNkfFEdY7XP8R6lWrWg4hUw==
dependencies:
bluebird "^3.5.0"
cls-bluebird "^2.1.0"
debug "^4.1.1"
dottie "^2.0.0"
inflection "1.12.0"
lodash "^4.17.15"
moment "^2.24.0"
moment-timezone "^0.5.21"
moment "^2.26.0"
moment-timezone "^0.5.31"
retry-as-promised "^3.2.0"
semver "^6.3.0"
sequelize-pool "^2.3.0"
semver "^7.3.2"
sequelize-pool "^6.0.0"
toposort-class "^1.0.1"
uuid "^3.3.3"
uuid "^8.1.0"
validator "^10.11.0"
wkx "^0.4.8"
wkx "^0.5.0"
serialize-javascript@^4.0.0:
version "4.0.0"
@@ -10235,11 +10184,6 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
shimmer@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
side-channel@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"
@@ -10830,13 +10774,13 @@ styled-components-breakpoint@^2.1.1:
integrity sha512-PkS7p3MkPJx/v930Q3MPJU8llfFJTxk8o009jl0p+OUFmVb2AlHmVclX1MBHSXk8sZYGoVTTVIPDuZCELi7QIg==
styled-components@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.0.1.tgz#57782a6471031abefb2db5820a1876ae853bc619"
integrity sha512-E0xKTRIjTs4DyvC1MHu/EcCXIj6+ENCP8hP01koyoADF++WdBUOrSGwU1scJRw7/YaYOhDvvoad6VlMG+0j53A==
version "5.1.1"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d"
integrity sha512-1ps8ZAYu2Husx+Vz8D+MvXwEwvMwFv+hqqUwhNlDN5ybg6A+3xyW1ECrAgywhvXapNfXiz79jJyU0x22z0FFTg==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/traverse" "^7.4.5"
"@emotion/is-prop-valid" "^0.8.3"
"@emotion/is-prop-valid" "^0.8.8"
"@emotion/stylis" "^0.8.4"
"@emotion/unitless" "^0.7.4"
babel-plugin-styled-components ">= 1"
@@ -11326,7 +11270,12 @@ typescript-compiler@^1.4.1-2:
resolved "https://registry.yarnpkg.com/typescript-compiler/-/typescript-compiler-1.4.1-2.tgz#ba4f7db22d91534a1929d90009dce161eb72fd3f"
integrity sha1-uk99si2RU0oZKdkACdzhYety/T8=
typescript@^3.4, typescript@^3.7.5:
typescript@3.7.5:
version "3.7.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
typescript@^3.4:
version "3.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9"
integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==
@@ -11349,7 +11298,7 @@ uid2@0.0.3:
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=
umzug@^2.1.0:
umzug@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/umzug/-/umzug-2.3.0.tgz#0ef42b62df54e216b05dcaf627830a6a8b84a184"
integrity sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==
@@ -11618,12 +11567,12 @@ uuid@3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0:
uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.2.0:
uuid@^8.1.0, uuid@^8.2.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea"
integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==
@@ -11909,10 +11858,10 @@ windows-release@^3.1.0:
dependencies:
execa "^1.0.0"
wkx@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.4.8.tgz#a092cf088d112683fdc7182fd31493b2c5820003"
integrity sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ==
wkx@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c"
integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==
dependencies:
"@types/node" "*"