diff --git a/packages/app/obojobo-document-engine/__tests__/oboeditor/components/visual-editor.test.js b/packages/app/obojobo-document-engine/__tests__/oboeditor/components/visual-editor.test.js
index 7bd38f546..eb93861fb 100644
--- a/packages/app/obojobo-document-engine/__tests__/oboeditor/components/visual-editor.test.js
+++ b/packages/app/obojobo-document-engine/__tests__/oboeditor/components/visual-editor.test.js
@@ -433,10 +433,14 @@ describe('VisualEditor', () => {
"addObjective": [Function],
"contentRect": null,
"editable": false,
+ "elapsed": 0,
+ "intervalId": 108,
+ "lastSaved": null,
"objectives": Array [],
"removeObjective": [Function],
"saveState": "saveSuccessful",
"showPlaceholders": true,
+ "unsavedChanges": false,
"updateObjective": [Function],
"value": Array [
Object {
@@ -458,10 +462,14 @@ describe('VisualEditor', () => {
"addObjective": [Function],
"contentRect": null,
"editable": true,
+ "elapsed": 0,
+ "intervalId": 114,
+ "lastSaved": null,
"objectives": Array [],
"removeObjective": [Function],
"saveState": "",
"showPlaceholders": true,
+ "unsavedChanges": false,
"updateObjective": [Function],
"value": Array [
Object {
@@ -681,6 +689,20 @@ describe('VisualEditor', () => {
expect(plugins).toMatchSnapshot()
})
+ test('draft saves to local storage every 10 seconds', () => {
+ jest.useFakeTimers();
+
+ const saveModuleToLocalStorage = jest.spyOn(VisualEditor.prototype, 'saveModuleToLocalStorage');
+
+ expect(setInterval).toHaveBeenCalledTimes(1);
+ expect(setInterval).toHaveBeenLastCalledWith(expect(saveModuleToLocalStorage), 10000);
+ })
+
+ test('lastSaved displays in toolbar after save', () => {
+ const component = mount()
+
+ })
+
test('exportToJSON returns expected json for assessment node', () => {
const spy = jest.spyOn(Common.Registry, 'getItemForType')
spy.mockReturnValueOnce({
diff --git a/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/code-editor.js b/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/code-editor.js
index 5b743ac27..ba6f0910e 100644
--- a/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/code-editor.js
+++ b/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/code-editor.js
@@ -99,6 +99,7 @@ class CodeEditor extends React.Component {
location.reload()
}
+ //TARGET
saveAndGetTitleFromCode() {
// Update the title in the File Toolbar
const title = EditorUtil.getTitleFromString(this.state.code, this.props.mode)
diff --git a/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/editor-app.js b/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/editor-app.js
index 21892d8cd..006ac2379 100644
--- a/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/editor-app.js
+++ b/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/editor-app.js
@@ -12,6 +12,7 @@ import React from 'react'
import enableWindowCloseDispatcher from '../../common/util/close-window-dispatcher'
import ObojoboIdleTimer from '../../common/components/obojobo-idle-timer'
import SimpleDialog from '../../common/components/modal/simple-dialog'
+import { isEqual } from 'underscore'
const ModalContainer = Common.components.ModalContainer
const ModalUtil = Common.util.ModalUtil
@@ -28,6 +29,7 @@ class EditorApp extends React.Component {
// store the current version id for locking
// not stored on state because no effect on render
this.contentId = null
+ const unsavedChanges = localStorage.getItem("localStorageJSON")
this.state = {
model: null,
@@ -40,7 +42,9 @@ class EditorApp extends React.Component {
mode: VISUAL_MODE,
code: null,
requestStatus: null,
- requestError: null
+ requestError: null,
+ unsavedChanges: unsavedChanges ? true : null,
+ overwrite: false
}
// caluclate edit lock settings
@@ -85,17 +89,49 @@ class EditorApp extends React.Component {
this.onWindowReturnFromInactive = this.onWindowReturnFromInactive.bind(this)
this.onWindowInactive = this.onWindowInactive.bind(this)
this.renewLockInterval = null
+ this.saveToLocalStorage = this.saveToLocalStorage.bind(this)
+ this.overwriteChanges = this.overwriteChanges.bind(this)
+ this.cancelOverwrite = this.cancelOverwrite.bind(this)
+ }
+
+ // all the stuff you need to do to save the current json string to localStorage
+ saveToLocalStorage(currentDraftJSON) {
+ // write the current draft JSON into local storage
+ // compare json here
+ if(!isEqual(currentDraftJSON, this.state.draft)) {
+ localStorage.setItem('localStorageJSON', JSON.stringify(currentDraftJSON))
+ this.setState({ unsavedChanges: true })
+ this.setState({ firstLoad: false })
+ } else {
+ localStorage.removeItem('localStorageJSON')
+ }
+
}
saveDraft(draftId, draftSrc, xmlOrJSON = 'json') {
const mode = xmlOrJSON === 'xml' ? 'text/plain' : 'application/json'
+ // remove local storage
+ if(localStorage.getItem("localStorageJSON")) {
+ localStorage.removeItem("localStorageJSON");
+ }
return EditorAPI.postDraft(draftId, draftSrc, mode)
.then(({ contentId, result }) => {
if (result.status !== 'ok') {
throw Error(result.value.message)
}
+
+ contentId = this.contentId
+
+ if(xmlOrJSON !== 'xml') {
+ this.setState({
+ draft: {
+ ...JSON.parse(draftSrc),
+ contentId
+ }
+ })
+
+ }
- this.contentId = contentId // keep new contentId for edit locks
return true
})
.catch(e => {
@@ -104,11 +140,61 @@ class EditorApp extends React.Component {
})
}
+ overwriteChanges() {
+ try {
+ const localStorageJSON = localStorage.getItem('localStorageJSON')
+ const parsedLocalStorageJSON = JSON.parse(localStorageJSON)
+
+ this.setState({
+ unsavedChanges: false,
+ }, () => {
+ localStorage.removeItem('localStorageJSON');
+ this.saveDraft(parsedLocalStorageJSON.draftId, localStorageJSON, 'json').then(() => {
+ this.reloadDraft(this.state.draftId, this.state.mode);
+ }).then(() => {
+ window.alert('Page needs to refresh in order7 for overwritten changes to show'); //eslint-disable-line no-alert
+ window.removeEventListener('beforeunload', this.checkIfSaved)
+ window.location.reload();
+ })
+ })
+
+ }catch(e) {
+ throw new Error(e)
+ }
+ }
+
+ cancelOverwrite() {
+ localStorage.removeItem('localStorageJSON');
+ ModalUtil.hide();
+ }
+
getVisualEditorState(draftId, draftModel) {
OboModel.clearAll()
const json = JSON.parse(draftModel)
const obomodel = OboModel.create(json)
+
+ // compare and show modal here
+ // define functions instead of using anonymous/arrow functions
+ const localStorageJSON = localStorage.getItem('localStorageJSON')
+ if (this.state.unsavedChanges) {
+ this.setState({ overwrite: true })
+ if(!isEqual(localStorageJSON, json) ) {
+ ModalUtil.show(
+
+ It looks like you did not save changes before closing the window.
+
+ Would you like to restore your unsaved changes?
+
+ )
+ }
+ }
+
EditorStore.init(
obomodel,
json.content.start,
@@ -209,6 +295,7 @@ class EditorApp extends React.Component {
return EditorAPI.getFullDraft(draftId, mode === VISUAL_MODE ? 'json' : mode)
.then(({ contentId, body }) => {
this.contentId = contentId
+
switch (mode) {
case XML_MODE:
return body
@@ -223,7 +310,7 @@ class EditorApp extends React.Component {
error.type = json.value.type
throw error
}
- // stringify and format the draft data
+
return JSON.stringify(json.value, null, 4)
}
}
@@ -355,6 +442,7 @@ class EditorApp extends React.Component {
switchMode={this.switchMode}
insertableItems={Common.Registry.insertableItems}
saveDraft={this.saveDraft}
+ saveToLocalStorage={this.saveToLocalStorage}
/>
)
}
@@ -371,6 +459,8 @@ class EditorApp extends React.Component {
switchMode={this.switchMode}
insertableItems={Common.Registry.insertableItems}
saveDraft={this.saveDraft}
+ saveToLocalStorage={this.saveToLocalStorage}
+ unsavedChanges={this.unsavedChanges}
readOnly={
// Prevents editing a draft that's a revision,
// even if the url was visited manually
diff --git a/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/visual-editor.js b/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/visual-editor.js
index d5daaca4e..99af5c547 100644
--- a/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/visual-editor.js
+++ b/packages/app/obojobo-document-engine/src/scripts/oboeditor/components/visual-editor.js
@@ -73,13 +73,17 @@ class VisualEditor extends React.Component {
this.state = {
value: json,
saveState: 'saveSuccessful',
+ lastSaved: null,
+ elapsed: 0,
+ unsavedChanges: false,
editable: json && json.length >= 1 && !json[0].text,
showPlaceholders: true,
contentRect: null,
objectives: this.props.model?.objectives ?? [],
addObjective: this.addObjective,
removeObjective: this.removeObjective,
- updateObjective: this.updateObjective
+ updateObjective: this.updateObjective,
+ intervalId: null,
}
this.pageEditorContainerRef = React.createRef()
@@ -102,6 +106,8 @@ class VisualEditor extends React.Component {
this.setEditorFocus = this.setEditorFocus.bind(this)
this.onClick = this.onClick.bind(this)
this.hasInvalidFields = this.hasInvalidFields.bind(this)
+ this.formatJSON = this.formatJSON.bind(this)
+ this.saveModuleToLocalStorage = this.saveModuleToLocalStorage.bind(this)
this.editor = this.withPlugins(withHistory(withReact(createEditor())))
this.editor.toggleEditable = this.toggleEditable
@@ -181,6 +187,12 @@ class VisualEditor extends React.Component {
return Array.from(items.values())
}
+ updateElapsed() {
+ if (this.state.lastSaved === null) return {...this.state.lastSaved}
+ const duration = Math.floor((Date.now() - this.state.lastSaved) / (60 * 1000))
+ this.setState({...this.state, elapsed: duration})
+ }
+
componentDidMount() {
Dispatcher.on('modal:show', () => {
this.toggleEditable(false)
@@ -193,6 +205,13 @@ class VisualEditor extends React.Component {
// Setup global keydown to listen to all global keys
window.addEventListener('keydown', this.onKeyDownGlobal)
+ const intervalId = setInterval(() => {
+ this.saveModuleToLocalStorage()
+ this.updateElapsed()
+ },
+ 10000)
+ this.setState({ ...this.state, intervalId })
+
// Set keyboard focus to the editor
Transforms.select(this.editor, Editor.start(this.editor, []))
this.setEditorFocus()
@@ -219,6 +238,7 @@ class VisualEditor extends React.Component {
window.removeEventListener('beforeunload', this.checkIfSaved)
window.removeEventListener('keydown', this.onKeyDownGlobal)
if (this.resizeObserver) this.resizeObserver.disconnect()
+ clearInterval(this.state.intervalId)
}
checkIfSaved(event) {
@@ -232,11 +252,13 @@ class VisualEditor extends React.Component {
return undefined
}
- if (this.state.saveState !== 'saveSuccessful') {
+ // have to change value before save state is unsuccessful or do it in the already existing if
+ if (this.state.saveState !== 'saveSuccessful') {
event.returnValue = true
return true // Returning true will cause browser to ask user to confirm leaving page
}
+
//eslint-disable-next-line
return undefined
}
@@ -407,7 +429,7 @@ class VisualEditor extends React.Component {
return false
}
- saveModule(draftId) {
+ formatJSON() {
if (this.props.readOnly) {
return
}
@@ -449,15 +471,20 @@ class VisualEditor extends React.Component {
contentJSON.content = child.get('content')
break
}
-
json.children.push(contentJSON)
})
+
+ return json
+ }
+
+ saveModule(draftId) {
+ const json = this.formatJSON()
this.setState({ saveState: 'saving' })
return this.props.saveDraft(draftId, JSON.stringify(json)).then(isSaved => {
if (isSaved) {
if (this.state.saveState === 'saving') {
- this.setState({ saveState: 'saveSuccessful' })
+ this.setState({...this.state, saveState: 'saveSuccessful', lastSaved: Date.now()})
}
} else {
this.setState({ saveState: 'saveFailed' })
@@ -465,6 +492,11 @@ class VisualEditor extends React.Component {
})
}
+ saveModuleToLocalStorage() {
+ const json = this.formatJSON()
+ this.props.saveToLocalStorage(json)
+ }
+
exportToJSON(page, value) {
if (page === null) return
@@ -498,13 +530,13 @@ class VisualEditor extends React.Component {
this.exportToJSON(this.props.page, this.state.value)
}
- importFromJSON() {
+ importFromJSON(existingJSON = null) {
if (!this.props.page) {
// if page is empty, exit
return [{ text: 'No content available, create a page to start editing' }]
}
- const json = this.props.page.toJSON()
+ const json = existingJSON ?? this.props.page.toJSON()
if (json.type === ASSESSMENT_NODE) {
return [this.assessment.oboToSlate(this.props.page)]
@@ -634,12 +666,14 @@ class VisualEditor extends React.Component {
}
}
+
render() {
const className =
'editor--page-editor ' +
isOrNot(this.state.showPlaceholders, 'show-placeholders') +
isOrNot(this.props.readOnly, 'read-only')
-
+
+ //check document engine for confirmation dialog/modal
return (
@@ -650,6 +684,7 @@ class VisualEditor extends React.Component {
+ {this.state.lastSaved ? {this.state.elapsed < 1 ? 'Last saved < 1m ago' : `Last saved ${this.state.elapsed}m ago`} : <>>}