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:
@@ -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;
|
||||
|
||||
20
server/migrations/20200727051157-add-templates.js
Normal file
20
server/migrations/20200727051157-add-templates.js
Normal 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');
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -71,6 +71,7 @@ Event.AUDIT_EVENTS = [
|
||||
"users.suspend",
|
||||
"users.activate",
|
||||
"users.delete",
|
||||
"documents.create",
|
||||
"documents.publish",
|
||||
"documents.update",
|
||||
"documents.archive",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user