diff --git a/src/app/core/learning-object-module/learning-object/learning-object.routes.ts b/src/app/core/learning-object-module/learning-object/learning-object.routes.ts index 7b9fce13b2..718deb29b5 100644 --- a/src/app/core/learning-object-module/learning-object/learning-object.routes.ts +++ b/src/app/core/learning-object-module/learning-object/learning-object.routes.ts @@ -77,6 +77,17 @@ export const LEARNING_OBJECT_ROUTES = { return `${environment.apiURL}/learning-objects`; }, + /** + * Request to check if a learning object name is available + * @method GET + * @auth required + * @param name - The name to check + * @returns Promise - true if name is available, false if duplicate exists + */ + CHECK_NAME_AVAILABILITY(name: string) { + return `${environment.apiURL}/learning-objects/name/check/${encodeURIComponent(name)}`; + }, + /** * Request to update the collection of a learning object * @method PATCH diff --git a/src/app/core/learning-object-module/learning-object/learning-object.service.ts b/src/app/core/learning-object-module/learning-object/learning-object.service.ts index 91d0020ae8..85f7e9ed7d 100644 --- a/src/app/core/learning-object-module/learning-object/learning-object.service.ts +++ b/src/app/core/learning-object-module/learning-object/learning-object.service.ts @@ -482,6 +482,27 @@ export class LearningObjectService { .toPromise(); } + /** + * Checks if a learning object name is available (not a duplicate) + * + * @param {string} name - The name to check + * @returns {Promise} - true if name is available, false if duplicate exists + * @memberof LearningObjectService + */ + async checkNameAvailability(name: string): Promise { + const route = LEARNING_OBJECT_ROUTES.CHECK_NAME_AVAILABILITY(name); + return this.http + .get<{ validName: boolean }>(route, { + headers: this.headers, + withCredentials: true + }) + .pipe( + catchError(this.handleError) + ) + .toPromise() + .then(response => response.validName); + } + /** * Method to delete multiple learning objects * diff --git a/src/app/onion/learning-object-builder/builder-store.service.ts b/src/app/onion/learning-object-builder/builder-store.service.ts index 4f1120c876..b2fdd33146 100644 --- a/src/app/onion/learning-object-builder/builder-store.service.ts +++ b/src/app/onion/learning-object-builder/builder-store.service.ts @@ -989,14 +989,7 @@ export class BuilderStore { }) .catch(e => { this.serviceInteraction$.next(false); - if (e.status === 409) { - // tried to save an object with a name that already exists - this.validator.errors.saveErrors.set( - 'name', - 'A learning object with this name already exists! The title should be unique within your learning objects.' - ); - this.handleServiceError(e, BUILDER_ERRORS.DUPLICATE_OBJECT_NAME); - } else if (e.status === 400) { + if (e.status === 400) { this.validator.errors.saveErrors.set( 'name', e.error.message @@ -1023,18 +1016,11 @@ export class BuilderStore { this.serviceInteraction$.next(false); }) .catch(e => { - if (e.status === 409) { - // tried to save an object with a name that already exists - this.validator.errors.saveErrors.set( - 'name', - 'A learning object with this name already exists! The title should be unique within your learning objects.' - ); - this.handleServiceError(e, BUILDER_ERRORS.DUPLICATE_OBJECT_NAME); - } else if (e.status === 400) { - this.validator.errors.saveErrors.set( - 'name', - JSON.parse(e.error).message - ); + if (e.status === 400) { + const body = typeof e.error === 'string' ? JSON.parse(e.error) : e.error; + const errorMsg = body?.message?.[0]?.message?.[0] ?? ''; + + this.validator.errors.saveErrors.set('name', errorMsg); this.handleServiceError(e, BUILDER_ERRORS.SPECIAL_CHARACTER_NAME); } else { this.handleServiceError(e, BUILDER_ERRORS.UPDATE_OBJECT); diff --git a/src/app/onion/learning-object-builder/pages/info-page/info-page.component.html b/src/app/onion/learning-object-builder/pages/info-page/info-page.component.html index d2b5b1af2d..0492d2c9d5 100644 --- a/src/app/onion/learning-object-builder/pages/info-page/info-page.component.html +++ b/src/app/onion/learning-object-builder/pages/info-page/info-page.component.html @@ -12,7 +12,17 @@
{{copy.NAME}}
{{copy.NAMEPLACEHOLDER}}
- +

diff --git a/src/app/onion/learning-object-builder/pages/info-page/info-page.component.ts b/src/app/onion/learning-object-builder/pages/info-page/info-page.component.ts index d182ae0e23..7b56ba52d7 100644 --- a/src/app/onion/learning-object-builder/pages/info-page/info-page.component.ts +++ b/src/app/onion/learning-object-builder/pages/info-page/info-page.component.ts @@ -1,12 +1,12 @@ import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; -import { takeUntil } from 'rxjs/operators'; +import { distinctUntilChanged, switchMap, takeUntil, map, catchError } from 'rxjs/operators'; import { BuilderStore, BUILDER_ACTIONS as actions } from '../../builder-store.service'; import { LearningObject, User } from '@entity'; import { COPY } from './info-page.copy'; -import { Subject } from 'rxjs'; +import { Observable, of, Subject, from } from 'rxjs'; import { LearningObjectValidator } from '../../validators/learning-object.validator'; import { LearningObjectService } from 'app/core/learning-object-module/learning-object/learning-object.service'; @Component({ @@ -26,6 +26,9 @@ export class InfoPageComponent implements OnInit, OnDestroy { destroyed$: Subject = new Subject(); + // Emits raw name values from the input for per-keystroke checks + private nameChanges$: Subject = new Subject(); + constructor( private store: BuilderStore, public validator: LearningObjectValidator, @@ -44,6 +47,15 @@ export class InfoPageComponent implements OnInit, OnDestroy { this.selectedLevels = payload.levels || []; } }); + + this.nameChanges$ + .pipe( + // Makes it so that it doesn't check a name unless it has changed + distinctUntilChanged(), + switchMap(name => this.checkNameAvailability(name)), + takeUntil(this.destroyed$) + ) + .subscribe(); } @@ -56,6 +68,35 @@ export class InfoPageComponent implements OnInit, OnDestroy { this.store.execute(actions.MUTATE_OBJECT, data); } + onNameInput(value: string) { + this.mutateLearningObject({ name: value }); + this.nameChanges$.next(value); + } + + private checkNameAvailability(name: string): Observable { + const trimmedName = (name || '').trim(); + const duplicateErrorText = + 'A learning object with this name already exists! The title should be unique within your learning objects.'; + + // If the name is empty, skip the check and return early + if (!trimmedName) { + return of(void 0); + } + + return from(this.learningObjectService.checkNameAvailability(trimmedName)).pipe( + map(available => { + if (!available) { + this.validator.errors.saveErrors.set('name', duplicateErrorText); + } else { + this.validator.errors.saveErrors.delete('name'); + } + + this.cd.markForCheck(); + }), + catchError(() => of(void 0)) + ); + } + toggleContributor(user: User) { let action: number;