feat: Templates (#1399)

* Migrations
* New from template
* fix: Don't allow public share of template
* chore: Template badges
* fix: Collection active
* feat: New doc button on template list item
* feat: New template menu
* fix: Sorting
* feat: Templates onboarding notice
* fix: New doc button showing on archived/deleted templates
This commit is contained in:
Tom Moor
2020-08-08 15:18:37 -07:00
committed by GitHub
parent 59c24aba7c
commit 869fc086d6
51 changed files with 1007 additions and 327 deletions

View File

@@ -29,7 +29,12 @@ const { authorize, cannot } = policy;
const router = new Router();
router.post("documents.list", auth(), pagination(), async ctx => {
const { sort = "updatedAt", backlinkDocumentId, parentDocumentId } = ctx.body;
const {
sort = "updatedAt",
template,
backlinkDocumentId,
parentDocumentId,
} = ctx.body;
// collection and user are here for backwards compatibility
const collectionId = ctx.body.collectionId || ctx.body.collection;
@@ -41,6 +46,10 @@ router.post("documents.list", auth(), pagination(), async ctx => {
const user = ctx.state.user;
let where = { teamId: user.teamId };
if (template) {
where = { ...where, template: true };
}
// if a specific user is passed then add to filters. If the user doesn't
// exist in the team then nothing will be returned, so no need to check auth
if (createdById) {
@@ -682,6 +691,8 @@ router.post("documents.create", auth(), async ctx => {
publish,
collectionId,
parentDocumentId,
templateId,
template,
index,
} = ctx.body;
const editorVersion = ctx.headers["x-editor-version"];
@@ -717,6 +728,12 @@ router.post("documents.create", auth(), async ctx => {
authorize(user, "read", parentDocument, { collection });
}
let templateDocument;
if (templateId) {
templateDocument = await Document.findByPk(templateId, { userId: user.id });
authorize(user, "read", templateDocument);
}
let document = await Document.create({
parentDocumentId,
editorVersion,
@@ -725,8 +742,10 @@ router.post("documents.create", auth(), async ctx => {
userId: user.id,
lastModifiedById: user.id,
createdById: user.id,
title,
text,
template,
templateId: templateDocument ? templateDocument.id : undefined,
title: templateDocument ? templateDocument.title : title,
text: templateDocument ? templateDocument.text : text,
});
await Event.create({
@@ -735,7 +754,7 @@ router.post("documents.create", auth(), async ctx => {
collectionId: document.collectionId,
teamId: document.teamId,
actorId: user.id,
data: { title: document.title },
data: { title: document.title, templateId },
ip: ctx.request.ip,
});
@@ -767,6 +786,46 @@ router.post("documents.create", auth(), async ctx => {
};
});
router.post("documents.templatize", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const original = await Document.findByPk(id, { userId: user.id });
authorize(user, "update", original);
let document = await Document.create({
editorVersion: original.editorVersion,
collectionId: original.collectionId,
teamId: original.teamId,
userId: user.id,
publishedAt: new Date(),
lastModifiedById: user.id,
createdById: user.id,
template: true,
title: original.title,
text: original.text,
});
await Event.create({
name: "documents.create",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
actorId: user.id,
data: { title: document.title, template: true },
ip: ctx.request.ip,
});
// reload to get all of the data needed to present (user, collection etc)
document = await Document.findByPk(document.id, { userId: user.id });
ctx.body = {
data: await presentDocument(document),
policies: presentPolicies(user, [document]),
};
});
router.post("documents.update", auth(), async ctx => {
const {
id,
@@ -776,6 +835,7 @@ router.post("documents.update", auth(), async ctx => {
autosave,
done,
lastRevision,
templateId,
append,
} = ctx.body;
const editorVersion = ctx.headers["x-editor-version"];
@@ -795,6 +855,7 @@ router.post("documents.update", auth(), async ctx => {
// Update document
if (title) document.title = title;
if (editorVersion) document.editorVersion = editorVersion;
if (templateId) document.templateId = templateId;
if (append) {
document.text += text;

View File

@@ -0,0 +1,20 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('documents', 'template', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
await queryInterface.addColumn('documents', 'templateId', {
type: Sequelize.UUID,
allowNull: true
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('documents', 'templateId');
await queryInterface.removeColumn('documents', 'template');
}
};

View File

@@ -98,6 +98,7 @@ const Document = sequelize.define(
},
},
version: DataTypes.SMALLINT,
template: DataTypes.BOOLEAN,
editorVersion: DataTypes.STRING,
text: DataTypes.TEXT,
@@ -142,6 +143,10 @@ Document.associate = models => {
as: "team",
foreignKey: "teamId",
});
Document.belongsTo(models.Document, {
as: "document",
foreignKey: "templateId",
});
Document.belongsTo(models.User, {
as: "createdBy",
foreignKey: "createdById",
@@ -431,20 +436,28 @@ Document.searchForUser = async (
// Hooks
Document.addHook("beforeSave", async model => {
if (!model.publishedAt) return;
if (!model.publishedAt || model.template) {
return;
}
const collection = await Collection.findByPk(model.collectionId);
if (!collection || collection.type !== "atlas") return;
if (!collection || collection.type !== "atlas") {
return;
}
await collection.updateDocument(model);
model.collection = collection;
});
Document.addHook("afterCreate", async model => {
if (!model.publishedAt) return;
if (!model.publishedAt || model.template) {
return;
}
const collection = await Collection.findByPk(model.collectionId);
if (!collection || collection.type !== "atlas") return;
if (!collection || collection.type !== "atlas") {
return;
}
await collection.addDocumentToStructure(model);
model.collection = collection;

View File

@@ -71,6 +71,7 @@ Event.AUDIT_EVENTS = [
"users.suspend",
"users.activate",
"users.delete",
"documents.create",
"documents.publish",
"documents.update",
"documents.archive",

View File

@@ -31,6 +31,7 @@ allow(User, ["share"], Document, (user, document) => {
allow(User, ["star", "unstar"], Document, (user, document) => {
if (document.archivedAt) return false;
if (document.deletedAt) return false;
if (document.template) return false;
if (!document.publishedAt) return false;
invariant(
@@ -58,6 +59,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.template) return false;
if (!document.publishedAt) return false;
invariant(
@@ -72,6 +74,7 @@ allow(User, "createChildDocument", Document, (user, document) => {
allow(User, ["move", "pin", "unpin"], Document, (user, document) => {
if (document.archivedAt) return false;
if (document.deletedAt) return false;
if (document.template) return false;
if (!document.publishedAt) return false;
invariant(

View File

@@ -54,6 +54,8 @@ export default async function present(document: Document, options: ?Options) {
archivedAt: document.archivedAt,
deletedAt: document.deletedAt,
teamId: document.teamId,
template: document.template,
templateId: document.templateId,
collaborators: [],
starred: document.starred ? !!document.starred.length : undefined,
revision: document.revisionCount,