diff --git a/app/models/Document.js b/app/models/Document.js index 50d08f82f..d751423b2 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -11,6 +11,8 @@ import type { User } from 'types'; import BaseModel from './BaseModel'; import Collection from './Collection'; +type SaveOptions = { publish: boolean, done: boolean, autosave: boolean }; + class Document extends BaseModel { isSaving: boolean = false; hasPendingChanges: boolean = false; @@ -168,7 +170,7 @@ class Document extends BaseModel { }; @action - save = async (publish: boolean = false, done: boolean = false) => { + save = async (options: SaveOptions) => { if (this.isSaving) return this; this.isSaving = true; @@ -180,8 +182,7 @@ class Document extends BaseModel { title: this.title, text: this.text, lastRevision: this.revision, - publish, - done, + ...options, }); } else { const data = { @@ -189,8 +190,7 @@ class Document extends BaseModel { collection: this.collection.id, title: this.title, text: this.text, - publish, - done, + ...options, }; if (this.parentDocument) { data.parentDocument = this.parentDocument; diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index e1e7e7cab..fef5f64b8 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -1,6 +1,7 @@ // @flow import * as React from 'react'; import get from 'lodash/get'; +import debounce from 'lodash/debounce'; import styled from 'styled-components'; import breakpoint from 'styled-components-breakpoint'; import { observable } from 'mobx'; @@ -32,6 +33,7 @@ import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; import Search from 'scenes/Search'; +const AUTOSAVE_INTERVAL = 3000; const DISCARD_CHANGES = ` You have unsaved changes. Are you sure you want to discard them? @@ -154,20 +156,20 @@ class DocumentScene extends React.Component { handleCloseMoveModal = () => (this.moveModalOpen = false); handleOpenMoveModal = () => (this.moveModalOpen = true); - onSave = async (options: { redirect?: boolean, publish?: boolean } = {}) => { - const { redirect, publish } = options; - + onSave = async ( + options: { redirect?: boolean, publish?: boolean, autosave?: boolean } = {} + ) => { let document = this.document; if (!document || !document.allowSave) return; this.editCache = null; this.isSaving = true; - this.isPublishing = !!publish; - document = await document.save(publish, redirect); + this.isPublishing = !!options.publish; + document = await document.save(options); this.isSaving = false; this.isPublishing = false; - if (redirect) { + if (options.redirect) { this.props.history.push(document.url); this.props.ui.setActiveDocument(document); } else if (this.props.newDocument) { @@ -176,6 +178,10 @@ class DocumentScene extends React.Component { } }; + autosave = debounce(async () => { + this.onSave({ redirect: false, autosave: true }); + }, AUTOSAVE_INTERVAL); + onImageUploadStart = () => { this.isLoading = true; }; @@ -189,6 +195,7 @@ class DocumentScene extends React.Component { if (!document) return; if (document.text.trim() === text.trim()) return; document.updateData({ text }, true); + this.autosave(); }; onDiscard = () => { diff --git a/server/api/documents.js b/server/api/documents.js index e16e46681..f17e7bbb9 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -334,7 +334,7 @@ router.post('documents.create', auth(), async ctx => { }); router.post('documents.update', auth(), async ctx => { - const { id, title, text, publish, done, lastRevision } = ctx.body; + const { id, title, text, publish, autosave, done, lastRevision } = ctx.body; ctx.assertPresent(id, 'id is required'); ctx.assertPresent(title || text, 'title or text is required'); @@ -355,7 +355,7 @@ router.post('documents.update', auth(), async ctx => { if (publish) { await document.publish(); } else { - await document.save(); + await document.save({ autosave }); if (document.publishedAt && done) { events.add({ name: 'documents.update', model: document }); diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 8c8eb45d2..20280ffbe 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -1,7 +1,7 @@ /* eslint-disable flowtype/require-valid-file-annotation */ import TestServer from 'fetch-test-server'; import app from '..'; -import { Document, View, Star } from '../models'; +import { Document, View, Star, Revision } from '../models'; import { flushdb, seed } from '../test/support'; import { buildUser } from '../test/factories'; @@ -484,6 +484,31 @@ describe('#documents.update', async () => { expect(body.data.collection.documents[0].title).toBe('Updated title'); }); + it('should not create new version when autosave=true', async () => { + const { user, document } = await seed(); + + const res = await server.post('/api/documents.update', { + body: { + token: user.getJwtToken(), + id: document.id, + title: 'Updated title', + text: 'Updated text', + lastRevision: document.revision, + autosave: true, + }, + }); + + const prevRevisionRecords = await Revision.count(); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.title).toBe('Updated title'); + expect(body.data.text).toBe('Updated text'); + + const revisionRecords = await Revision.count(); + expect(revisionRecords).toBe(prevRevisionRecords); + }); + it('should fallback to a default title', async () => { const { user, document } = await seed(); diff --git a/server/models/Document.js b/server/models/Document.js index e47510f87..7bcb61d98 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -25,8 +25,9 @@ const slugify = text => remove: /[.]/g, }); -const createRevision = doc => { - // Create revision of the current (latest) +const createRevision = (doc, options = {}) => { + if (options.autosave) return; + return Revision.create({ title: doc.title, text: doc.text, @@ -204,15 +205,14 @@ Document.searchForUser = async ( LIMIT :limit OFFSET :offset; `; - const results = await sequelize - .query(sql, { - replacements: { - query, - limit, - offset, - }, - model: Document, - }) + const results = await sequelize.query(sql, { + replacements: { + query, + limit, + offset, + }, + model: Document, + }); const ids = results.map(document => document.id); // Second query to get views for the data