From 0aa338cccca7e83c93783a13a3277ce0a704ff9f Mon Sep 17 00:00:00 2001 From: Guilherme DIniz <64857365+guilherme-diniz@users.noreply.github.com> Date: Tue, 1 Sep 2020 00:03:05 -0300 Subject: [PATCH 01/47] feat: Allow unpublishing documents (#1467) * Allow unpublishing documents * add block unpublish files that has children * add api tests to new route --- app/menus/DocumentMenu.js | 17 ++++++++--- app/models/Document.js | 6 +++- app/stores/DocumentsStore.js | 16 +++++++++++ server/api/documents.js | 35 +++++++++++++++++++--- server/api/documents.test.js | 56 ++++++++++++++++++++++++++++++++++++ server/models/Document.js | 17 +++++++++-- server/policies/document.js | 24 ++++++++++++++++ 7 files changed, 159 insertions(+), 12 deletions(-) diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js index 0d99e948b..2a13df7c9 100644 --- a/app/menus/DocumentMenu.js +++ b/app/menus/DocumentMenu.js @@ -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"; @@ -15,10 +14,10 @@ import DocumentTemplatize from "scenes/DocumentTemplatize"; import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu"; import Modal from "components/Modal"; import { - documentUrl, - documentMoveUrl, - editDocumentUrl, documentHistoryUrl, + documentMoveUrl, + documentUrl, + editDocumentUrl, newDocumentUrl, } from "utils/routeHelpers"; @@ -106,6 +105,11 @@ class DocumentMenu extends React.Component { 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(); }; @@ -225,6 +229,11 @@ class DocumentMenu extends React.Component { New nested document )} + {can.unpublish && ( + + Unpublish + + )} {can.update && !document.isTemplate && ( Create template… diff --git a/app/models/Document.js b/app/models/Document.js index 6c6101a42..86d3fb71f 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -1,7 +1,7 @@ // @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"; @@ -145,6 +145,10 @@ export default class Document extends BaseModel { return this.store.restore(this, revision); }; + unpublish = () => { + return this.store.unpublish(this); + }; + @action enableEmbeds = () => { this.embedsDisabled = false; diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index a21a69c3d..41bdb1cf7 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -535,6 +535,22 @@ export default class DocumentsStore extends BaseStore { 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 }); }; diff --git a/server/api/documents.js b/server/api/documents.js index 7135992d0..96321c79e 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -5,20 +5,20 @@ import documentMover from "../commands/documentMover"; import { 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"; @@ -1018,4 +1018,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; diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 83041f37d..f7ee19e28 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -1736,3 +1736,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); + }); +}); diff --git a/server/models/Document.js b/server/models/Document.js index 4d08f077d..2b0e5d494 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -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"; @@ -556,6 +555,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) { diff --git a/server/policies/document.js b/server/policies/document.js index 8ee7196b4..29e4c0411 100644 --- a/server/policies/document.js +++ b/server/policies/document.js @@ -157,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 + ); +}); From 8550116c6bee5add7917344305b8e8f4a0a228aa Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 31 Aug 2020 20:15:10 -0700 Subject: [PATCH 02/47] fix: shares.list should not return shares for deleted documents fix: shares.info should not return info for revoked shares closes #1492 --- server/api/shares.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/api/shares.js b/server/api/shares.js index fcb74c1c0..78cdacf1c 100644 --- a/server/api/shares.js +++ b/server/api/shares.js @@ -21,10 +21,12 @@ router.post("shares.info", auth(), async (ctx) => { where: id ? { id, + revokedAt: { [Op.eq]: null }, } : { documentId, userId: user.id, + revokedAt: { [Op.eq]: null }, }, }); if (!share) { @@ -63,6 +65,7 @@ router.post("shares.list", auth(), pagination(), async (ctx) => { { model: Document, required: true, + paranoid: true, as: "document", where: { collectionId: collectionIds, From dd0d51dd9dd89ab30e83e0605df8620317a30887 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 31 Aug 2020 20:23:51 -0700 Subject: [PATCH 03/47] test --- server/api/shares.js | 7 ++++-- server/api/shares.test.js | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/server/api/shares.js b/server/api/shares.js index 78cdacf1c..751932cb0 100644 --- a/server/api/shares.js +++ b/server/api/shares.js @@ -171,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", diff --git a/server/api/shares.test.js b/server/api/shares.test.js index a48da59eb..b5ec45595 100644 --- a/server/api/shares.test.js +++ b/server/api/shares.test.js @@ -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,20 @@ describe("#shares.info", () => { expect(res.status).toEqual(404); }); + it("should not find revoked share", async () => { + const { user, admin, document } = await seed(); + const share = await buildShare({ + documentId: document.id, + teamId: admin.teamId, + userId: admin.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 require authentication", async () => { const { user, document } = await seed(); const share = await buildShare({ @@ -382,6 +415,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({ From 759d4a5ac2d77210e0548161a9e0c653ea1a2811 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 31 Aug 2020 20:24:05 -0700 Subject: [PATCH 04/47] fix: Handle error revoking share link on frontend --- app/menus/ShareMenu.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/menus/ShareMenu.js b/app/menus/ShareMenu.js index 406ad79e1..d950a5d33 100644 --- a/app/menus/ShareMenu.js +++ b/app/menus/ShareMenu.js @@ -31,10 +31,15 @@ class ShareMenu extends React.Component { 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 = () => { From 31522b0d6f2bc6edc87b0d278ae4838998e4b809 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 31 Aug 2020 20:28:41 -0700 Subject: [PATCH 05/47] fix: Local shares state not cleared when document is deleted --- app/stores/DocumentsStore.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index 41bdb1cf7..d1b19ef56 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -500,6 +500,13 @@ export default class DocumentsStore extends BaseStore { 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(); } From 4edf90a184f551adde6c0256d791073507ff36cb Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 31 Aug 2020 20:29:55 -0700 Subject: [PATCH 06/47] fix: Development warning, missing key on event list items --- app/scenes/Settings/Events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scenes/Settings/Events.js b/app/scenes/Settings/Events.js index 1e1d3b4af..3e70aa408 100644 --- a/app/scenes/Settings/Events.js +++ b/app/scenes/Settings/Events.js @@ -86,7 +86,7 @@ class Events extends React.Component { ) : ( <> {events.orderedData.map((event) => ( - + ))} {this.allowLoadMore && ( From 95b91c466a2bda0dc37425378bedd17901863c0d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 31 Aug 2020 20:32:08 -0700 Subject: [PATCH 07/47] fix: Disallow creating nested document within document in trash --- server/policies/document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/policies/document.js b/server/policies/document.js index 29e4c0411..32bf90241 100644 --- a/server/policies/document.js +++ b/server/policies/document.js @@ -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; From 637a9b5cf98e2192556edd557fb48014c3f2f55b Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 31 Aug 2020 20:44:19 -0700 Subject: [PATCH 08/47] refactor: Remove unused event handlers --- app/scenes/Settings/components/ImageUpload.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scenes/Settings/components/ImageUpload.js b/app/scenes/Settings/components/ImageUpload.js index 4eb2e49e3..9e5d5ac42 100644 --- a/app/scenes/Settings/components/ImageUpload.js +++ b/app/scenes/Settings/components/ImageUpload.js @@ -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, @@ -123,10 +125,8 @@ class ImageUpload extends React.Component { {this.props.children} From 82433e02a01d7e4603d1a943f3c593bbc0a1be12 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 31 Aug 2020 21:09:23 -0700 Subject: [PATCH 09/47] feat: Add custom error state for chunk loading failed --- app/components/ErrorBoundary.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/components/ErrorBoundary.js b/app/components/ErrorBoundary.js index 24c087e50..e578a4e9e 100644 --- a/app/components/ErrorBoundary.js +++ b/app/components/ErrorBoundary.js @@ -55,7 +55,26 @@ class ErrorBoundary extends React.Component { 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 ( + + +

Loading Failed

+ + 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. + +

+ +

+
+ ); + } return ( @@ -66,7 +85,7 @@ class ErrorBoundary extends React.Component { {isReported && " – our engineers have been notified"}. Please try reloading the page, it may have been a temporary glitch. - {this.showDetails &&
{this.error.toString()}
} + {this.showDetails &&
{error.toString()}
}

{" "} {this.showDetails ? ( From 4bf5926ee312845b7b6618e15953970f56bdc853 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 31 Aug 2020 21:57:26 -0700 Subject: [PATCH 10/47] Bump RME --- package.json | 2 +- yarn.lock | 39 ++++++++++++++++----------------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index d5217dffd..ad98dad27 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "react-portal": "^4.0.0", "react-router-dom": "^5.1.2", "react-waypoint": "^9.0.2", - "rich-markdown-editor": "^10.6.5", + "rich-markdown-editor": "^10.6.6-0", "semver": "^7.3.2", "sequelize": "^6.3.4", "sequelize-cli": "^6.2.0", diff --git a/yarn.lock b/yarn.lock index b0d2f7c9b..00e4f3e01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8555,10 +8555,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" @@ -8909,20 +8909,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" @@ -9536,14 +9529,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" @@ -9813,10 +9806,10 @@ retry-as-promised@^3.2.0: dependencies: any-promise "^1.3.0" -rich-markdown-editor@^10.6.5: - version "10.6.5" - resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-10.6.5.tgz#b74ae2e7d05eaa3c8ef34744e5cb0ed2dbdb0958" - integrity sha512-C/C+6L7BTXC4zSHgOYMljOQ3CvFt8zNCT829woKBHcDWSnXiUzpjgZZ4qEeNRlh/XJmqeFZYfqY+OzIMsVP2+Q== +rich-markdown-editor@^10.6.6-0: + version "10.6.6-0" + resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-10.6.6-0.tgz#13f3760b5f089ccc9dfc66ff49325a3210f78ae2" + integrity sha512-3CTSkcqm2+1pfL+phlXIXNQHzmc9T4F5FjWrymnY0BmpiHJ/RDloY44ThwG+YiV4VmpC69LzBhxrTYyAWBhiWA== dependencies: copy-to-clipboard "^3.0.8" lodash "^4.17.11" @@ -9840,7 +9833,7 @@ rich-markdown-editor@^10.6.5: 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" slugify "^1.4.0" smooth-scroll-into-view-if-needed "^1.1.27" styled-components "^5.1.0" From 206160582e36114b448a2a00520a637dd9f991ca Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 2 Sep 2020 20:19:36 -0700 Subject: [PATCH 11/47] chore: Tweak ordering of unpublish menu item --- app/menus/DocumentMenu.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js index 2a13df7c9..7bd7a00c6 100644 --- a/app/menus/DocumentMenu.js +++ b/app/menus/DocumentMenu.js @@ -229,16 +229,16 @@ class DocumentMenu extends React.Component { New nested document )} - {can.unpublish && ( - - Unpublish - - )} {can.update && !document.isTemplate && ( Create template… )} + {can.unpublish && ( + + Unpublish + + )} {can.update && ( Edit )} From b93a397ab372be20e8facfba012b1a0d5d77ae1d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 3 Sep 2020 22:27:30 -0700 Subject: [PATCH 12/47] feat: Bump RME --- package.json | 2 +- yarn.lock | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index ad98dad27..17a8af446 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "react-portal": "^4.0.0", "react-router-dom": "^5.1.2", "react-waypoint": "^9.0.2", - "rich-markdown-editor": "^10.6.6-0", + "rich-markdown-editor": "^10.6.6", "semver": "^7.3.2", "sequelize": "^6.3.4", "sequelize-cli": "^6.2.0", diff --git a/yarn.lock b/yarn.lock index 00e4f3e01..bd6541cd6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7595,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" @@ -9806,15 +9801,14 @@ retry-as-promised@^3.2.0: dependencies: any-promise "^1.3.0" -rich-markdown-editor@^10.6.6-0: - version "10.6.6-0" - resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-10.6.6-0.tgz#13f3760b5f089ccc9dfc66ff49325a3210f78ae2" - integrity sha512-3CTSkcqm2+1pfL+phlXIXNQHzmc9T4F5FjWrymnY0BmpiHJ/RDloY44ThwG+YiV4VmpC69LzBhxrTYyAWBhiWA== +rich-markdown-editor@^10.6.6: + version "10.6.6" + resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-10.6.6.tgz#ef6de577ca7652d8c3fd70f0ccdb9fa783571552" + integrity sha512-divXnkfEpuLuGH8qGXNbJT4WDgLkN4TU4eW/c8fBSfkGTnZ/n6yHKMEfabgj/AXsKL14GOSHsRpvZc8KCF9CJA== 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" From dd7436f78ccda4ce1fdbaafaade3fe788dfb23df Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 3 Sep 2020 22:44:42 -0700 Subject: [PATCH 13/47] fix: async error in backlinks service when dealing with a deleted document --- server/services/backlinks.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/services/backlinks.js b/server/services/backlinks.js index 2fe1686ca..e6f4720f0 100644 --- a/server/services/backlinks.js +++ b/server/services/backlinks.js @@ -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: { From e8648d4611a10a2e690dbd96408ed2aa6cb8665c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 3 Sep 2020 23:22:41 -0700 Subject: [PATCH 14/47] fix: Error in shares.info endpoint when requesting share record for deleted document --- server/api/shares.js | 2 +- server/api/shares.test.js | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/server/api/shares.js b/server/api/shares.js index 751932cb0..dda9039ba 100644 --- a/server/api/shares.js +++ b/server/api/shares.js @@ -29,7 +29,7 @@ router.post("shares.info", auth(), async (ctx) => { revokedAt: { [Op.eq]: null }, }, }); - if (!share) { + if (!share || !share.document) { throw new NotFoundError(); } diff --git a/server/api/shares.test.js b/server/api/shares.test.js index b5ec45595..b9b768d88 100644 --- a/server/api/shares.test.js +++ b/server/api/shares.test.js @@ -288,11 +288,11 @@ describe("#shares.info", () => { }); it("should not find revoked share", async () => { - const { user, admin, document } = await seed(); + const { user, document } = await seed(); const share = await buildShare({ documentId: document.id, - teamId: admin.teamId, - userId: admin.id, + teamId: user.teamId, + userId: user.id, }); await share.revoke(); const res = await server.post("/api/shares.info", { @@ -301,6 +301,20 @@ describe("#shares.info", () => { 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({ From 9049785d98cff7d279066c9a65debe79e916a47c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 4 Sep 2020 12:42:41 -0700 Subject: [PATCH 15/47] fix: e shortcut to edit doesn't work when title is selected closes #1510 --- app/scenes/Document/components/Editor.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scenes/Document/components/Editor.js b/app/scenes/Document/components/Editor.js index 4962451dd..800ff95bb 100644 --- a/app/scenes/Document/components/Editor.js +++ b/app/scenes/Document/components/Editor.js @@ -41,7 +41,11 @@ class DocumentEditor extends React.Component { }; handleTitleKeyDown = (event: SyntheticKeyboardEvent<>) => { - if (event.key === "Enter" || event.key === "Tab") { + if ( + event.key === "Enter" || + event.key === "Tab" || + event.key === "ArrowDown" + ) { event.preventDefault(); this.focusAtStart(); } @@ -78,6 +82,7 @@ class DocumentEditor extends React.Component { value={!title && readOnly ? document.titleWithDefault : title} style={startsWithEmojiAndSpace ? { marginLeft: "-1.2em" } : undefined} readOnly={readOnly} + disabled={readOnly} autoFocus={!title} maxLength={100} /> From a98e8ad8df69066f1317e8d2c40937ffcb72101f Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 4 Sep 2020 12:49:20 -0700 Subject: [PATCH 16/47] fix: Breadcrumb overflow color is inaccessible closes #1505 --- app/components/Breadcrumb.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/Breadcrumb.js b/app/components/Breadcrumb.js index 08cc104e7..f567a75ef 100644 --- a/app/components/Breadcrumb.js +++ b/app/components/Breadcrumb.js @@ -127,12 +127,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}; } `; From 6b6d67beb66fd6a1ff7bdace76723e4187166a3f Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 4 Sep 2020 12:51:48 -0700 Subject: [PATCH 17/47] fix: Allow disabling SSL for Postgres connection with standard PGSSLMODE env closes #1501 --- server/sequelize.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/sequelize.js b/server/sequelize.js index d6ad784be..faaf77c17 100644 --- a/server/sequelize.js +++ b/server/sequelize.js @@ -4,6 +4,7 @@ import Sequelize from "sequelize"; import EncryptedField from "sequelize-encrypted"; const isProduction = process.env.NODE_ENV === "production"; +const isSSLDisabled = process.env.PGSSLMODE === "disable"; export const encryptedFields = () => EncryptedField(Sequelize, process.env.SECRET_KEY); @@ -15,11 +16,12 @@ export const sequelize = new Sequelize(process.env.DATABASE_URL, { logging: debug("sql"), typeValidation: true, dialectOptions: { - ssl: isProduction - ? { - // Ref.: https://github.com/brianc/node-postgres/issues/2009 - rejectUnauthorized: false, - } - : false, + ssl: + isProduction && !isSSLDisabled + ? { + // Ref.: https://github.com/brianc/node-postgres/issues/2009 + rejectUnauthorized: false, + } + : false, }, }); From cf1e506009d577cb4bbbfe5687a7660a7151d5fb Mon Sep 17 00:00:00 2001 From: Matheus Rocha Vieira Date: Fri, 4 Sep 2020 17:21:27 -0300 Subject: [PATCH 18/47] feat: Add ClickUp Embed Service (#1465) * Add Clickup Embed Service * Transparency Icon Co-authored-by: Tom Moor --- app/embeds/ClickUp.js | 22 ++++++++++++++++++++++ app/embeds/ClickUp.test.js | 17 +++++++++++++++++ app/embeds/index.js | 8 ++++++++ public/images/clickup.png | Bin 0 -> 14895 bytes 4 files changed, 47 insertions(+) create mode 100644 app/embeds/ClickUp.js create mode 100644 app/embeds/ClickUp.test.js create mode 100644 public/images/clickup.png diff --git a/app/embeds/ClickUp.js b/app/embeds/ClickUp.js new file mode 100644 index 000000000..f09da5ecc --- /dev/null +++ b/app/embeds/ClickUp.js @@ -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 { + static ENABLED = [URL_REGEX]; + + render() { + return ; + } +} diff --git a/app/embeds/ClickUp.test.js b/app/embeds/ClickUp.test.js new file mode 100644 index 000000000..3014b49a7 --- /dev/null +++ b/app/embeds/ClickUp.test.js @@ -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); + }); +}); diff --git a/app/embeds/index.js b/app/embeds/index.js index 27319b9d0..939a6d921 100644 --- a/app/embeds/index.js +++ b/app/embeds/index.js @@ -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: () => , + component: ClickUp, + matcher: matcher(ClickUp), + }, { title: "Codepen", keywords: "code editor", diff --git a/public/images/clickup.png b/public/images/clickup.png new file mode 100644 index 0000000000000000000000000000000000000000..2d16de1baed6d814ed7d469728fa5ad937afe085 GIT binary patch literal 14895 zcmV+~I?%<5P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 hF*7 Date: Fri, 4 Sep 2020 13:23:27 -0700 Subject: [PATCH 19/47] fix: Enter in document title should create a newline at the top of editor and focus closes #1511 --- app/scenes/Document/components/Editor.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/scenes/Document/components/Editor.js b/app/scenes/Document/components/Editor.js index 800ff95bb..893c1ea19 100644 --- a/app/scenes/Document/components/Editor.js +++ b/app/scenes/Document/components/Editor.js @@ -40,12 +40,22 @@ class DocumentEditor extends React.Component { } }; + 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())); + } + }; + handleTitleKeyDown = (event: SyntheticKeyboardEvent<>) => { - if ( - event.key === "Enter" || - event.key === "Tab" || - event.key === "ArrowDown" - ) { + if (event.key === "Enter") { + event.preventDefault(); + this.insertParagraph(); + this.focusAtStart(); + return; + } + if (event.key === "Tab" || event.key === "ArrowDown") { event.preventDefault(); this.focusAtStart(); } From d6f245d67e34472f9ec3be9123cb85771f4f373d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 5 Sep 2020 19:51:12 -0700 Subject: [PATCH 20/47] Bump RME: Fixes inline styles in table cells with newlines --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17a8af446..93aa42ca0 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "react-portal": "^4.0.0", "react-router-dom": "^5.1.2", "react-waypoint": "^9.0.2", - "rich-markdown-editor": "^10.6.6", + "rich-markdown-editor": "^10.6.7", "semver": "^7.3.2", "sequelize": "^6.3.4", "sequelize-cli": "^6.2.0", From f13696dd2aba96690ba7ed681a302ec742afce31 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 5 Sep 2020 19:58:17 -0700 Subject: [PATCH 21/47] Update yarn.lock --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index bd6541cd6..d4c45b129 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9801,10 +9801,10 @@ retry-as-promised@^3.2.0: dependencies: any-promise "^1.3.0" -rich-markdown-editor@^10.6.6: - version "10.6.6" - resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-10.6.6.tgz#ef6de577ca7652d8c3fd70f0ccdb9fa783571552" - integrity sha512-divXnkfEpuLuGH8qGXNbJT4WDgLkN4TU4eW/c8fBSfkGTnZ/n6yHKMEfabgj/AXsKL14GOSHsRpvZc8KCF9CJA== +rich-markdown-editor@^10.6.7: + version "10.6.7" + resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-10.6.7.tgz#d5d0367779ed3342b7aae283e57765ba69c73ae3" + integrity sha512-sl0UNCK4Anj4rlfPyBOtCvPVopRbUJccevKE36YIdWmvVdylLWjCAsvV7UUewmXxb2lKzofkTYkR4eR8T3C6dA== dependencies: copy-to-clipboard "^3.0.8" lodash "^4.17.11" From acb04fdf1a1ea81b080fe2917337fa2329fa7a8e Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 6 Sep 2020 10:33:39 -0700 Subject: [PATCH 22/47] fix: CMD+S should save when editor title is focused --- app/scenes/Document/components/Editor.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/scenes/Document/components/Editor.js b/app/scenes/Document/components/Editor.js index 893c1ea19..c05237b91 100644 --- a/app/scenes/Document/components/Editor.js +++ b/app/scenes/Document/components/Editor.js @@ -21,6 +21,7 @@ type Props = { isDraft: boolean, isShare: boolean, readOnly?: boolean, + onSave: () => mixed, innerRef: { current: any }, }; @@ -58,6 +59,12 @@ class DocumentEditor extends React.Component { if (event.key === "Tab" || event.key === "ArrowDown") { event.preventDefault(); this.focusAtStart(); + return; + } + if (event.key === "s" && event.metaKey) { + event.preventDefault(); + this.props.onSave(); + return; } }; From 709c3e78bd629ee768dd27d42433fd406376d2ac Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 7 Sep 2020 10:42:51 -0700 Subject: [PATCH 23/47] 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 --- app/scenes/Document/components/Editor.js | 7 +++++++ package.json | 2 +- yarn.lock | 17 +++++++++++------ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/scenes/Document/components/Editor.js b/app/scenes/Document/components/Editor.js index 893c1ea19..c05237b91 100644 --- a/app/scenes/Document/components/Editor.js +++ b/app/scenes/Document/components/Editor.js @@ -21,6 +21,7 @@ type Props = { isDraft: boolean, isShare: boolean, readOnly?: boolean, + onSave: () => mixed, innerRef: { current: any }, }; @@ -58,6 +59,12 @@ class DocumentEditor extends React.Component { if (event.key === "Tab" || event.key === "ArrowDown") { event.preventDefault(); this.focusAtStart(); + return; + } + if (event.key === "s" && event.metaKey) { + event.preventDefault(); + this.props.onSave(); + return; } }; diff --git a/package.json b/package.json index 93aa42ca0..b5f37f80c 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "react-portal": "^4.0.0", "react-router-dom": "^5.1.2", "react-waypoint": "^9.0.2", - "rich-markdown-editor": "^10.6.7", + "rich-markdown-editor": "^11.0.0-0", "semver": "^7.3.2", "sequelize": "^6.3.4", "sequelize-cli": "^6.2.0", diff --git a/yarn.lock b/yarn.lock index d4c45b129..48e521652 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9720,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" @@ -9801,10 +9806,10 @@ retry-as-promised@^3.2.0: dependencies: any-promise "^1.3.0" -rich-markdown-editor@^10.6.7: - version "10.6.7" - resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-10.6.7.tgz#d5d0367779ed3342b7aae283e57765ba69c73ae3" - integrity sha512-sl0UNCK4Anj4rlfPyBOtCvPVopRbUJccevKE36YIdWmvVdylLWjCAsvV7UUewmXxb2lKzofkTYkR4eR8T3C6dA== +rich-markdown-editor@^11.0.0-0: + version "11.0.0-0" + resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.0.0-0.tgz#52878d98695c709ccb7d95be060f0098d6f7238d" + integrity sha512-j9nSFYONL4uJwYLdLuPY2ZQVrnnptUzK399yRjVojV4NRHtuQ0mVdNS0iZ8M5tGcVjzlS1su3e/4l5mioKivwA== dependencies: copy-to-clipboard "^3.0.8" lodash "^4.17.11" @@ -9828,9 +9833,9 @@ rich-markdown-editor@^10.6.7: react-medium-image-zoom "^3.0.16" react-portal "^4.2.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.1.0" typescript "3.7.5" rimraf@2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: @@ -10768,7 +10773,7 @@ styled-components-breakpoint@^2.1.1: resolved "https://registry.yarnpkg.com/styled-components-breakpoint/-/styled-components-breakpoint-2.1.1.tgz#37c1b92b0e96c1bbc5d293724d7a114daaa15fca" integrity sha512-PkS7p3MkPJx/v930Q3MPJU8llfFJTxk8o009jl0p+OUFmVb2AlHmVclX1MBHSXk8sZYGoVTTVIPDuZCELi7QIg== -styled-components@^5.0.0, styled-components@^5.1.0: +styled-components@^5.0.0: 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== From 4de3f694745570be2c4e791b0b6a54da7bca5880 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 7 Sep 2020 11:51:09 -0700 Subject: [PATCH 24/47] 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 --- app/components/Breadcrumb.js | 99 ++++++++++++++++++++-------- app/components/CollectionIcon.js | 2 +- app/menus/DocumentMenu.js | 52 +++++++++++++-- app/menus/RevisionMenu.js | 2 +- app/models/Document.js | 5 +- app/scenes/CollectionDelete.js | 5 +- app/stores/DocumentsStore.js | 7 +- server/api/documents.js | 17 ++++- server/api/documents.test.js | 108 +++++++++++++++++++++++++++++++ server/api/events.js | 3 +- server/commands/documentMover.js | 3 +- server/models/User.js | 5 +- 12 files changed, 258 insertions(+), 50 deletions(-) diff --git a/app/components/Breadcrumb.js b/app/components/Breadcrumb.js index f567a75ef..79466dc40 100644 --- a/app/components/Breadcrumb.js +++ b/app/components/Breadcrumb.js @@ -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

; +function Icon({ document }) { + if (document.isDeleted) { + return ( + <> + + +   + Trash + + + + ); + } + if (document.isArchived) { + return ( + <> + + +   + Archive + + + + ); + } + if (document.isDraft) { + return ( + <> + + +   + Drafts + + + + ); + } + if (document.isTemplate) { + return ( + <> + + +   + Templates + + + + ); + } + 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
; + + 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 ( - {isTemplate && ( - <> - - -   - Templates - - - - )} - {isDraft && ( - <> - - -   - Drafts - - - - )} +   diff --git a/app/components/CollectionIcon.js b/app/components/CollectionIcon.js index 3d1e4fda2..3efe7ebb3 100644 --- a/app/components/CollectionIcon.js +++ b/app/components/CollectionIcon.js @@ -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" diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js index 7bd7a00c6..d7eb6539c 100644 --- a/app/menus/DocumentMenu.js +++ b/app/menus/DocumentMenu.js @@ -11,7 +11,12 @@ 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 { documentHistoryUrl, @@ -100,8 +105,11 @@ class DocumentMenu extends React.Component { 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"); }; @@ -154,6 +162,7 @@ class DocumentMenu extends React.Component { showPrint, showPin, auth, + collections, onOpen, onClose, } = this.props; @@ -161,6 +170,7 @@ class DocumentMenu extends React.Component { 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 ( <> @@ -170,11 +180,45 @@ class DocumentMenu extends React.Component { onOpen={onOpen} onClose={onClose} > - {(can.unarchive || can.restore) && ( + {can.unarchive && ( Restore )} + {can.restore && + (collection ? ( + + Restore + + ) : ( + Restore…} + style={{ + left: -170, + position: "relative", + top: -40, + }} + hover + > +
Choose a collection
+ {collections.orderedData.map((collection) => { + const can = policies.abilities(collection.id); + + return ( + + this.handleRestore(ev, { collectionId: collection.id }) + } + disabled={!can.update} + > + +  {collection.name} + + ); + })} +
+ ))} {showPin && (document.pinned ? can.unpin && ( diff --git a/app/menus/RevisionMenu.js b/app/menus/RevisionMenu.js index 0f4578ee4..888ee0ebd 100644 --- a/app/menus/RevisionMenu.js +++ b/app/menus/RevisionMenu.js @@ -24,7 +24,7 @@ type Props = { class RevisionMenu extends React.Component { 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); }; diff --git a/app/models/Document.js b/app/models/Document.js index 86d3fb71f..7538fe927 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -6,7 +6,6 @@ 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,8 @@ 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 = () => { diff --git a/app/scenes/CollectionDelete.js b/app/scenes/CollectionDelete.js index 098a4ab34..44c262943 100644 --- a/app/scenes/CollectionDelete.js +++ b/app/scenes/CollectionDelete.js @@ -46,8 +46,9 @@ class CollectionDelete extends React.Component {
Are you sure about that? Deleting the{" "} - {collection.name} collection is permanent and will - also delete all of the documents within it, so be extra careful. + {collection.name} collection is permanent and + cannot be restored, however documents within will be moved to the + trash.