feat: Separate title from body (#1216)
* first pass at updating all Time components each second * fix a couple date variable typos * use class style state management instead of hooks * wip: Separate title from body * address feedback * test: Remove unused test * feat: You in publishing info language fix: Removal of secondary headings * After much deliberation… a migration is needed for this to be reliable * fix: Export to work with new title structure * fix: Untitled * fix: Consistent spacing of first editor node * fix: Emoji in title handling * fix: Time component not updating for new props * chore: Add createdAt case * fix: Conflict after merging new TOC * PR feedback * lint * fix: Heading level adjustment Co-authored-by: Taylor Lapeyre <taylorlapeyre@gmail.com>
This commit is contained in:
@@ -425,7 +425,7 @@ router.post('documents.revision', auth(), async ctx => {
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: presentRevision(revision),
|
||||
data: await presentRevision(revision),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -445,9 +445,13 @@ router.post('documents.revisions', auth(), pagination(), async ctx => {
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
const data = await Promise.all(
|
||||
revisions.map(revision => presentRevision(revision))
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: revisions.map(presentRevision),
|
||||
data,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
19
server/migrations/20200330053639-document-version.js
Normal file
19
server/migrations/20200330053639-document-version.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('documents', 'version', {
|
||||
type: Sequelize.SMALLINT,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('revisions', 'version', {
|
||||
type: Sequelize.SMALLINT,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('documents', 'version');
|
||||
await queryInterface.removeColumn('revisions', 'version');
|
||||
}
|
||||
};
|
||||
@@ -17,7 +17,8 @@ import Revision from './Revision';
|
||||
const Op = Sequelize.Op;
|
||||
const Markdown = new MarkdownSerializer();
|
||||
const URL_REGEX = /^[0-9a-zA-Z-_~]*-([a-zA-Z0-9]{10,15})$/;
|
||||
const DEFAULT_TITLE = 'Untitled';
|
||||
|
||||
export const DOCUMENT_VERSION = 1;
|
||||
|
||||
slug.defaults.mode = 'rfc3986';
|
||||
const slugify = text =>
|
||||
@@ -38,6 +39,7 @@ const createRevision = (doc, options = {}) => {
|
||||
text: doc.text,
|
||||
userId: doc.lastModifiedById,
|
||||
editorVersion: doc.editorVersion,
|
||||
version: doc.version,
|
||||
documentId: doc.id,
|
||||
},
|
||||
{
|
||||
@@ -50,16 +52,19 @@ const createUrlId = doc => {
|
||||
return (doc.urlId = doc.urlId || randomstring.generate(10));
|
||||
};
|
||||
|
||||
const beforeCreate = async doc => {
|
||||
doc.version = DOCUMENT_VERSION;
|
||||
return beforeSave(doc);
|
||||
};
|
||||
|
||||
const beforeSave = async doc => {
|
||||
const { emoji, title } = parseTitle(doc.text);
|
||||
const { emoji } = parseTitle(doc.text);
|
||||
|
||||
// emoji in the title is split out for easier display
|
||||
doc.emoji = emoji;
|
||||
|
||||
// ensure documents have a title
|
||||
if (!title) {
|
||||
doc.title = DEFAULT_TITLE;
|
||||
}
|
||||
doc.title = doc.title || '';
|
||||
|
||||
// add the current user as a collaborator on this doc
|
||||
if (!doc.collaboratorIds) doc.collaboratorIds = [];
|
||||
@@ -92,6 +97,7 @@ const Document = sequelize.define(
|
||||
},
|
||||
},
|
||||
},
|
||||
version: DataTypes.SMALLINT,
|
||||
editorVersion: DataTypes.STRING,
|
||||
text: DataTypes.TEXT,
|
||||
isWelcome: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||
@@ -105,7 +111,7 @@ const Document = sequelize.define(
|
||||
paranoid: true,
|
||||
hooks: {
|
||||
beforeValidate: createUrlId,
|
||||
beforeCreate: beforeSave,
|
||||
beforeCreate: beforeCreate,
|
||||
beforeUpdate: beforeSave,
|
||||
afterCreate: createRevision,
|
||||
afterUpdate: createRevision,
|
||||
@@ -427,6 +433,26 @@ Document.addHook('afterCreate', async model => {
|
||||
|
||||
// Instance methods
|
||||
|
||||
Document.prototype.toMarkdown = function() {
|
||||
const text = unescape(this.text);
|
||||
|
||||
if (this.version) {
|
||||
return `# ${this.title}\n\n${text}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
Document.prototype.migrateVersion = function() {
|
||||
// migrate from document version 0 -> 1 means removing the title from the
|
||||
// document text attribute.
|
||||
if (!this.version) {
|
||||
this.text = this.text.replace(/^#\s(.*)\n/, '');
|
||||
this.version = 1;
|
||||
return this.save({ silent: true, hooks: false });
|
||||
}
|
||||
};
|
||||
|
||||
// Note: This method marks the document and it's children as deleted
|
||||
// in the database, it does not permanantly delete them OR remove
|
||||
// from the collection structure.
|
||||
|
||||
@@ -7,6 +7,7 @@ const Revision = sequelize.define('revision', {
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
version: DataTypes.SMALLINT,
|
||||
editorVersion: DataTypes.STRING,
|
||||
title: DataTypes.STRING,
|
||||
text: DataTypes.TEXT,
|
||||
@@ -31,4 +32,14 @@ Revision.associate = models => {
|
||||
);
|
||||
};
|
||||
|
||||
Revision.prototype.migrateVersion = function() {
|
||||
// migrate from revision version 0 -> 1 means removing the title from the
|
||||
// revision text attribute.
|
||||
if (!this.version) {
|
||||
this.text = this.text.replace(/^#\s(.*)\n/, '');
|
||||
this.version = 1;
|
||||
return this.save({ silent: true, hooks: false });
|
||||
}
|
||||
};
|
||||
|
||||
export default Revision;
|
||||
|
||||
@@ -31,7 +31,9 @@ export default async function present(document: Document, options: ?Options) {
|
||||
...options,
|
||||
};
|
||||
|
||||
const text = options.isPublic
|
||||
await document.migrateVersion();
|
||||
|
||||
let text = options.isPublic
|
||||
? await replaceImageAttachments(document.text)
|
||||
: document.text;
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { Revision } from '../models';
|
||||
import presentUser from './user';
|
||||
|
||||
export default function present(revision: Revision) {
|
||||
export default async function present(revision: Revision) {
|
||||
await revision.migrateVersion();
|
||||
|
||||
return {
|
||||
id: revision.id,
|
||||
documentId: revision.documentId,
|
||||
|
||||
@@ -3,14 +3,13 @@ import fs from 'fs';
|
||||
import JSZip from 'jszip';
|
||||
import tmp from 'tmp';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import unescape from '../../shared/utils/unescape';
|
||||
import { Attachment, Collection, Document } from '../models';
|
||||
import { getImageByKey } from './s3';
|
||||
|
||||
async function addToArchive(zip, documents) {
|
||||
for (const doc of documents) {
|
||||
const document = await Document.findByPk(doc.id);
|
||||
let text = unescape(document.text);
|
||||
let text = document.toMarkdown();
|
||||
|
||||
const attachments = await Attachment.findAll({
|
||||
where: { documentId: document.id },
|
||||
@@ -21,7 +20,7 @@ async function addToArchive(zip, documents) {
|
||||
text = text.replace(attachment.redirectUrl, encodeURI(attachment.key));
|
||||
}
|
||||
|
||||
zip.file(`${document.title}.md`, text);
|
||||
zip.file(`${document.title || 'Untitled'}.md`, text);
|
||||
|
||||
if (doc.children && doc.children.length) {
|
||||
const folder = zip.folder(document.title);
|
||||
|
||||
Reference in New Issue
Block a user