feat: Improved onboarding documents (#970)

* feat: New onboarding documents

* Images -> blocks

* Add tips

* test: removes assumptions of welcome documents

this actually results in the tests being much more understandable too

* add db flag when document was created from welcome flow
This commit is contained in:
Tom Moor
2019-07-04 10:33:00 -07:00
committed by GitHub
parent eb3a1dd673
commit ccc0906b0a
12 changed files with 163 additions and 111 deletions

View File

@@ -5,7 +5,6 @@ import randomstring from 'randomstring';
import { DataTypes, sequelize } from '../sequelize';
import Document from './Document';
import CollectionUser from './CollectionUser';
import { welcomeMessage } from '../utils/onboarding';
slug.defaults.mode = 'rfc3986';
@@ -37,33 +36,6 @@ const Collection = sequelize.define(
beforeValidate: (collection: Collection) => {
collection.urlId = collection.urlId || randomstring.generate(10);
},
afterCreate: async (collection: Collection) => {
const team = await collection.getTeam();
const collections = await team.getCollections();
// Don't auto-create for journal types, yet
if (collection.type !== 'atlas') return;
if (collections.length < 2) {
// Create intro document if first collection for team
const document = await Document.create({
parentDocumentId: null,
collectionId: collection.id,
teamId: collection.teamId,
userId: collection.creatorId,
lastModifiedById: collection.creatorId,
createdById: collection.creatorId,
publishedAt: new Date(),
title: 'Welcome to Outline',
text: welcomeMessage(collection.id),
});
collection.documentStructure = [document.toJSON()];
} else {
// Let user create first document
collection.documentStructure = [];
}
await collection.save();
},
},
getterMethods: {
url() {
@@ -140,7 +112,9 @@ Collection.prototype.addDocumentToStructure = async function(
index: number,
options = {}
) {
if (!this.documentStructure) return;
if (!this.documentStructure) {
this.documentStructure = [];
}
let transaction;

View File

@@ -24,8 +24,8 @@ describe('#addDocumentToStructure', async () => {
});
await collection.addDocumentToStructure(newDocument);
expect(collection.documentStructure.length).toBe(3);
expect(collection.documentStructure[2].id).toBe(id);
expect(collection.documentStructure.length).toBe(2);
expect(collection.documentStructure[1].id).toBe(id);
});
test('should add with an index', async () => {
@@ -38,7 +38,7 @@ describe('#addDocumentToStructure', async () => {
});
await collection.addDocumentToStructure(newDocument, 1);
expect(collection.documentStructure.length).toBe(3);
expect(collection.documentStructure.length).toBe(2);
expect(collection.documentStructure[1].id).toBe(id);
});
@@ -52,10 +52,10 @@ describe('#addDocumentToStructure', async () => {
});
await collection.addDocumentToStructure(newDocument, 1);
expect(collection.documentStructure.length).toBe(2);
expect(collection.documentStructure[1].id).toBe(document.id);
expect(collection.documentStructure[1].children.length).toBe(1);
expect(collection.documentStructure[1].children[0].id).toBe(id);
expect(collection.documentStructure.length).toBe(1);
expect(collection.documentStructure[0].id).toBe(document.id);
expect(collection.documentStructure[0].children.length).toBe(1);
expect(collection.documentStructure[0].children[0].id).toBe(id);
});
test('should add as a child if with parent with index', async () => {
@@ -74,10 +74,10 @@ describe('#addDocumentToStructure', async () => {
await collection.addDocumentToStructure(newDocument);
await collection.addDocumentToStructure(secondDocument, 0);
expect(collection.documentStructure.length).toBe(2);
expect(collection.documentStructure[1].id).toBe(document.id);
expect(collection.documentStructure[1].children.length).toBe(2);
expect(collection.documentStructure[1].children[0].id).toBe(id);
expect(collection.documentStructure.length).toBe(1);
expect(collection.documentStructure[0].id).toBe(document.id);
expect(collection.documentStructure[0].children.length).toBe(2);
expect(collection.documentStructure[0].children[0].id).toBe(id);
});
describe('options: documentJson', async () => {
@@ -101,8 +101,8 @@ describe('#addDocumentToStructure', async () => {
],
},
});
expect(collection.documentStructure[2].children.length).toBe(1);
expect(collection.documentStructure[2].children[0].id).toBe(id);
expect(collection.documentStructure[1].children.length).toBe(1);
expect(collection.documentStructure[1].children[0].id).toBe(id);
});
});
});
@@ -112,16 +112,15 @@ describe('#updateDocument', () => {
const { collection, document } = await seed();
document.title = 'Updated title';
await document.save();
await document.save();
await collection.updateDocument(document);
expect(collection.documentStructure[1].title).toBe('Updated title');
expect(collection.documentStructure[0].title).toBe('Updated title');
});
test("should update child document's data", async () => {
const { collection, document } = await seed();
// Add a child for testing
const newDocument = await Document.create({
parentDocumentId: document.id,
collectionId: collection.id,
@@ -139,7 +138,7 @@ describe('#updateDocument', () => {
await collection.updateDocument(newDocument);
expect(collection.documentStructure[1].children[0].title).toBe(
expect(collection.documentStructure[0].children[0].title).toBe(
'Updated title'
);
});
@@ -158,7 +157,7 @@ describe('#removeDocument', () => {
const { collection, document } = await seed();
await collection.deleteDocument(document);
expect(collection.documentStructure.length).toBe(1);
expect(collection.documentStructure.length).toBe(0);
// Verify that the document was removed
const collectionDocuments = await Document.findAndCountAll({
@@ -166,7 +165,7 @@ describe('#removeDocument', () => {
collectionId: collection.id,
},
});
expect(collectionDocuments.count).toBe(1);
expect(collectionDocuments.count).toBe(0);
});
test('should remove a document with child documents', async () => {
@@ -184,17 +183,17 @@ describe('#removeDocument', () => {
text: 'content',
});
await collection.addDocumentToStructure(newDocument);
expect(collection.documentStructure[1].children.length).toBe(1);
expect(collection.documentStructure[0].children.length).toBe(1);
// Remove the document
await collection.deleteDocument(document);
expect(collection.documentStructure.length).toBe(1);
expect(collection.documentStructure.length).toBe(0);
const collectionDocuments = await Document.findAndCountAll({
where: {
collectionId: collection.id,
},
});
expect(collectionDocuments.count).toBe(1);
expect(collectionDocuments.count).toBe(0);
});
test('should remove a child document', async () => {
@@ -213,21 +212,20 @@ describe('#removeDocument', () => {
text: 'content',
});
await collection.addDocumentToStructure(newDocument);
expect(collection.documentStructure.length).toBe(2);
expect(collection.documentStructure[1].children.length).toBe(1);
expect(collection.documentStructure.length).toBe(1);
expect(collection.documentStructure[0].children.length).toBe(1);
// Remove the document
await collection.deleteDocument(newDocument);
expect(collection.documentStructure.length).toBe(2);
expect(collection.documentStructure.length).toBe(1);
expect(collection.documentStructure[0].children.length).toBe(0);
expect(collection.documentStructure[1].children.length).toBe(0);
const collectionDocuments = await Document.findAndCountAll({
where: {
collectionId: collection.id,
},
});
expect(collectionDocuments.count).toBe(2);
expect(collectionDocuments.count).toBe(1);
});
});

View File

@@ -88,6 +88,7 @@ const Document = sequelize.define(
},
},
text: DataTypes.TEXT,
isWelcome: { type: DataTypes.BOOLEAN, defaultValue: false },
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
archivedAt: DataTypes.DATE,
publishedAt: DataTypes.DATE,

View File

@@ -1,16 +1,23 @@
// @flow
import uuid from 'uuid';
import { URL } from 'url';
import fs from 'fs';
import util from 'util';
import path from 'path';
import { DataTypes, sequelize, Op } from '../sequelize';
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
import {
stripSubdomain,
RESERVED_SUBDOMAINS,
} from '../../shared/utils/domains';
import parseTitle from '../../shared/utils/parseTitle';
import Collection from './Collection';
import Document from './Document';
import User from './User';
const readFile = util.promisify(fs.readFile);
const Team = sequelize.define(
'team',
{
@@ -112,13 +119,37 @@ Team.prototype.provisionSubdomain = async function(subdomain) {
};
Team.prototype.provisionFirstCollection = async function(userId) {
return await Collection.create({
name: 'General',
description: '',
const collection = await Collection.create({
name: 'Welcome',
description:
'This collection is a quick guide to what Outline is all about. Feel free to delete this collection once your team is up to speed with the basics!',
type: 'atlas',
teamId: this.id,
creatorId: userId,
});
// For the first collection we go ahead and create some intitial documents to get
// the team started. You can edit these in /server/onboarding/x.md
const onboardingDocs = ['support', 'integrations', 'editor', 'philosophy'];
for (const name of onboardingDocs) {
const text = await readFile(
path.join(__dirname, '..', 'onboarding', `${name}.md`),
'utf8'
);
const { title } = parseTitle(text);
const document = await Document.create({
isWelcome: true,
parentDocumentId: null,
collectionId: collection.id,
teamId: collection.teamId,
userId: collection.creatorId,
lastModifiedById: collection.creatorId,
createdById: collection.creatorId,
title,
text,
});
await document.publish();
}
};
Team.prototype.addAdmin = async function(user: User) {