From 620e4942d857d6341cb382c798d5d64e789584c3 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 31 Jan 2021 12:37:27 -0800 Subject: [PATCH] feat: Update default collection tab (#1821) * feat: Allow listing root level documents only via documents.list * feat: New tab on collection home * update tab layout * fix: Correctly sort index sorted documents.list * revert: Tab layout changes * fix: Missing route for recently published fix: Redirect unknown tabs --- app/scenes/Collection.js | 28 +++++++++++-- app/stores/DocumentsStore.js | 9 ++++ server/api/documents.js | 30 +++++++++++++- server/api/documents.test.js | 48 +++++++++++++++++++++- shared/i18n/locales/en_US/translation.json | 1 - 5 files changed, 109 insertions(+), 7 deletions(-) diff --git a/app/scenes/Collection.js b/app/scenes/Collection.js index fbbaa4139..ed941b3e6 100644 --- a/app/scenes/Collection.js +++ b/app/scenes/Collection.js @@ -278,9 +278,12 @@ class CollectionScene extends React.Component { + {t("Documents")} + + {t("Recently updated")} - + {t("Recently published")} @@ -313,9 +316,9 @@ class CollectionScene extends React.Component { showPin /> - + { showPin /> - + { showPin /> + + + + + + )} diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index d0b253288..fe182f18c 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -111,6 +111,15 @@ export default class DocumentsStore extends BaseStore { ); } + rootInCollection(collectionId: string): Document[] { + const collection = this.rootStore.collections.get(collectionId); + if (!collection) { + return []; + } + + return compact(collection.documents.map((node) => this.get(node.id))); + } + leastRecentlyUpdatedInCollection(collectionId: string): Document[] { return orderBy(this.inCollection(collectionId), "updatedAt", "asc"); } diff --git a/server/api/documents.js b/server/api/documents.js index 1916c138e..de7e1966e 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -37,7 +37,7 @@ const { authorize, cannot } = policy; const router = new Router(); router.post("documents.list", auth(), pagination(), async (ctx) => { - const { + let { sort = "updatedAt", template, backlinkDocumentId, @@ -70,6 +70,7 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { where = { ...where, createdById }; } + let documentIds = []; // if a specific collection is passed then we need to check auth to view it if (collectionId) { ctx.assertUuid(collectionId, "collection must be a UUID"); @@ -80,6 +81,15 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { }).findByPk(collectionId); authorize(user, "read", collection); + // index sort is special because it uses the order of the documents in the + // collection.documentStructure rather than a database column + if (sort === "index") { + documentIds = collection.documentStructure + .map((node) => node.id) + .slice(ctx.state.pagination.offset, ctx.state.pagination.limit); + where = { ...where, id: documentIds }; + } + // otherwise, filter by all collections the user has access to } else { const collectionIds = await user.collectionIds(); @@ -91,6 +101,12 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { where = { ...where, parentDocumentId }; } + // Explicitly passing 'null' as the parentDocumentId allows listing documents + // that have no parent document (aka they are at the root of the collection) + if (parentDocumentId === null) { + where = { ...where, parentDocumentId: { [Op.eq]: null } }; + } + if (backlinkDocumentId) { ctx.assertUuid(backlinkDocumentId, "backlinkDocumentId must be a UUID"); @@ -107,6 +123,10 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { }; } + if (sort === "index") { + sort = "updatedAt"; + } + // add the users starred state to the response by default const starredScope = { method: ["withStarred", user.id] }; const collectionScope = { method: ["withCollection", user.id] }; @@ -123,6 +143,14 @@ router.post("documents.list", auth(), pagination(), async (ctx) => { limit: ctx.state.pagination.limit, }); + // index sort is special because it uses the order of the documents in the + // collection.documentStructure rather than a database column + if (documentIds.length) { + documents.sort( + (a, b) => documentIds.indexOf(a.id) - documentIds.indexOf(b.id) + ); + } + const data = await Promise.all( documents.map((document) => presentDocument(document)) ); diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 76c7c4062..bde70bbde 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -433,7 +433,27 @@ describe("#documents.list", () => { expect(body.data[0].id).toEqual(document.id); }); - it("should not return unpublished documents", async () => { + it("should allow filtering documents with no parent", async () => { + const { user, document } = await seed(); + await buildDocument({ + title: "child document", + text: "random text", + parentDocumentId: document.id, + userId: user.id, + teamId: user.teamId, + }); + + const res = await server.post("/api/documents.list", { + body: { token: user.getJwtToken(), parentDocumentId: null }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(1); + expect(body.data[0].id).toEqual(document.id); + }); + + it("should not return draft documents", async () => { const { user, document } = await seed(); document.publishedAt = null; await document.save(); @@ -493,6 +513,32 @@ describe("#documents.list", () => { expect(body.data[1].id).toEqual(anotherDoc.id); }); + it("should allow sorting by collection index", async () => { + const { user, document, collection } = await seed(); + const anotherDoc = await buildDocument({ + title: "another document", + text: "random text", + userId: user.id, + teamId: user.teamId, + collectionId: collection.id, + }); + await collection.addDocumentToStructure(anotherDoc, 0); + + const res = await server.post("/api/documents.list", { + body: { + token: user.getJwtToken(), + collectionId: collection.id, + sort: "index", + direction: "ASC", + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data[0].id).toEqual(anotherDoc.id); + expect(body.data[1].id).toEqual(document.id); + }); + it("should allow filtering by collection", async () => { const { user, document } = await seed(); const res = await server.post("/api/documents.list", { diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index bd4b5f072..d8c075809 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -249,7 +249,6 @@ "Edit {{noun}}": "Edit {{noun}}", "New from template": "New from template", "Publish": "Publish", - "Publish document": "Publish document", "Publishing": "Publishing", "Are you sure you want to delete the <2>{{documentTitle}} template?": "Are you sure you want to delete the <2>{{documentTitle}} template?", "Are you sure about that? Deleting the <2>{{documentTitle}} document will delete all of its history and any nested documents.": "Are you sure about that? Deleting the <2>{{documentTitle}} document will delete all of its history and any nested documents.",