Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

courses add: unsaved changes dialog (fixes #8135) #8143

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,8 @@ npm i mime@3.0.0
npm i @types/mime@3.0.0
```

### Error on initial npm install

If your npm install fails on your first try, first check if you are using Node v14. Other versions of Node may throw errors when installing dependencies.

This project is tested with [BrowserStack](https://www.browserstack.com/).
98 changes: 92 additions & 6 deletions src/app/courses/add-courses/courses-add.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { Subject, forkJoin, of, combineLatest, race, interval } from 'rxjs';
import { takeWhile, debounce, catchError, switchMap } from 'rxjs/operators';
import { Router, ActivatedRoute, NavigationStart } from '@angular/router';
import { Subject, forkJoin, of, combineLatest, race, interval, Subscription } from 'rxjs';
import { takeWhile, debounce, catchError, switchMap, takeUntil } from 'rxjs/operators';

import { CouchService } from '../../shared/couchdb.service';
import { CustomValidators } from '../../validators/custom-validators';
Expand All @@ -17,12 +17,14 @@ import { PlanetStepListService } from '../../shared/forms/planet-step-list.compo
import { PouchService } from '../../shared/database/pouch.service';
import { TagsService } from '../../shared/forms/tags.service';
import { showFormErrors } from '../../shared/table-helpers';
import { UnsavedChangesService } from '../../shared/unsaved-changes.service';
import { CanComponentDeactivate } from '../../shared/unsaved-changes.guard';

@Component({
templateUrl: 'courses-add.component.html',
styleUrls: [ './courses-add.scss' ]
})
export class CoursesAddComponent implements OnInit, OnDestroy {
export class CoursesAddComponent implements OnInit, OnDestroy, CanComponentDeactivate {

readonly dbName = 'courses'; // make database name a constant
courseForm: FormGroup;
Expand All @@ -35,6 +37,12 @@ export class CoursesAddComponent implements OnInit, OnDestroy {
private isSaved = false;
private stepsChange$ = new Subject<any[]>();
private _steps = [];
hasUnsavedChanges = false;
private isFormInitialized = false;
private subscriptions: Subscription = new Subscription();
private isNavigating = false;
private initialFormValues: any;
private initialStepCount: number;
get steps() {
return this._steps;
}
Expand All @@ -46,6 +54,7 @@ export class CoursesAddComponent implements OnInit, OnDestroy {
}));
this.coursesService.course = { form: this.courseForm.value, steps: this._steps };
this.stepsChange$.next(value);
this.checkPristineState();
}

// from the constants import
Expand All @@ -70,10 +79,22 @@ export class CoursesAddComponent implements OnInit, OnDestroy {
private stateService: StateService,
private planetStepListService: PlanetStepListService,
private pouchService: PouchService,
private tagsService: TagsService
private tagsService: TagsService,
private unsavedChangesService: UnsavedChangesService
) {
this.createForm();
this.onFormChanges();
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
this.isNavigating = true;
}
});
this.planetStepListService.stepMoveClick$.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
this.checkPristineState();
});
this.planetStepListService.stepAdded$.pipe(takeUntil(this.onDestroy$)).subscribe(() => {
this.checkPristineState();
});
}

createForm() {
Expand Down Expand Up @@ -125,6 +146,10 @@ export class CoursesAddComponent implements OnInit, OnDestroy {
this.coursesService.returnUrl = this.router.serializeUrl(returnRoute);
this.coursesService.course = { form: this.courseForm.value, steps: this.steps };
this.coursesService.stepIndex = undefined;
this.setupFormValueChanges();
this.isFormInitialized = true;
this.initialFormValues = { ...this.courseForm.value };
this.initialStepCount = this.steps.length;
}

ngOnDestroy() {
Expand All @@ -134,6 +159,7 @@ export class CoursesAddComponent implements OnInit, OnDestroy {
this.isDestroyed = true;
this.onDestroy$.next();
this.onDestroy$.complete();
this.subscriptions.unsubscribe();
}

submitAddedExam() {
Expand All @@ -156,6 +182,8 @@ export class CoursesAddComponent implements OnInit, OnDestroy {
this.images = course.form.images || [];
this.steps = course.steps || [];
this.tags.setValue(course.tags || (course.initialTags || []).map((tag: any) => tag._id));
this.initialFormValues = { ...this.courseForm.value };
this.initialStepCount = this.steps.length;
}

setInitialTags(tags, documentInfo, draft?) {
Expand Down Expand Up @@ -212,6 +240,8 @@ export class CoursesAddComponent implements OnInit, OnDestroy {
showFormErrors(this.courseForm.controls);
return;
}
this.hasUnsavedChanges = false;
this.unsavedChangesService.setHasUnsavedChanges(false);
this.updateCourse(this.courseForm.value, shouldNavigate);
}

Expand Down Expand Up @@ -244,6 +274,7 @@ export class CoursesAddComponent implements OnInit, OnDestroy {
images: []
});
this.planetStepListService.addStep(this.steps.length - 1);
this.checkPristineState();
}

cancel() {
Expand All @@ -264,6 +295,7 @@ export class CoursesAddComponent implements OnInit, OnDestroy {

removeStep(pos) {
this.steps.splice(pos, 1);
this.checkPristineState();
}

stepTrackByFn(index, item) {
Expand All @@ -274,4 +306,58 @@ export class CoursesAddComponent implements OnInit, OnDestroy {
return { ...this.coursesService.storeMarkdownImages({ ...course, steps }) };
}

setupFormValueChanges() {
this.courseForm.valueChanges.subscribe(() => {
if (this.isFormInitialized) {
this.checkPristineState();
}
});
}

checkPristineState() {
const currentFormValue = { ...this.courseForm.value, description: this.courseForm.value.description.text || this.courseForm.value.description };
const initialFormValue = { ...this.initialFormValues, description: this.initialFormValues.description.text || this.initialFormValues.description };
const currentStepCount = this.steps.length;
const stepCountChanged = currentStepCount !== this.initialStepCount;
const formChanged = JSON.stringify(currentFormValue) !== JSON.stringify(initialFormValue);
const isPristine = !stepCountChanged && !formChanged;
console.log('isPristine:', isPristine);
if (!isPristine) {
this.hasUnsavedChanges = true;
this.unsavedChangesService.setHasUnsavedChanges(true);
} else {
this.hasUnsavedChanges = false;
this.unsavedChangesService.setHasUnsavedChanges(false);
}
}

canDeactivate(): boolean {
console.log('canDeactivate called');
if (this.hasUnsavedChanges) {
console.log('Unsaved changes detected in canDeactivate');
return window.confirm('You have unsaved changes. Are you sure you want to leave?');
}
return true;
}

@HostListener('window:beforeunload', [ '$event' ])
unloadNotification($event: BeforeUnloadEvent): void {
console.log('unloadNotification called');
if (this.hasUnsavedChanges) {
console.log('Unsaved changes detected in unloadNotification');
$event.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
}
}

addExam(type = 'exam') {
this.coursesService.stepIndex = this.steps.length - 1; // Use the last step index
this.hasUnsavedChanges = false; // Bypass the guard
this.unsavedChangesService.setHasUnsavedChanges(false); // Bypass the guard
const step = this.steps[this.coursesService.stepIndex];
if (step[type]) {
this.router.navigate([ '/courses/update/exam/', step[type]._id, { type } ], { skipLocationChange: true });
} else {
this.router.navigate([ '/courses/exam/', { type } ], { skipLocationChange: true });
}
}
}
14 changes: 11 additions & 3 deletions src/app/courses/add-courses/courses-step.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { takeUntil } from 'rxjs/operators';
import { CoursesService } from '../courses.service';
import { DialogsAddResourcesComponent } from '../../shared/dialogs/dialogs-add-resources.component';
import { DialogsLoadingService } from '../../shared/dialogs/dialogs-loading.service';
import { UnsavedChangesService } from '../../shared/unsaved-changes.service';

@Component({
selector: 'planet-courses-step',
Expand All @@ -31,7 +32,8 @@ export class CoursesStepComponent implements OnDestroy {
private fb: FormBuilder,
private dialog: MatDialog,
private coursesService: CoursesService,
private dialogsLoadingService: DialogsLoadingService
private dialogsLoadingService: DialogsLoadingService,
private unsavedChangesService: UnsavedChangesService
) {
this.stepForm = this.fb.group({
id: '',
Expand All @@ -41,6 +43,7 @@ export class CoursesStepComponent implements OnDestroy {
this.stepForm.valueChanges.pipe(takeUntil(this.onDestroy$)).subscribe(value => {
this.steps[this.activeStepIndex] = { ...this.activeStep, ...value };
this.stepsChange.emit(this.steps);
this.unsavedChangesService.setHasUnsavedChanges(true);
});
}

Expand Down Expand Up @@ -76,28 +79,33 @@ export class CoursesStepComponent implements OnDestroy {
this.stepsChange.emit(this.steps);
this.dialogsLoadingService.stop();
this.dialogRef.close();
this.unsavedChangesService.setHasUnsavedChanges(true);
}

removeResource(position: number) {
this.steps[this.activeStepIndex].resources.splice(position, 1);
this.unsavedChangesService.setHasUnsavedChanges(true);
}

addExam(type = 'exam') {
this.coursesService.stepIndex = this.activeStepIndex;
this.unsavedChangesService.setHasUnsavedChanges(false); // Bypass the guard
if (this.activeStep[type]) {
this.router.navigate([ '/courses/update/exam/', this.activeStep[type]._id, { type } ]);
this.router.navigate([ '/courses/update/exam/', this.activeStep[type]._id, { type } ], { skipLocationChange: true });
} else {
this.router.navigate([ '/courses/exam/', { type } ]);
this.router.navigate([ '/courses/exam/', { type } ], { skipLocationChange: true });
}
}

stepsMoved(steps) {
this.steps = steps;
this.stepsChange.emit(this.steps);
this.unsavedChangesService.setHasUnsavedChanges(true);
}

addStep() {
this.addStepEvent.emit();
this.unsavedChangesService.setHasUnsavedChanges(true);
}

}
9 changes: 8 additions & 1 deletion src/app/exams/exams-add.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { markdownToPlainText } from '../shared/utils';
import { SubmissionsService } from './../submissions/submissions.service';
import { findDocuments } from '../shared/mangoQueries';
import { forkJoin, of } from 'rxjs';
import { UnsavedChangesService } from '../shared/unsaved-changes.service';

const showdown = require('showdown');

Expand All @@ -45,6 +46,7 @@ export class ExamsAddComponent implements OnInit {
isCourseContent = this.router.url.match(/courses/);
returnUrl = this.coursesService.returnUrl || 'courses';
activeQuestionIndex = -1;
hasUnsavedChanges = false; // Define the property
private _question: FormGroup;
get question(): FormGroup {
return this._question;
Expand Down Expand Up @@ -72,7 +74,8 @@ export class ExamsAddComponent implements OnInit {
private userService: UserService,
private dialog: MatDialog,
private stateService: StateService,
private submissionsService: SubmissionsService
private submissionsService: SubmissionsService,
private unsavedChangesService: UnsavedChangesService // Add the service to the constructor
) {
this.createForm();
}
Expand Down Expand Up @@ -125,6 +128,10 @@ export class ExamsAddComponent implements OnInit {
this.examForm.value.teamId = this.teamId;
}
this.showFormError = false;
if (reRoute) {
this.hasUnsavedChanges = false; // Bypass the guard
this.unsavedChangesService.setHasUnsavedChanges(false); // Bypass the guard
}
this.addExam(Object.assign({}, this.examForm.value, this.documentInfo), reRoute);
} else {
this.showErrorMessage();
Expand Down
5 changes: 5 additions & 0 deletions src/app/shared/forms/planet-step-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { FormArray } from '@angular/forms';
import { uniqueId } from '../utils';
import { UnsavedChangesService } from '../unsaved-changes.service';

@Injectable({
providedIn: 'root'
Expand All @@ -26,12 +27,16 @@ export class PlanetStepListService {
stepMoveClick$ = new Subject<any>();
stepAdded$ = new Subject<number>();

constructor(private unsavedChangesService: UnsavedChangesService) {}

moveStep(index, direction, listId) {
this.stepMoveClick$.next({ index, direction, listId });
this.unsavedChangesService.setHasUnsavedChanges(true);
}

addStep(index: number) {
this.stepAdded$.next(index);
this.unsavedChangesService.setHasUnsavedChanges(true);
}

}
Expand Down
Loading