open-learning-exchange/planet

View on GitHub
src/app/courses/add-courses/courses-add.component.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { Component, OnInit, OnDestroy } 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 { CouchService } from '../../shared/couchdb.service';
import { CustomValidators } from '../../validators/custom-validators';
import { ValidatorService } from '../../validators/validator.service';
import * as constants from '../constants';
import { languages } from '../../shared/languages';
import { PlanetMessageService } from '../../shared/planet-message.service';
import { CoursesService } from '../courses.service';
import { UserService } from '../../shared/user.service';
import { StateService } from '../../shared/state.service';
import { PlanetStepListService } from '../../shared/forms/planet-step-list.component';
import { PouchService } from '../../shared/database/pouch.service';
import { TagsService } from '../../shared/forms/tags.service';
import { showFormErrors } from '../../shared/table-helpers';

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

  readonly dbName = 'courses'; // make database name a constant
  courseForm: FormGroup;
  documentInfo = { '_rev': undefined, '_id': undefined };
  courseId = this.route.snapshot.paramMap.get('id') || undefined;
  pageType = 'Add new';
  tags = this.fb.control([]);
  private onDestroy$ = new Subject<void>();
  private isDestroyed = false;
  private isSaved = false;
  private stepsChange$ = new Subject<any[]>();
  private _steps = [];
  get steps() {
    return this._steps;
  }
  set steps(value: any[]) {
    this._steps = value.map(step => ({
      ...step,
      description: step.description.text || step.description,
      images: [ ...(step.description.images || []), ...(step.images || []) ]
    }));
    this.coursesService.course = { form: this.courseForm.value, steps: this._steps };
    this.stepsChange$.next(value);
  }

  // from the constants import
  gradeLevels = constants.gradeLevels;
  subjectLevels = constants.subjectLevels;
  images: any[] = [];

  // from the languages import
  languageNames = languages.map(list => list.name);

  mockStep = { stepTitle: $localize`Add title`, description: '!!!' };

  constructor(
    private router: Router,
    private route: ActivatedRoute,
    private fb: FormBuilder,
    private couchService: CouchService,
    private validatorService: ValidatorService,
    private planetMessageService: PlanetMessageService,
    private coursesService: CoursesService,
    private userService: UserService,
    private stateService: StateService,
    private planetStepListService: PlanetStepListService,
    private pouchService: PouchService,
    private tagsService: TagsService
  ) {
    this.createForm();
    this.onFormChanges();
  }

  createForm() {
    const configuration = this.stateService.configuration;
    this.courseForm = this.fb.group({
      courseTitle: [
        '',
        CustomValidators.required,
        ac => this.validatorService.isUnique$(
          this.dbName, 'courseTitle', ac, { selectors: { '_id': { '$ne': this.documentInfo._id || '' } } }
        )
      ],
      description: [ '', CustomValidators.requiredMarkdown ],
      languageOfInstruction: '',
      gradeLevel: '',
      subjectLevel: '',
      createdDate: this.couchService.datePlaceholder,
      creator: this.userService.get().name + '@' + configuration.code,
      sourcePlanet: configuration.code,
      resideOn: configuration.code,
      updatedDate: this.couchService.datePlaceholder
    });
  }

  ngOnInit() {
    const continued = this.route.snapshot.params.continue === 'true' && Object.keys(this.coursesService.course).length;
    forkJoin([
      this.pouchService.getDocEditing(this.dbName, this.courseId),
      this.couchService.get('courses/' + this.courseId).pipe(catchError((err) => of(err.error))),
      this.stateService.getCouchState('tags', 'local')
    ]).subscribe(([ draft, saved, tags ]: [ any, any, any[] ]) => {
      if (saved.error !== 'not_found') {
        this.setDocumentInfo(saved);
        this.pageType = 'Update';
      }
      const doc = draft === undefined ? saved : draft;
      this.setInitialTags(tags, this.documentInfo, draft);
      if (!continued) {
        this.setFormAndSteps({ form: doc, steps: doc.steps, tags: doc.tags, initialTags: this.coursesService.course.initialTags });
      }
    });
    if (continued) {
      this.setFormAndSteps(this.coursesService.course);
      this.submitAddedExam();
    }
    const returnRoute = this.router.createUrlTree([ '.', { continue: true } ], { relativeTo: this.route });
    this.coursesService.returnUrl = this.router.serializeUrl(returnRoute);
    this.coursesService.course = { form: this.courseForm.value, steps: this.steps };
    this.coursesService.stepIndex = undefined;
  }

  ngOnDestroy() {
    if (this.coursesService.stepIndex === undefined) {
      this.coursesService.reset();
    }
    this.isDestroyed = true;
    this.onDestroy$.next();
    this.onDestroy$.complete();
  }

  submitAddedExam() {
    setTimeout(() => {
      if (!this.courseForm.pending) {
        this.onSubmit(false);
      } else {
        this.submitAddedExam();
      }
    }, 1000);
  }

  setDocumentInfo(doc) {
    this.documentInfo = { '_id': doc._id, '_rev': doc._rev };
    this.courseForm.controls.courseTitle.updateValueAndValidity();
  }

  setFormAndSteps(course: any) {
    this.courseForm.patchValue(course.form);
    this.images = course.form.images || [];
    this.steps = course.steps || [];
    this.tags.setValue(course.tags || (course.initialTags || []).map((tag: any) => tag._id));
  }

  setInitialTags(tags, documentInfo, draft?) {
    if (this.isDestroyed) {
      return;
    }
    const courseTags = documentInfo._id ? this.tagsService.attachTagsToDocs(this.dbName, [ documentInfo ], tags)[0].tags : [];
    this.coursesService.course = { initialTags: courseTags || [] };
    this.tags.setValue(draft === undefined ? this.coursesService.course.initialTags.map((tag: any) => tag._id) : draft.tags);
  }

  onFormChanges() {
    combineLatest(this.courseForm.valueChanges, this.stepsChange$, this.tags.valueChanges).pipe(
      debounce(() => race(interval(2000), this.onDestroy$)),
      takeWhile(() => this.isDestroyed === false, true)
    ).subscribe(([ value, steps, tags ]) => {
      if (this.isSaved) {
        return;
      }
      const course = this.convertMarkdownImagesText({ ...value, images: this.images }, steps);
      this.coursesService.course = { form: course, steps: course.steps, tags };
      this.pouchService.saveDocEditing(
        { ...course, tags, initialTags: this.coursesService.course.initialTags }, this.dbName, this.courseId
      );
    });
  }

  updateCourse(courseInfo, shouldNavigate) {
    if (courseInfo.createdDate.constructor === Object) {
      courseInfo.createdDate = this.couchService.datePlaceholder;
    }
    const newCourse = { ...this.convertMarkdownImagesText({ ...courseInfo, images: this.images }, this.steps), ...this.documentInfo };
    this.couchService.updateDocument(
      this.dbName, { ...newCourse, updatedDate: this.couchService.datePlaceholder }
    ).pipe(switchMap((res: any) =>
      forkJoin([
        of(res),
        this.couchService.bulkDocs(
          'tags',
          this.tagsService.tagBulkDocs(res.id, this.dbName, this.tags.value, this.coursesService.course.initialTags)
        )
      ])
    )).subscribe(([ courseRes, tagsRes ]) => {
      const message = courseInfo.courseTitle + (this.pageType === 'Update' ? $localize` Updated Successfully` : $localize` Added`);
      this.courseChangeComplete(message, courseRes, shouldNavigate);
    }, (err) => {
      // Connect to an error display component to show user that an error has occurred
      console.log(err);
    });
  }

  onSubmit(shouldNavigate = true) {
    if (!this.courseForm.valid) {
      showFormErrors(this.courseForm.controls);
      return;
    }
    this.updateCourse(this.courseForm.value, shouldNavigate);
  }

  courseChangeComplete(message, response: any, shouldNavigate) {
    this.pouchService.deleteDocEditing(this.dbName, this.courseId);
    this.isSaved = true;
    if (shouldNavigate) {
      this.navigateBack();
      return;
    }
    this.planetMessageService.showMessage(message);
    if (this.isDestroyed) {
      return;
    }
    this.isSaved = false;
    this.courseId = response.id;
    this.setDocumentInfo(response.doc);
    this.stateService.getCouchState('tags', 'local').subscribe((tags) => this.setInitialTags(tags, this.documentInfo));
    this.coursesService.course = { ...this.documentInfo };
    if (this.pageType === 'Add new') {
      this.router.navigate([ '../update/', this.courseId ], { relativeTo: this.route, replaceUrl: true });
    }
  }

  addStep() {
    this.steps.push({
      stepTitle: '',
      description: '',
      resources: [],
      images: []
    });
    this.planetStepListService.addStep(this.steps.length - 1);
  }

  cancel() {
    this.pouchService.deleteDocEditing(this.dbName, this.courseId);
    this.navigateBack();
  }

  navigateBack() {
    const relativeRoute = (urlArray: string[]) => {
      const lastIndex = urlArray.length - 1;
      const endConditions = [ 'update', 'add' ];
      return `../${
        (lastIndex === 1 || endConditions.indexOf(urlArray[lastIndex]) > -1) ? '' : relativeRoute(urlArray.slice(0, lastIndex))
      }`;
    };
    this.router.navigate([ relativeRoute(this.router.url.split('/')) ], { relativeTo: this.route });
  }

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

  stepTrackByFn(index, item) {
    return item.id;
  }

  convertMarkdownImagesText(course, steps) {
    return { ...this.coursesService.storeMarkdownImages({ ...course, steps }) };
  }

}