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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user