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:
@ -188,7 +188,7 @@ describe('#documents.list', async () => {
|
|||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data.length).toEqual(2);
|
expect(body.data.length).toEqual(1);
|
||||||
expect(body.data[0].id).toEqual(document.id);
|
expect(body.data[0].id).toEqual(document.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ describe('#documents.list', async () => {
|
|||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data.length).toEqual(1);
|
expect(body.data.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return documents in private collections not a member of', async () => {
|
it('should not return documents in private collections not a member of', async () => {
|
||||||
@ -222,13 +222,20 @@ describe('#documents.list', async () => {
|
|||||||
|
|
||||||
it('should allow changing sort direction', async () => {
|
it('should allow changing sort direction', async () => {
|
||||||
const { user, document } = await seed();
|
const { user, document } = await seed();
|
||||||
|
const anotherDoc = await buildDocument({
|
||||||
|
title: 'another document',
|
||||||
|
text: 'random text',
|
||||||
|
userId: user.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
});
|
||||||
const res = await server.post('/api/documents.list', {
|
const res = await server.post('/api/documents.list', {
|
||||||
body: { token: user.getJwtToken(), direction: 'ASC' },
|
body: { token: user.getJwtToken(), direction: 'ASC' },
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data[1].id).toEqual(document.id);
|
expect(body.data[0].id).toEqual(document.id);
|
||||||
|
expect(body.data[1].id).toEqual(anotherDoc.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow filtering by collection', async () => {
|
it('should allow filtering by collection', async () => {
|
||||||
@ -242,7 +249,7 @@ describe('#documents.list', async () => {
|
|||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data.length).toEqual(2);
|
expect(body.data.length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
@ -339,7 +346,7 @@ describe('#documents.search', async () => {
|
|||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data.length).toEqual(1);
|
expect(body.data.length).toEqual(1);
|
||||||
expect(body.data[0].document.text).toEqual('# Much guidance');
|
expect(body.data[0].document.text).toEqual('# Much test support');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return results in ranked order', async () => {
|
it('should return results in ranked order', async () => {
|
||||||
|
12
server/migrations/20190704070630-welcome-docs.js
Normal file
12
server/migrations/20190704070630-welcome-docs.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn('documents', 'isWelcome', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn('documents', 'isWelcome');
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,6 @@ import randomstring from 'randomstring';
|
|||||||
import { DataTypes, sequelize } from '../sequelize';
|
import { DataTypes, sequelize } from '../sequelize';
|
||||||
import Document from './Document';
|
import Document from './Document';
|
||||||
import CollectionUser from './CollectionUser';
|
import CollectionUser from './CollectionUser';
|
||||||
import { welcomeMessage } from '../utils/onboarding';
|
|
||||||
|
|
||||||
slug.defaults.mode = 'rfc3986';
|
slug.defaults.mode = 'rfc3986';
|
||||||
|
|
||||||
@ -37,33 +36,6 @@ const Collection = sequelize.define(
|
|||||||
beforeValidate: (collection: Collection) => {
|
beforeValidate: (collection: Collection) => {
|
||||||
collection.urlId = collection.urlId || randomstring.generate(10);
|
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: {
|
getterMethods: {
|
||||||
url() {
|
url() {
|
||||||
@ -140,7 +112,9 @@ Collection.prototype.addDocumentToStructure = async function(
|
|||||||
index: number,
|
index: number,
|
||||||
options = {}
|
options = {}
|
||||||
) {
|
) {
|
||||||
if (!this.documentStructure) return;
|
if (!this.documentStructure) {
|
||||||
|
this.documentStructure = [];
|
||||||
|
}
|
||||||
|
|
||||||
let transaction;
|
let transaction;
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ describe('#addDocumentToStructure', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await collection.addDocumentToStructure(newDocument);
|
await collection.addDocumentToStructure(newDocument);
|
||||||
expect(collection.documentStructure.length).toBe(3);
|
expect(collection.documentStructure.length).toBe(2);
|
||||||
expect(collection.documentStructure[2].id).toBe(id);
|
expect(collection.documentStructure[1].id).toBe(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should add with an index', async () => {
|
test('should add with an index', async () => {
|
||||||
@ -38,7 +38,7 @@ describe('#addDocumentToStructure', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await collection.addDocumentToStructure(newDocument, 1);
|
await collection.addDocumentToStructure(newDocument, 1);
|
||||||
expect(collection.documentStructure.length).toBe(3);
|
expect(collection.documentStructure.length).toBe(2);
|
||||||
expect(collection.documentStructure[1].id).toBe(id);
|
expect(collection.documentStructure[1].id).toBe(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,10 +52,10 @@ describe('#addDocumentToStructure', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await collection.addDocumentToStructure(newDocument, 1);
|
await collection.addDocumentToStructure(newDocument, 1);
|
||||||
expect(collection.documentStructure.length).toBe(2);
|
expect(collection.documentStructure.length).toBe(1);
|
||||||
expect(collection.documentStructure[1].id).toBe(document.id);
|
expect(collection.documentStructure[0].id).toBe(document.id);
|
||||||
expect(collection.documentStructure[1].children.length).toBe(1);
|
expect(collection.documentStructure[0].children.length).toBe(1);
|
||||||
expect(collection.documentStructure[1].children[0].id).toBe(id);
|
expect(collection.documentStructure[0].children[0].id).toBe(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should add as a child if with parent with index', async () => {
|
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(newDocument);
|
||||||
await collection.addDocumentToStructure(secondDocument, 0);
|
await collection.addDocumentToStructure(secondDocument, 0);
|
||||||
expect(collection.documentStructure.length).toBe(2);
|
expect(collection.documentStructure.length).toBe(1);
|
||||||
expect(collection.documentStructure[1].id).toBe(document.id);
|
expect(collection.documentStructure[0].id).toBe(document.id);
|
||||||
expect(collection.documentStructure[1].children.length).toBe(2);
|
expect(collection.documentStructure[0].children.length).toBe(2);
|
||||||
expect(collection.documentStructure[1].children[0].id).toBe(id);
|
expect(collection.documentStructure[0].children[0].id).toBe(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('options: documentJson', async () => {
|
describe('options: documentJson', async () => {
|
||||||
@ -101,8 +101,8 @@ describe('#addDocumentToStructure', async () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(collection.documentStructure[2].children.length).toBe(1);
|
expect(collection.documentStructure[1].children.length).toBe(1);
|
||||||
expect(collection.documentStructure[2].children[0].id).toBe(id);
|
expect(collection.documentStructure[1].children[0].id).toBe(id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -112,16 +112,15 @@ describe('#updateDocument', () => {
|
|||||||
const { collection, document } = await seed();
|
const { collection, document } = await seed();
|
||||||
|
|
||||||
document.title = 'Updated title';
|
document.title = 'Updated title';
|
||||||
await document.save();
|
|
||||||
|
|
||||||
|
await document.save();
|
||||||
await collection.updateDocument(document);
|
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 () => {
|
test("should update child document's data", async () => {
|
||||||
const { collection, document } = await seed();
|
const { collection, document } = await seed();
|
||||||
// Add a child for testing
|
|
||||||
const newDocument = await Document.create({
|
const newDocument = await Document.create({
|
||||||
parentDocumentId: document.id,
|
parentDocumentId: document.id,
|
||||||
collectionId: collection.id,
|
collectionId: collection.id,
|
||||||
@ -139,7 +138,7 @@ describe('#updateDocument', () => {
|
|||||||
|
|
||||||
await collection.updateDocument(newDocument);
|
await collection.updateDocument(newDocument);
|
||||||
|
|
||||||
expect(collection.documentStructure[1].children[0].title).toBe(
|
expect(collection.documentStructure[0].children[0].title).toBe(
|
||||||
'Updated title'
|
'Updated title'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -158,7 +157,7 @@ describe('#removeDocument', () => {
|
|||||||
const { collection, document } = await seed();
|
const { collection, document } = await seed();
|
||||||
|
|
||||||
await collection.deleteDocument(document);
|
await collection.deleteDocument(document);
|
||||||
expect(collection.documentStructure.length).toBe(1);
|
expect(collection.documentStructure.length).toBe(0);
|
||||||
|
|
||||||
// Verify that the document was removed
|
// Verify that the document was removed
|
||||||
const collectionDocuments = await Document.findAndCountAll({
|
const collectionDocuments = await Document.findAndCountAll({
|
||||||
@ -166,7 +165,7 @@ describe('#removeDocument', () => {
|
|||||||
collectionId: collection.id,
|
collectionId: collection.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(collectionDocuments.count).toBe(1);
|
expect(collectionDocuments.count).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should remove a document with child documents', async () => {
|
test('should remove a document with child documents', async () => {
|
||||||
@ -184,17 +183,17 @@ describe('#removeDocument', () => {
|
|||||||
text: 'content',
|
text: 'content',
|
||||||
});
|
});
|
||||||
await collection.addDocumentToStructure(newDocument);
|
await collection.addDocumentToStructure(newDocument);
|
||||||
expect(collection.documentStructure[1].children.length).toBe(1);
|
expect(collection.documentStructure[0].children.length).toBe(1);
|
||||||
|
|
||||||
// Remove the document
|
// Remove the document
|
||||||
await collection.deleteDocument(document);
|
await collection.deleteDocument(document);
|
||||||
expect(collection.documentStructure.length).toBe(1);
|
expect(collection.documentStructure.length).toBe(0);
|
||||||
const collectionDocuments = await Document.findAndCountAll({
|
const collectionDocuments = await Document.findAndCountAll({
|
||||||
where: {
|
where: {
|
||||||
collectionId: collection.id,
|
collectionId: collection.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(collectionDocuments.count).toBe(1);
|
expect(collectionDocuments.count).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should remove a child document', async () => {
|
test('should remove a child document', async () => {
|
||||||
@ -213,21 +212,20 @@ describe('#removeDocument', () => {
|
|||||||
text: 'content',
|
text: 'content',
|
||||||
});
|
});
|
||||||
await collection.addDocumentToStructure(newDocument);
|
await collection.addDocumentToStructure(newDocument);
|
||||||
expect(collection.documentStructure.length).toBe(2);
|
expect(collection.documentStructure.length).toBe(1);
|
||||||
expect(collection.documentStructure[1].children.length).toBe(1);
|
expect(collection.documentStructure[0].children.length).toBe(1);
|
||||||
|
|
||||||
// Remove the document
|
// Remove the document
|
||||||
await collection.deleteDocument(newDocument);
|
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[0].children.length).toBe(0);
|
||||||
expect(collection.documentStructure[1].children.length).toBe(0);
|
|
||||||
|
|
||||||
const collectionDocuments = await Document.findAndCountAll({
|
const collectionDocuments = await Document.findAndCountAll({
|
||||||
where: {
|
where: {
|
||||||
collectionId: collection.id,
|
collectionId: collection.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(collectionDocuments.count).toBe(2);
|
expect(collectionDocuments.count).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -88,6 +88,7 @@ const Document = sequelize.define(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
text: DataTypes.TEXT,
|
text: DataTypes.TEXT,
|
||||||
|
isWelcome: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||||
archivedAt: DataTypes.DATE,
|
archivedAt: DataTypes.DATE,
|
||||||
publishedAt: DataTypes.DATE,
|
publishedAt: DataTypes.DATE,
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
import util from 'util';
|
||||||
|
import path from 'path';
|
||||||
import { DataTypes, sequelize, Op } from '../sequelize';
|
import { DataTypes, sequelize, Op } from '../sequelize';
|
||||||
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
|
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
|
||||||
import {
|
import {
|
||||||
stripSubdomain,
|
stripSubdomain,
|
||||||
RESERVED_SUBDOMAINS,
|
RESERVED_SUBDOMAINS,
|
||||||
} from '../../shared/utils/domains';
|
} from '../../shared/utils/domains';
|
||||||
|
import parseTitle from '../../shared/utils/parseTitle';
|
||||||
|
|
||||||
import Collection from './Collection';
|
import Collection from './Collection';
|
||||||
|
import Document from './Document';
|
||||||
import User from './User';
|
import User from './User';
|
||||||
|
|
||||||
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
|
||||||
const Team = sequelize.define(
|
const Team = sequelize.define(
|
||||||
'team',
|
'team',
|
||||||
{
|
{
|
||||||
@ -112,13 +119,37 @@ Team.prototype.provisionSubdomain = async function(subdomain) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Team.prototype.provisionFirstCollection = async function(userId) {
|
Team.prototype.provisionFirstCollection = async function(userId) {
|
||||||
return await Collection.create({
|
const collection = await Collection.create({
|
||||||
name: 'General',
|
name: 'Welcome',
|
||||||
description: '',
|
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',
|
type: 'atlas',
|
||||||
teamId: this.id,
|
teamId: this.id,
|
||||||
creatorId: userId,
|
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) {
|
Team.prototype.addAdmin = async function(user: User) {
|
||||||
|
17
server/onboarding/editor.md
Normal file
17
server/onboarding/editor.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# 📝 Our Editor
|
||||||
|
|
||||||
|
The heart of Outline is the document editor. We let you write in the way that you prefer – be it Markdown, WYSIWYG, or taking advantage of the many keyboard shortcuts.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Markdown
|
||||||
|
|
||||||
|
If you’re comfortable writing markdown then all of the usual shortcuts are supported, for example type \*\*bold\*\* and hit `space` to instantly create bold text. If you forget some syntax or are after a refresher, simply type `?` to access the keyboard shortcut help.
|
||||||
|
|
||||||
|
*Tip:* You can even paste markdown from elsewhere directly into a document.
|
||||||
|
|
||||||
|
## Blocks
|
||||||
|
|
||||||
|
The editor supports a variety of content blocks including images, tables, lists, quotes, and more. You can also drag and drop images to include them in your document or paste a link to embed content from one of the many supported [integrations](/integrations)
|
||||||
|
|
||||||
|
*Tip:* Headings are collapsible, just click the arrow to the left of any heading to temporarily hide the content below.
|
28
server/onboarding/integrations.md
Normal file
28
server/onboarding/integrations.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# 🚀 Integrations & API
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
Outline supports tons of the most popular tools on the market out of the box. Just paste links to a YouTube video, Figma design, or Realtimeboard to get instant live-embeds in your documents.
|
||||||
|
|
||||||
|
Our integration code is [open-source](https://github.com/outline/outline) and we encourage third party developers and the community to build support for additional tools! Find out more on our [integrations directory](https://www.getoutline.com/integrations).
|
||||||
|
|
||||||
|
*Tip:* Most integrations work by simply pasting a link from a supported service into a document.
|
||||||
|
|
||||||
|
## Slack
|
||||||
|
|
||||||
|
If your team is using Slack to communicate then you’ll definitely want to enable our [Slack App](https://getoutline.slack.com/apps/A0W3UMKBQ-outline) to get instant link unfurling for Outline documents and access to the `/outline` slash command to search your knowledgebase from within Slack.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Have some technical skills? Outline is built on a fully featured RPC-style [API](https://www.getoutline.com/developers). Create (or even append to) documents, collections, provision users, and more programmatically. All documents are edited and stored in markdown format – try out this CURL request!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -XPOST -H "Content-type: application/json" -d '{
|
||||||
|
"title": "My first document",
|
||||||
|
"text": "# My first document \n Hello from the API 👋",
|
||||||
|
"collectionId": "COLLECTION_ID", // find the collection id in the URL bar
|
||||||
|
"token": "API_TOKEN", // get an API token from https://www.getoutline.com/settings/tokens
|
||||||
|
"publish": true
|
||||||
|
}' 'https://www.getoutline.com/api/documents.create'
|
||||||
|
```
|
||||||
|
|
19
server/onboarding/philosophy.md
Normal file
19
server/onboarding/philosophy.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# 👋 What is Outline
|
||||||
|
|
||||||
|
Outline is a place to build your team knowledge base, you could think of it like your team’s shared library – a place for important documentation, notes, and ideas to live and be discovered. Some things you might want to keep in Outline:
|
||||||
|
|
||||||
|
- Documentation
|
||||||
|
- Sales playbooks
|
||||||
|
- Support scripts
|
||||||
|
- Onboarding
|
||||||
|
- HR documents
|
||||||
|
- Meeting notes
|
||||||
|
- …and more
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
Outline allows you to organize documents in "collections", for example these could represent topics like Sales, Product, or HR. Within collections documents can be interlinked and deeply nested to easily build relationships within your knowledgebase.
|
||||||
|
|
||||||
|
## Search
|
||||||
|
|
||||||
|
Outline is built to be crazy fast, and that includes [search](/search). You can start searching from anywhere with the `CMD+K` shortcut. Then filter by time, author, and more to get to the info you need.
|
9
server/onboarding/support.md
Normal file
9
server/onboarding/support.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# ❤️ Support
|
||||||
|
|
||||||
|
We hate bugs as much as you and do everything possible to keep the app bug-free. Help us out by getting in touch with the team if you see any problems with Outline. You can email [hello@getoutline.com](hello@getoutline.com) directly and we’ll get back to you (hopefully with a fix!) as soon as possible.
|
||||||
|
|
||||||
|
If you already have a GitHub account then you can also submit issues directly to the development team on our [open issue tracker](https://github.com/outline/outline/issues).
|
||||||
|
|
||||||
|
## Ideas
|
||||||
|
|
||||||
|
We’d love to hear your ideas about how Outline can be improved and features you would like to see built. The best place to let the team know is through our [Spectrum community](https://spectrum.chat/outline).
|
@ -70,11 +70,10 @@ const seed = async () => {
|
|||||||
userId: collection.creatorId,
|
userId: collection.creatorId,
|
||||||
lastModifiedById: collection.creatorId,
|
lastModifiedById: collection.creatorId,
|
||||||
createdById: collection.creatorId,
|
createdById: collection.creatorId,
|
||||||
publishedAt: new Date(),
|
title: 'First ever document',
|
||||||
title: 'Second document',
|
text: '# Much test support',
|
||||||
text: '# Much guidance',
|
|
||||||
});
|
});
|
||||||
|
await document.publish();
|
||||||
await collection.reload();
|
await collection.reload();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
// @flow
|
|
||||||
export const welcomeMessage = (collectionId: string) =>
|
|
||||||
`# Welcome to Outline
|
|
||||||
|
|
||||||
Outline is a place for your team to build your knowledge base. This can include:
|
|
||||||
|
|
||||||
* Team wiki
|
|
||||||
* Documentation
|
|
||||||
* Playbooks
|
|
||||||
* Employee onboarding
|
|
||||||
* ...or anything you can think of
|
|
||||||
|
|
||||||
## 🖋 A powerful editor
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Outline's editor lets you easily format your documents with keyboard shortcuts, Markdown syntax or by simply highlighting the text and making your selections. To add images, just drag and drop them to your canvas.
|
|
||||||
|
|
||||||
## 👩💻 Developer friendly
|
|
||||||
|
|
||||||
Outline features an [API](https://www.getoutline.com/developers) for programatic document creation. To create your first document using the API, simply write it in Markdown and make a call to add it into your collection:
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
const newDocument = {
|
|
||||||
title: 'Getting started with codebase',
|
|
||||||
text: 'All the information needed in Markdown',
|
|
||||||
collectionId: '${collectionId}',
|
|
||||||
token: 'API_KEY', // Replace with a value from https://www.getoutline.com/settings/tokens
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('https://www.getoutline.com/api/documents.create', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(newDocument),
|
|
||||||
});
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## 👋 Say hi to the team
|
|
||||||
|
|
||||||
Outline is built by a small team and we would love to get to know our users. Drop by at [our Spectrum community](https://spectrum.chat/outline) or [drop us an email](mailto:hello@getoutline.com).
|
|
||||||
`;
|
|
Reference in New Issue
Block a user