Skip to content

Commit badcff8

Browse files
authored
[ENG-1641] Draft registration save before exit (#1655)
- Ticket: [ENG-1641] ## Purpose - Let users know they have unsaved changes before they exit the draft registration workflow - Let users know when an autosave is in progress ## Summary of Changes - Change some of the links on the Draft Registration Metadata page to open in a new tab to prevent users from navigating away - Add a new `{{before-unload}}` modifier - Add logic to show popup if the draft has pending changes - One to handle `onBeforeUnload` (closing or refreshing the browser window) - One to handle route transitions (clicking the "Add new"/"My Registrations" buttons or navigating away within the app) - Add same logic for edit-revision workflow - Update rightnav template to show when an autosave is in progress
1 parent 1048430 commit badcff8

File tree

17 files changed

+371
-20
lines changed

17 files changed

+371
-20
lines changed

lib/osf-components/addon/components/contributors/card/editable/template.hbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<OsfLink
2121
data-test-contributor-link={{@contributor.id}}
2222
data-analytics-name='View user'
23+
@target='_blank'
2324
@href={{concat '/' @contributor.users.id '/'}}
2425
>
2526
{{@contributor.users.fullName}}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Modifier from 'ember-modifier';
2+
3+
interface BeforeUnloadModifierArgs {
4+
positional: any;
5+
named: {};
6+
}
7+
8+
export default class BeforeUnloadModifier extends Modifier<BeforeUnloadModifierArgs> {
9+
listener?: any;
10+
didReceiveArguments() {
11+
if (this.listener) {
12+
window.removeEventListener('beforeunload', this.listener);
13+
}
14+
this.listener = this.args.positional[0];
15+
window.addEventListener('beforeunload', this.listener);
16+
}
17+
18+
willRemove() {
19+
window.removeEventListener('beforeunload', this.listener);
20+
}
21+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from 'osf-components/modifiers/before-unload';

lib/registries/addon/drafts/draft/-components/right-nav/template.hbs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,15 @@
7474
</span>
7575
{{/if}}
7676
<span local-class='SaveMessage'>
77-
{{t 'registries.drafts.draft.form.last_saved'}}
78-
<TimeSince
79-
@date={{@draftManager.draftRegistration.datetimeUpdated}}
80-
/>
77+
{{#if @draftManager.autoSaving}}
78+
<LoadingIndicator @inline={{true}} @dark={{true}}/>
79+
{{t 'registries.drafts.draft.auto_saving'}}
80+
{{else}}
81+
{{t 'registries.drafts.draft.form.last_saved'}}
82+
<TimeSince
83+
@date={{@draftManager.draftRegistration.datetimeUpdated}}
84+
/>
85+
{{/if}}
8186
</span>
8287

8388
{{#if @draftManager.currentUserIsAdmin}}

lib/registries/addon/drafts/draft/controller.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import { alias, not } from '@ember/object/computed';
44
import { inject as service } from '@ember/service';
55

66
import Media from 'ember-responsive';
7+
import IntlService from 'ember-intl/services/intl';
78

89
import BrandModel from 'ember-osf-web/models/brand';
910
import ProviderModel from 'ember-osf-web/models/provider';
1011
import Registration from 'ember-osf-web/models/registration';
12+
import { taskFor } from 'ember-concurrency-ts';
1113

1214
export default class RegistriesDraft extends Controller {
1315
@service media!: Media;
16+
@service intl!: IntlService;
1417

1518
@not('media.isDesktop') showMobileView!: boolean;
1619

@@ -24,4 +27,17 @@ export default class RegistriesDraft extends Controller {
2427
onSubmitRedirect(registrationId: Registration) {
2528
this.transitionToRoute('overview.index', registrationId);
2629
}
30+
31+
@action
32+
saveBeforeUnload(event: BeforeUnloadEvent) {
33+
const { draftRegistrationManager } = this.model;
34+
if (draftRegistrationManager.onMetadataInput.isRunning ||
35+
draftRegistrationManager.onPageInput.isRunning ||
36+
draftRegistrationManager.saveWithToast.isRunning ||
37+
draftRegistrationManager.lastSaveFailed) {
38+
event.preventDefault();
39+
event.returnValue = this.intl.t('registries.drafts.draft.save_before_exit');
40+
taskFor(draftRegistrationManager.saveWithToast).perform();
41+
}
42+
}
2743
}

lib/registries/addon/drafts/draft/draft-registration-manager.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default class DraftRegistrationManager {
5454
@alias('draftRegistration.currentUserIsAdmin') currentUserIsAdmin!: boolean;
5555
@alias('provider.reviewsWorkflow') reviewsWorkflow?: string;
5656
@alias('draftRegistration.hasProject') hasProject?: boolean;
57-
@or('onPageInput.isRunning', 'onMetadataInput.isRunning') autoSaving!: boolean;
57+
@alias('draftRegistration.isSaving') autoSaving!: boolean;
5858
@or('initializePageManagers.isRunning', 'initializeMetadataChangeset.isRunning') initializing!: boolean;
5959
@not('registrationResponsesIsValid') hasInvalidResponses!: boolean;
6060
@filterBy('pageManagers', 'isVisited', true) visitedPages!: PageManager[];
@@ -117,8 +117,14 @@ export default class DraftRegistrationManager {
117117
@restartableTask
118118
@waitFor
119119
async onPageInput(currentPageManager: PageManager) {
120-
await timeout(5000); // debounce
120+
await timeout(3000); // debounce
121+
122+
await taskFor(this.saveRegistrationResponses).perform(currentPageManager);
123+
}
121124

125+
@restartableTask
126+
@waitFor
127+
async saveRegistrationResponses(currentPageManager: PageManager) {
122128
if (currentPageManager && currentPageManager.schemaBlockGroups) {
123129
this.updateRegistrationResponses(currentPageManager);
124130

@@ -131,7 +137,6 @@ export default class DraftRegistrationManager {
131137
const errorMessage = this.intl.t('registries.drafts.draft.form.failed_auto_save');
132138
captureException(e, { errorMessage });
133139
this.toast.error(getApiErrorMessage(e), errorMessage);
134-
throw e;
135140
}
136141
}
137142
}
@@ -200,7 +205,6 @@ export default class DraftRegistrationManager {
200205
const errorMessage = this.intl.t('registries.drafts.draft.metadata.failed_auto_save');
201206
captureException(e, { errorMessage });
202207
this.toast.error(getApiErrorMessage(e), errorMessage);
203-
throw e;
204208
}
205209
}
206210

@@ -217,6 +221,21 @@ export default class DraftRegistrationManager {
217221
}
218222
}
219223

224+
@restartableTask
225+
@waitFor
226+
async saveWithToast() {
227+
try {
228+
taskFor(this.onMetadataInput).cancelAll();
229+
taskFor(this.onPageInput).cancelAll();
230+
await taskFor(this.updateDraftRegistrationAndSave).perform();
231+
await taskFor(this.saveAllVisitedPages).perform();
232+
this.toast.success(this.intl.t('registries.drafts.draft.save_success'));
233+
} catch (e) {
234+
const errorTitle = this.intl.t('registries.drafts.draft.save_failed');
235+
this.toast.error(getApiErrorMessage(e), errorTitle);
236+
}
237+
}
238+
220239
@action
221240
onPageChange(currentPage: number) {
222241
if (this.hasVisitedPages) {

lib/registries/addon/drafts/draft/route.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import Store from '@ember-data/store';
22
import { getOwner } from '@ember/application';
3+
import { action } from '@ember/object';
4+
import Transition from '@ember/routing/-private/transition';
35
import Route from '@ember/routing/route';
46
import RouterService from '@ember/routing/router-service';
57
import { inject as service } from '@ember/service';
68
import { waitFor } from '@ember/test-waiters';
79
import { task } from 'ember-concurrency';
810
import { taskFor } from 'ember-concurrency-ts';
11+
import Intl from 'ember-intl/services/intl';
912

1013
import requireAuth from 'ember-osf-web/decorators/require-auth';
1114
import DraftRegistration from 'ember-osf-web/models/draft-registration';
@@ -25,6 +28,7 @@ export interface DraftRouteModel {
2528
export default class DraftRegistrationRoute extends Route {
2629
@service store!: Store;
2730
@service router!: RouterService;
31+
@service intl!: Intl;
2832

2933
@task
3034
@waitFor
@@ -65,6 +69,22 @@ export default class DraftRegistrationRoute extends Route {
6569
};
6670
}
6771

72+
@action
73+
willTransition(transition: Transition) {
74+
const { draftRegistrationManager } = this.controller.model;
75+
const notBeingDeleted = !draftRegistrationManager.deleteDraft.isRunning;
76+
const draftIsDirty = draftRegistrationManager.onMetadataInput.isRunning ||
77+
draftRegistrationManager.onPageInput.isRunning ||
78+
draftRegistrationManager.saveWithToast.isRunning ||
79+
draftRegistrationManager.lastSaveFailed;
80+
if (!transition.to.name.includes(this.routeName) && draftIsDirty && notBeingDeleted) {
81+
if (!window.confirm(this.intl.t('registries.drafts.draft.save_before_exit'))) {
82+
transition.abort();
83+
taskFor(draftRegistrationManager.saveWithToast).perform();
84+
}
85+
}
86+
}
87+
6888
buildRouteInfoMetadata() {
6989
return {
7090
osfMetrics: {

lib/registries/addon/drafts/draft/template.hbs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@
7878
@navMode={{leftNav.leftGutterMode}}
7979
/>
8080
</layout.leftNav>
81-
<layout.main local-class='Main'>
81+
<layout.main
82+
{{before-unload this.saveBeforeUnload}}
83+
local-class='Main'
84+
>
8285
{{outlet}}
8386
</layout.main>
8487
{{#unless this.showMobileView}}

lib/registries/addon/edit-revision/-components/right-nav/template.hbs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,15 @@
7373
</span>
7474
{{/if}}
7575
<span local-class='SaveMessage'>
76-
{{t 'registries.drafts.draft.form.last_saved'}}
77-
<TimeSince
78-
@date={{@revisionManager.revision.dateModified}}
79-
/>
76+
{{#if @revisionManager.autoSaving}}
77+
<LoadingIndicator @inline={{true}} @dark={{true}} />
78+
{{t 'registries.drafts.draft.auto_saving'}}
79+
{{else}}
80+
{{t 'registries.drafts.draft.form.last_saved'}}
81+
<TimeSince
82+
@date={{@revisionManager.revision.dateModified}}
83+
/>
84+
{{/if}}
8085
</span>
8186

8287
{{#if @revisionManager.showDeleteButton}}

lib/registries/addon/edit-revision/controller.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import Controller from '@ember/controller';
2+
import { action } from '@ember/object';
23
import { alias, not } from '@ember/object/computed';
34
import RouterService from '@ember/routing/router-service';
45
import { inject as service } from '@ember/service';
6+
import { taskFor } from 'ember-concurrency-ts';
57
import IntlService from 'ember-intl/services/intl';
68
import RegistrationModel from 'ember-osf-web/models/registration';
79
import SchemaResponseModel from 'ember-osf-web/models/schema-response';
@@ -17,4 +19,17 @@ export default class EditRevisionController extends Controller {
1719

1820
@alias('model.revisionManager.revision') revision?: SchemaResponseModel;
1921
@alias('model.revisionManager.registration') registration?: RegistrationModel;
22+
23+
@action
24+
saveBeforeUnload(event: BeforeUnloadEvent) {
25+
const { revisionManager } = this.model;
26+
if (revisionManager.onJustificationInput.isRunning ||
27+
revisionManager.onPageInput.isRunning ||
28+
revisionManager.saveWithToast.isRunning ||
29+
revisionManager.lastSaveFailed) {
30+
event.preventDefault();
31+
event.returnValue = this.intl.t('registries.drafts.draft.save_before_exit');
32+
taskFor(revisionManager.saveWithToast).perform();
33+
}
34+
}
2035
}

0 commit comments

Comments
 (0)