From f5512c9e0de3574863b07fbb0d4024af62de64c4 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 19 Jun 2023 15:16:50 +0200 Subject: [PATCH 1/3] When an entry is created, wait for it, before successive saves --- .../lexicon/editor/editor.component.ts | 73 ++++++++++--------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/src/angular-app/languageforge/lexicon/editor/editor.component.ts b/src/angular-app/languageforge/lexicon/editor/editor.component.ts index 4d1cea6fb7..c44aaabc79 100644 --- a/src/angular-app/languageforge/lexicon/editor/editor.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/editor.component.ts @@ -32,7 +32,8 @@ import { import { LexiconProject } from '../shared/model/lexicon-project.model'; import { LexOptionList } from '../shared/model/option-list.model'; import { FieldControl } from './field/field-control.model'; -import {OfflineCacheUtilsService} from '../../../bellows/core/offline/offline-cache-utils.service'; +import { OfflineCacheUtilsService } from '../../../bellows/core/offline/offline-cache-utils.service'; +import { IPromise } from 'angular'; class Show { more: () => void; @@ -77,6 +78,7 @@ export class LexiconEditorController implements angular.IController { private pristineEntry: LexEntry = new LexEntry(); private warnOfUnsavedEditsId: string; + private saving$: IPromise; static $inject = ['$filter', '$interval', '$q', '$scope', @@ -156,7 +158,7 @@ export class LexiconEditorController implements angular.IController { this.saveCurrentEntry(); } // destroy listeners when leaving editor page - angular.element(window).unbind('keyup', (e: Event) => {}); + angular.element(window).unbind('keyup', (e: Event) => { }); }; this.show.entryListModifiers = !(this.$window.localStorage.getItem('viewFilter') == null || @@ -223,7 +225,7 @@ export class LexiconEditorController implements angular.IController { $onDestroy(): void { this.cancelAutoSaveTimer(); this.saveCurrentEntry(); - angular.element(window).unbind('keydown', (e: Event) => {}); + angular.element(window).unbind('keydown', (e: Event) => { }); } navigateToLiftImport(): void { @@ -353,11 +355,16 @@ export class LexiconEditorController implements angular.IController { return diffs && diffs.length && diffs.some((diff) => diff.kind === 'A'); } - saveCurrentEntry = (doSetEntry: boolean = false, successCallback: () => void = () => { }, + saveCurrentEntry = async (doSetEntry: boolean = false, successCallback: () => void = () => { }, failCallback: (reason?: any) => void = () => { }) => { + const isNewEntry = LexiconEditorController.entryIsNew(this.currentEntry); + if (isNewEntry) { + // We have to wait for the initial save to complete so that we have + await this.saving$; + } + // `doSetEntry` is mainly used for when the save button is pressed, that is when the user is saving the current // entry and is NOT going to a different entry (as is the case with editing another entry. - let isNewEntry = false; let newEntryTempId: string; if (this.hasUnsavedChanges() && this.lecRights.canEditEntry()) { @@ -367,8 +374,7 @@ export class LexiconEditorController implements angular.IController { this.currentEntry = LexiconEditorController.normalizeStrings(this.currentEntry); this.control.currentEntry = this.currentEntry; const entryToSave = angular.copy(this.currentEntry); - if (LexiconEditorController.entryIsNew(entryToSave)) { - isNewEntry = true; + if (isNewEntry) { newEntryTempId = entryToSave.id; entryToSave.id = ''; // send empty id to indicate "create new" } @@ -379,18 +385,20 @@ export class LexiconEditorController implements angular.IController { id: entryForUpdate.id, _update_deep_diff: diff(LexiconEditorController.normalizeStrings(pristineEntryForDiffing), entryForDiffing) }; + let entryOrDiff = isNewEntry ? entryForUpdate : diffForUpdate; if (!isNewEntry && this.hasArrayChange(diffForUpdate._update_deep_diff)) { // Updates involving adding or deleting any array item cannot be delta updates due to MongoDB limitations entryOrDiff = entryForUpdate; } - return this.$q.all({ - entry: this.lexService.update(entryOrDiff), - isSR: this.sendReceive.isSendReceiveProject() - }).then(data => { - const entry = data.entry.data; - if (!entry && data.isSR) { + try { + const { result: { data: entry }, isSR } = await this.$q.all({ + result: this.lexService.update(entryOrDiff), + isSR: this.sendReceive.isSendReceiveProject() + }); + + if (!entry && isSR) { this.warnOfUnsavedEdits(entryToSave); this.sendReceive.startSyncStatusTimer(); } @@ -429,28 +437,27 @@ export class LexiconEditorController implements angular.IController { } // refresh data will add the new entry to the entries list - this.editorService.refreshEditorData().then(() => { - this.activityService.markRefreshRequired(); - if (entry && isNewEntry) { - this.setCurrentEntry(this.entries[this.editorService.getIndexInList(entry.id, this.entries)]); - this.editorService.removeEntryFromLists(newEntryTempId); - - if (doSetEntry) { - this.$state.go('.', { - entryId: entry.id, - }, { notify: false }); - - this.scrollListToEntry(entry.id, 'top'); - } + await this.editorService.refreshEditorData(); + this.activityService.markRefreshRequired(); + if (entry && isNewEntry) { + this.setCurrentEntry(this.entries[this.editorService.getIndexInList(entry.id, this.entries)]); + this.editorService.removeEntryFromLists(newEntryTempId); + + if (doSetEntry) { + this.$state.go('.', { + entryId: entry.id, + }, { notify: false }); + + this.scrollListToEntry(entry.id, 'top'); } - }); - this.saveStatus = 'saved'; - successCallback(); - }).catch(reason => { + this.saveStatus = 'saved'; + successCallback(); + } + } catch (reason) { this.saveStatus = 'unsaved'; failCallback(reason); - }); + } } else { successCallback(); } @@ -982,7 +989,7 @@ export class LexiconEditorController implements angular.IController { // see if there is a most-recently viewed entry in the cache await this.offlineCacheUtils.getProjectMruEntryData().then(data => { - if(data && data.mruEntryId && this.editorService.getIndexInList(data.mruEntryId, this.entries) != null){ + if (data && data.mruEntryId && this.editorService.getIndexInList(data.mruEntryId, this.entries) != null) { entryId = data.mruEntryId; } @@ -1285,7 +1292,7 @@ export class LexiconEditorController implements angular.IController { } private static syncListEntryWithCurrentEntry(elementId: string, alignment: string = 'center'): void { - const element = document.querySelector(elementId); + const element = document.querySelector(elementId); const block = alignment !== 'top' ? 'center' : 'start'; // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView From fb3733bb4245e1523aa753ebb457ca89c0d9917e Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 19 Jun 2023 16:49:39 +0200 Subject: [PATCH 2/3] Make sure we don't diff anything with a deleted entry. --- .../languageforge/lexicon/editor/editor.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/angular-app/languageforge/lexicon/editor/editor.component.ts b/src/angular-app/languageforge/lexicon/editor/editor.component.ts index c44aaabc79..17ab085b61 100644 --- a/src/angular-app/languageforge/lexicon/editor/editor.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/editor.component.ts @@ -535,6 +535,8 @@ export class LexiconEditorController implements angular.IController { if (iShowList !== 0) { iShowList--; } + this.currentEntry = new LexEntry(); + this.pristineEntry = new LexEntry(); this.editEntryAndScroll(this.visibleEntries[iShowList].id); } else { this.returnToList(); From 7987f4d062d476d0da58b0795a43e11a67275605 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 19 Jun 2023 16:50:47 +0200 Subject: [PATCH 3/3] Fix race conditions when switching entries before saved --- .../lexicon/editor/editor.component.ts | 62 +++++++++---------- .../editor/field/field-control.model.ts | 2 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/angular-app/languageforge/lexicon/editor/editor.component.ts b/src/angular-app/languageforge/lexicon/editor/editor.component.ts index 17ab085b61..db46b071ee 100644 --- a/src/angular-app/languageforge/lexicon/editor/editor.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/editor.component.ts @@ -463,14 +463,14 @@ export class LexiconEditorController implements angular.IController { } } - editEntryAndScroll(id: string): void { - this.editEntry(id); + async editEntryAndScroll(id: string): Promise { + await this.editEntry(id); this.scrollListToEntry(id); } - editEntry(id: string): void { + async editEntry(id: string): Promise { if (this.currentEntry.id !== id) { - this.saveCurrentEntry(); + await this.saveCurrentEntry(); this.setCurrentEntry(this.entries[this.editorService.getIndexInList(id, this.entries)]); // noinspection JSIgnoredPromiseFromCall - comments will load in the background this.commentService.loadEntryComments(id); @@ -483,10 +483,10 @@ export class LexiconEditorController implements angular.IController { this.goToEntry(id); } - gotoToEntry(index: number, isValid: boolean) { + async gotoToEntry(index: number, isValid: boolean): Promise { if (isValid) { let id = this.editorService.getIdInFilteredList(Number(index)); - this.editEntryAndScroll(id); + await this.editEntryAndScroll(id); } } @@ -502,9 +502,9 @@ export class LexiconEditorController implements angular.IController { return i >= 0 && i < this.visibleEntries.length; } - skipToEntry(distance: number): void { + async skipToEntry(distance: number): Promise { const i = this.editorService.getIndexInList(this.currentEntry.id, this.visibleEntries) + distance; - this.editEntry(this.visibleEntries[i].id); + await this.editEntry(this.visibleEntries[i].id); this.scrollListToEntry(this.visibleEntries[i].id); } @@ -525,32 +525,32 @@ export class LexiconEditorController implements angular.IController { }); } - deleteEntry = (entry: LexEntry): void => { + deleteEntry = async (entry: LexEntry): Promise => { const deleteMsg = 'Are you sure you want to delete the entry \'' + LexiconUtilityService.getLexeme(this.lecConfig, this.lecConfig.entry, entry) + '\'?'; - this.modal.showModalSimple('Delete Entry', deleteMsg, 'Cancel', 'Delete Entry').then(() => { - let iShowList = this.editorService.getIndexInList(entry.id, this.visibleEntries); - this.editorService.removeEntryFromLists(entry.id); - if (this.entries.length > 0) { - if (iShowList !== 0) { - iShowList--; - } - this.currentEntry = new LexEntry(); - this.pristineEntry = new LexEntry(); - this.editEntryAndScroll(this.visibleEntries[iShowList].id); - } else { - this.returnToList(); - } + await this.modal.showModalSimple('Delete Entry', deleteMsg, 'Cancel', 'Delete Entry'); - if (!LexiconEditorController.entryIsNew(entry)) { - this.sendReceive.setStateUnsynced(); - this.lexService.remove(entry.id, () => { - this.editorService.refreshEditorData(); - }); + let iShowList = this.editorService.getIndexInList(entry.id, this.visibleEntries); + this.editorService.removeEntryFromLists(entry.id); + if (this.entries.length > 0) { + if (iShowList !== 0) { + iShowList--; } + this.currentEntry = new LexEntry(); + this.pristineEntry = new LexEntry(); + await this.editEntryAndScroll(this.visibleEntries[iShowList].id); + } else { + this.returnToList(); + } - this.hideRightPanel(); - }, () => { }); + if (!LexiconEditorController.entryIsNew(entry)) { + this.sendReceive.setStateUnsynced(); + this.lexService.remove(entry.id, () => { + this.editorService.refreshEditorData(); + }); + } + + this.hideRightPanel(); } makeValidModelRecursive = (config: LexConfigField, data: any = {}, stopAtNodes: string | string[] = []): any => { @@ -978,7 +978,7 @@ export class LexiconEditorController implements angular.IController { } } - private evaluateStateFromURL(): void { + private async evaluateStateFromURL(): Promise { this.editorService.loadEditorData().then(async () => { if (this.$state.is("editor.entry")) { @@ -1001,7 +1001,7 @@ export class LexiconEditorController implements angular.IController { } }); } - this.editEntryAndScroll(entryId); + await this.editEntryAndScroll(entryId); } else { // there are no entries, go to the list view this.$state.go('editor.list'); diff --git a/src/angular-app/languageforge/lexicon/editor/field/field-control.model.ts b/src/angular-app/languageforge/lexicon/editor/field/field-control.model.ts index acba579671..5ec8e76437 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/field-control.model.ts +++ b/src/angular-app/languageforge/lexicon/editor/field/field-control.model.ts @@ -14,7 +14,7 @@ export class FieldControl { commentContext: { contextGuid: string }; config: LexiconConfig; currentEntry: LexEntry; - deleteEntry: (currentEntry: LexEntry) => void; + deleteEntry: (currentEntry: LexEntry) => Promise; getContextParts: (contextGuid: string) => any; getNewComment?: () => LexComment; hideRightPanel: () => void;