From 38d98200a6d52dad612acc29e2d0bb4a02bbb151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ja=C5=9Bkowski?= <138671284+g-jaskowski@users.noreply.github.com> Date: Fri, 7 Nov 2025 09:05:55 +0100 Subject: [PATCH] [ACS-10196] other a11y all libraries error message not provided for name and library id fields (#11311) * [ACS-10196] error message for required fields, unit test coverage, refactor deprecated code * [ACS-10196] use constructor instead of assertion * [ACS-10196] fix sonar errors * [ACS-10196] fix sonar errors * [ACS-10196] code review fixes * [ACS-10196] fix after setting autofocus to false --- .../lib/dialogs/library/library.dialog.html | 73 ++-- .../dialogs/library/library.dialog.spec.ts | 315 ++++++++++++------ .../src/lib/dialogs/library/library.dialog.ts | 105 +++--- lib/content-services/src/lib/i18n/en.json | 4 +- 4 files changed, 321 insertions(+), 176 deletions(-) diff --git a/lib/content-services/src/lib/dialogs/library/library.dialog.html b/lib/content-services/src/lib/dialogs/library/library.dialog.html index 42fb246dbe..6fb35bac7c 100644 --- a/lib/content-services/src/lib/dialogs/library/library.dialog.html +++ b/lib/content-services/src/lib/dialogs/library/library.dialog.html @@ -11,21 +11,18 @@ autocomplete="off" adf-auto-focus /> - - {{ - 'LIBRARY.HINTS.SITE_TITLE_EXISTS' | translate - }} - - {{ 'LIBRARY.ERRORS.TITLE_TOO_LONG' | translate }} - - - - {{ 'LIBRARY.ERRORS.TITLE_TOO_SHORT' | translate }} - - - - {{ form.controls['title'].errors?.message | translate }} - + @if (libraryTitleExists) { + {{ 'LIBRARY.HINTS.SITE_TITLE_EXISTS' | translate }} + } + @if (form.controls['title'].errors?.maxlength) { + {{ 'LIBRARY.ERRORS.TITLE_TOO_LONG' | translate }} + } + @else if (form.controls['title'].errors?.message) { + {{ form.controls['title'].errors?.message | translate }} + } + @else if (form.controls['title'].errors?.required) { + {{ 'LIBRARY.ERRORS.NAME_REQUIRED' | translate }} + } @@ -37,13 +34,15 @@ autocomplete="off" /> - - {{ form.controls['id'].errors?.message | translate }} - - - - {{ 'LIBRARY.ERRORS.ID_TOO_LONG' | translate }} - + @if (form.controls['id'].errors?.message) { + {{ form.controls['id'].errors?.message | translate }} + } + @else if (form.controls['id'].errors?.maxlength) { + {{ 'LIBRARY.ERRORS.ID_TOO_LONG' | translate }} + } + @else if (form.controls['id'].errors?.required) { + {{ 'LIBRARY.ERRORS.ID_REQUIRED' | translate }} + } @@ -53,10 +52,9 @@ rows="3" formControlName="description" > - - - {{ 'LIBRARY.ERRORS.DESCRIPTION_TOO_LONG' | translate }} - + @if (form.controls['description'].errors?.maxlength) { + {{ 'LIBRARY.ERRORS.DESCRIPTION_TOO_LONG' | translate }} + } - - {{ option.label | translate }} - + @for (option of visibilityOptions; track option.value) { + + {{ option.label | translate }} + + } diff --git a/lib/content-services/src/lib/dialogs/library/library.dialog.spec.ts b/lib/content-services/src/lib/dialogs/library/library.dialog.spec.ts index 304432de6d..fa9a477d31 100644 --- a/lib/content-services/src/lib/dialogs/library/library.dialog.spec.ts +++ b/lib/content-services/src/lib/dialogs/library/library.dialog.spec.ts @@ -22,21 +22,40 @@ import { MatDialogRef } from '@angular/material/dialog'; import { ContentTestingModule } from '../../testing/content.testing.module'; import { of, throwError } from 'rxjs'; import { delay } from 'rxjs/operators'; -import { SiteEntry } from '@alfresco/js-api'; +import { FindQuery, SiteEntry, SitePaging } from '@alfresco/js-api'; import { SitesService } from '../../common/services/sites.service'; -import { NotificationService } from '@alfresco/adf-core'; +import { NotificationService, UnitTestingUtils } from '@alfresco/adf-core'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatRadioGroupHarness } from '@angular/material/radio/testing'; describe('LibraryDialogComponent', () => { let fixture: ComponentFixture; let component: LibraryDialogComponent; let sitesService: SitesService; - let findSitesSpy; + let findSitesSpy: jasmine.Spy<(term: string, opts?: FindQuery) => Promise>; let notificationService: NotificationService; - const findSitesResponse = { list: { entries: [] } }; + let unitTestingUtils: UnitTestingUtils; + let loader: HarnessLoader; + + const findSitesResponse = new SitePaging({ list: { entries: [], pagination: {} } }); const dialogRef = { close: jasmine.createSpy('close') }; + /** + * Sets form field value and triggers necessary events to process value changes + * + * @param fieldName Name of the form field + * @param value Value to set + */ + function setFormFieldValue(fieldName: string, value: string) { + component.form.controls[fieldName].setValue(value); + tick(500); + flush(); + fixture.detectChanges(); + } + beforeEach(() => { TestBed.configureTestingModule({ imports: [ContentTestingModule, LibraryDialogComponent], @@ -48,6 +67,8 @@ describe('LibraryDialogComponent', () => { sitesService = TestBed.inject(SitesService); findSitesSpy = spyOn(component['queriesApi'], 'findSites'); notificationService = TestBed.inject(NotificationService); + loader = TestbedHarnessEnvironment.loader(fixture); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement, loader); }); afterEach(() => { @@ -56,45 +77,33 @@ describe('LibraryDialogComponent', () => { it('should set library id automatically on title input', fakeAsync(() => { findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); + spyOn(sitesService, 'getSite').and.callFake(() => throwError(() => 'error')); fixture.detectChanges(); - component.form.controls.title.setValue('libraryTitle'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', 'libraryTitle'); expect(component.form.controls.id.value).toBe('libraryTitle'); })); it('should translate library title space character to dash for library id', fakeAsync(() => { findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); + spyOn(sitesService, 'getSite').and.callFake(() => throwError(() => 'error')); fixture.detectChanges(); - component.form.controls.title.setValue('library title'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', 'library title'); expect(component.form.controls.id.value).toBe('library-title'); })); it('should not change custom library id on title input', fakeAsync(() => { findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); + spyOn(sitesService, 'getSite').and.callFake(() => throwError(() => 'error')); fixture.detectChanges(); - component.form.controls.id.setValue('custom-id'); + setFormFieldValue('id', 'custom-id'); component.form.controls.id.markAsDirty(); - tick(500); - flush(); - fixture.detectChanges(); - component.form.controls.title.setValue('library title'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', 'library title'); expect(component.form.controls.id.value).toBe('custom-id'); })); @@ -103,10 +112,7 @@ describe('LibraryDialogComponent', () => { spyOn(sitesService, 'getSite').and.returnValue(of(null)); fixture.detectChanges(); - component.form.controls.id.setValue('existingLibrary'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('id', 'existingLibrary'); expect(component.form.controls.id.errors).toEqual({ message: 'LIBRARY.ERRORS.EXISTENT_SITE' @@ -117,13 +123,10 @@ describe('LibraryDialogComponent', () => { it('should create site when form is valid', fakeAsync(() => { findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); spyOn(sitesService, 'createSite').and.returnValue(of({ entry: { id: 'fake-id' } } as SiteEntry).pipe(delay(100))); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); + spyOn(sitesService, 'getSite').and.callFake(() => throwError(() => 'error')); fixture.detectChanges(); - component.form.controls.title.setValue('library title'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', 'library title'); component.submit(); fixture.detectChanges(); @@ -152,10 +155,7 @@ describe('LibraryDialogComponent', () => { spyOn(sitesService, 'getSite').and.returnValue(of(null)); fixture.detectChanges(); - component.form.controls.title.setValue('existingLibrary'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', 'existingLibrary'); component.submit(); fixture.detectChanges(); @@ -166,108 +166,231 @@ describe('LibraryDialogComponent', () => { it('should notify when library title is already used', fakeAsync(() => { spyOn(sitesService, 'getSite').and.returnValue(of(null)); - findSitesSpy.and.returnValue(Promise.resolve({ list: { entries: [{ entry: { title: 'TEST', id: 'library-id' } }] } })); + const sitePaging = new SitePaging({ + list: { + entries: [ + { + entry: { + id: 'library-id', + title: 'TEST', + guid: '', + visibility: 'PUBLIC' + } + } + ], + pagination: {} + } + }); + findSitesSpy.and.returnValue(Promise.resolve(sitePaging)); fixture.detectChanges(); - component.form.controls.title.setValue('test'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', 'test'); expect(component.libraryTitleExists).toBe(true); })); - it('should notify on 409 conflict error (might be in trash)', fakeAsync(() => { - findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); - const error = { message: '{ "error": { "statusCode": 409 } }' }; - spyOn(sitesService, 'createSite').and.callFake(() => throwError(error)); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); + describe('create site error', () => { + /** + * Performs setup for create site errors related tests. + * + * @param statusCode Optional status code to include in the error message. + */ + function throwCreateError(statusCode?: number) { + let error = {}; + if (statusCode) { + error = { message: `{ "error": { "statusCode": ${statusCode} } }` }; + } - fixture.detectChanges(); - component.form.controls.title.setValue('test'); - tick(500); - flush(); - fixture.detectChanges(); + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + spyOn(sitesService, 'createSite').and.callFake(() => throwError(() => error)); + spyOn(sitesService, 'getSite').and.callFake(() => throwError(() => 'error')); - component.submit(); - fixture.detectChanges(); - flush(); + fixture.detectChanges(); + setFormFieldValue('title', 'test'); - expect(component.form.controls.id.errors).toEqual({ - message: 'LIBRARY.ERRORS.CONFLICT' - }); - })); + component.submit(); + fixture.detectChanges(); + flush(); + } - it('should handle default errors and show generic error in snackbar', fakeAsync(() => { - findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); - const error = {}; - spyOn(sitesService, 'createSite').and.callFake(() => throwError(error)); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); - spyOn(notificationService, 'showError').and.callThrough(); + it('should notify on 409 conflict error (might be in trash)', fakeAsync(() => { + throwCreateError(409); + expect(component.form.controls.id.errors).toEqual({ + message: 'LIBRARY.ERRORS.CONFLICT' + }); + })); - fixture.detectChanges(); - component.form.controls.title.setValue('test'); - tick(500); - flush(); - fixture.detectChanges(); + it('should show generic error notification on error code other than 409', fakeAsync(() => { + spyOn(notificationService, 'showError'); + throwCreateError(404); + expect(notificationService.showError).toHaveBeenCalledWith('CORE.MESSAGES.ERRORS.GENERIC'); + })); - component.submit(); - fixture.detectChanges(); - flush(); - - expect(notificationService.showError).toHaveBeenCalledWith('CORE.MESSAGES.ERRORS.GENERIC'); - })); + it('should handle default errors and show generic error in snackbar', fakeAsync(() => { + spyOn(notificationService, 'showError'); + throwCreateError(); + expect(notificationService.showError).toHaveBeenCalledWith('CORE.MESSAGES.ERRORS.GENERIC'); + })); + }); it('should not translate library title if value is not a valid id', fakeAsync(() => { findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); + spyOn(sitesService, 'getSite').and.callFake(() => throwError(() => 'error')); fixture.detectChanges(); - component.form.controls.title.setValue('@@@####'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', '@@@####'); expect(component.form.controls.id.value).toBe(null); })); it('should translate library title partially for library id', fakeAsync(() => { findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); + spyOn(sitesService, 'getSite').and.callFake(() => throwError(() => 'error')); fixture.detectChanges(); - component.form.controls.title.setValue('@@@####library'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', '@@@####library'); expect(component.form.controls.id.value).toBe('library'); })); it('should translate library title multiple space character to one dash for library id', fakeAsync(() => { findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); + spyOn(sitesService, 'getSite').and.callFake(() => throwError(() => 'error')); fixture.detectChanges(); - component.form.controls.title.setValue('library title'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', 'library title'); expect(component.form.controls.id.value).toBe('library-title'); })); it('should invalidate library title if is too short', fakeAsync(() => { findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); - spyOn(sitesService, 'getSite').and.callFake(() => throwError('error')); + spyOn(sitesService, 'getSite').and.callFake(() => throwError(() => 'error')); fixture.detectChanges(); - component.form.controls.title.setValue('l'); - tick(500); - flush(); - fixture.detectChanges(); + setFormFieldValue('title', 'l'); - expect(component.form.controls.title.errors['minlength']).toBeTruthy(); + expect(component.form.controls.title.errors).toEqual({ + message: 'LIBRARY.ERRORS.TITLE_TOO_SHORT' + }); + expect(component.form.valid).toBe(false); + + setFormFieldValue('title', 'l '); + + expect(component.form.controls.title.errors).toEqual({ + message: 'LIBRARY.ERRORS.TITLE_TOO_SHORT' + }); expect(component.form.valid).toBe(false); })); + + it('should handle getters when form fields have no values', () => { + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + fixture.detectChanges(); + component.form.controls.title.setValue(null); + component.form.controls.id.setValue(null); + component.visibilityOption = null; + + expect(component.title).toBe(''); + expect(component.id).toBe(''); + expect(component.visibility).toBe(''); + }); + + it('should handle visibility change', async () => { + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + fixture.detectChanges(); + + const radioGroup = await loader.getHarness(MatRadioGroupHarness); + const radioButtons = await radioGroup.getRadioButtons(); + + await radioButtons[1].check(); + fixture.detectChanges(); + + expect(component.visibilityOption).toBe('PRIVATE'); + expect(component.visibility).toBe('PRIVATE'); + }); + + it('should set libraryTitleExists to false when findSites returns no sites', () => { + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + fixture.detectChanges(); + component.form.controls.title.setValue('library'); + fixture.detectChanges(); + + expect(component.libraryTitleExists).toBe(false); + }); + + it('should clear timeout if validator is called multiple times', () => { + spyOn(sitesService, 'getSite').and.returnValue(of(null)); + spyOn(window, 'clearTimeout').and.callThrough(); + + fixture.detectChanges(); + + component.form.controls.id.setValue('first'); + component.form.controls.id.setValue('second'); + + expect(window.clearTimeout).toHaveBeenCalled(); + }); + + it('should catch error if findLibraryByTitle fails', async () => { + findSitesSpy.and.returnValue(Promise.reject(new Error('error'))); + const result = await component['findLibraryByTitle']('library'); + expect(result).toEqual(findSitesResponse); + }); + + it('should show correct error message when value is only spaces', () => { + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + fixture.detectChanges(); + component.form.controls.title.setValue(' '); + expect(component.form.controls.title.errors).toEqual({ message: 'LIBRARY.ERRORS.ONLY_SPACES' }); + }); + + it('should show correct error message when value contains special characters', () => { + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + fixture.detectChanges(); + component.form.controls.id.setValue('wąż'); + expect(component.form.controls.id.errors).toEqual({ message: 'LIBRARY.ERRORS.ILLEGAL_CHARACTERS' }); + }); + + it('should not show required error on opening dialog', async () => { + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + fixture.detectChanges(); + const titleFormField = await unitTestingUtils.getMatFormField(); + const errors = await titleFormField.getTextErrors(); + expect(errors.length).toBe(0); + }); + + it('should mark title as touched on value change', fakeAsync(() => { + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + spyOn(sitesService, 'getSite').and.returnValue(of(null)); + fixture.detectChanges(); + expect(component.form.controls.title.touched).toBeFalse(); + setFormFieldValue('title', 'library title'); + expect(component.form.controls.title.touched).toBeTrue(); + })); + + it('should show correct error message when there is no library title', async () => { + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + fixture.detectChanges(); + component.form.controls.title.setValue(''); + fixture.detectChanges(); + + const titleFormField = await unitTestingUtils.getMatFormField(); + const errors = await titleFormField.getTextErrors(); + + expect(component.form.controls.title.errors).toEqual({ required: true }); + expect(errors[0]).toContain('LIBRARY.ERRORS.NAME_REQUIRED'); + }); + + it('should show correct error message when there is no library id', async () => { + findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse)); + fixture.detectChanges(); + component.form.controls.id.setValue(''); + component.form.controls.id.markAsTouched(); + fixture.detectChanges(); + + const idFormField = await unitTestingUtils.getMatFormFieldByCSS('.adf-library-dialog-form-field:nth-of-type(2)'); + const errors = await idFormField.getTextErrors(); + + expect(component.form.controls.id.errors).toEqual({ required: true }); + expect(errors[0]).toContain('LIBRARY.ERRORS.ID_REQUIRED'); + }); }); diff --git a/lib/content-services/src/lib/dialogs/library/library.dialog.ts b/lib/content-services/src/lib/dialogs/library/library.dialog.ts index 269dd7a984..c7de2d700d 100644 --- a/lib/content-services/src/lib/dialogs/library/library.dialog.ts +++ b/lib/content-services/src/lib/dialogs/library/library.dialog.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Observable } from 'rxjs'; +import { from, Observable } from 'rxjs'; import { Component, DestroyRef, EventEmitter, inject, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { AbstractControl, @@ -24,23 +24,30 @@ import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, + ValidationErrors, Validators } from '@angular/forms'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { QueriesApi, SiteBodyCreate, SiteEntry, SitePaging } from '@alfresco/js-api'; import { NotificationService } from '@alfresco/adf-core'; -import { debounceTime, finalize, mergeMap } from 'rxjs/operators'; +import { debounceTime, finalize, map, mergeMap, take } from 'rxjs/operators'; import { SitesService } from '../../common/services/sites.service'; import { CommonModule } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AutoFocusDirective } from '../../directives'; -import { MatRadioModule } from '@angular/material/radio'; +import { MatRadioChange, MatRadioModule } from '@angular/material/radio'; import { MatButtonModule } from '@angular/material/button'; import { AlfrescoApiService } from '../../services'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +interface VisibilityOption { + value: string; + label: string; + disabled: boolean; +} + @Component({ selector: 'adf-library-dialog', imports: [ @@ -71,13 +78,13 @@ export class LibraryDialogComponent implements OnInit { * newly-created library. */ @Output() - success = new EventEmitter(); + success = new EventEmitter(); createTitle = 'LIBRARY.DIALOG.CREATE_TITLE'; libraryTitleExists = false; form: UntypedFormGroup; - visibilityOption: any; - visibilityOptions = [ + visibilityOption: string; + visibilityOptions: VisibilityOption[] = [ { value: 'PUBLIC', label: 'LIBRARY.VISIBILITY.PUBLIC', disabled: false }, { value: 'PRIVATE', label: 'LIBRARY.VISIBILITY.PRIVATE', disabled: false }, { @@ -107,7 +114,7 @@ export class LibraryDialogComponent implements OnInit { ngOnInit() { const validators = { id: [Validators.required, Validators.maxLength(72), this.forbidSpecialCharacters], - title: [Validators.required, this.forbidOnlySpaces, Validators.minLength(2), Validators.maxLength(256)], + title: [Validators.required, this.forbidOnlySpaces, this.minLengthTrimmed, Validators.maxLength(256)], description: [Validators.maxLength(512)] }; @@ -119,13 +126,14 @@ export class LibraryDialogComponent implements OnInit { this.visibilityOption = this.visibilityOptions[0].value; + this.form.controls['title'].valueChanges + .pipe(take(1), takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.form.controls['title'].markAsTouched()); + this.form.controls['title'].valueChanges .pipe( debounceTime(500), - mergeMap( - (title) => this.checkLibraryNameExists(title), - (title) => title - ), + mergeMap((title) => from(this.checkLibraryNameExists(title)).pipe(map(() => title))), takeUntilDestroyed(this.destroyRef) ) .subscribe((title: string) => { @@ -168,16 +176,16 @@ export class LibraryDialogComponent implements OnInit { this.disableCreateButton = true; this.create() .pipe(finalize(() => (this.disableCreateButton = false))) - .subscribe( - (node: SiteEntry) => { + .subscribe({ + next: (node: SiteEntry) => { this.success.emit(node); dialog.close(node); }, - (error) => this.handleError(error) - ); + error: (error) => this.handleError(error) + }); } - visibilityChangeHandler(event) { + visibilityChangeHandler(event: MatRadioChange) { this.visibilityOption = event.value; } @@ -224,13 +232,7 @@ export class LibraryDialogComponent implements OnInit { } private async checkLibraryNameExists(libraryTitle: string) { - let entries = []; - - try { - entries = (await this.findLibraryByTitle(libraryTitle)).list.entries; - } catch { - entries = []; - } + const entries = (await this.findLibraryByTitle(libraryTitle)).list.entries; if (entries.length) { this.libraryTitleExists = entries[0].entry.title.toLowerCase() === libraryTitle.toLowerCase(); @@ -239,20 +241,24 @@ export class LibraryDialogComponent implements OnInit { } } - private findLibraryByTitle(libraryTitle: string): Promise { - return this.queriesApi.findSites(libraryTitle, { - maxItems: 1, - fields: ['title'] - }); + private async findLibraryByTitle(libraryTitle: string): Promise { + try { + return await this.queriesApi.findSites(libraryTitle, { + maxItems: 1, + fields: ['title'] + }); + } catch { + return new SitePaging({ list: { entries: [], pagination: {} } }); + } } - private forbidSpecialCharacters({ value }: UntypedFormControl) { + private forbidSpecialCharacters({ value }: UntypedFormControl): ValidationErrors | null { if (value === null || value.length === 0) { return null; } const validCharacters: RegExp = /[^A-Za-z0-9-]/; - const isValid: boolean = !validCharacters.test(value); + const isValid = !validCharacters.test(value); return isValid ? null @@ -261,12 +267,12 @@ export class LibraryDialogComponent implements OnInit { }; } - private forbidOnlySpaces({ value }: UntypedFormControl) { + private forbidOnlySpaces({ value }: UntypedFormControl): ValidationErrors | null { if (value === null || value.length === 0) { return null; } - const isValid: boolean = !!(value || '').trim(); + const isValid = !!value.trim(); return isValid ? null @@ -275,24 +281,39 @@ export class LibraryDialogComponent implements OnInit { }; } - private createSiteIdValidator() { + private minLengthTrimmed({ value }: UntypedFormControl): ValidationErrors | null { + if (value === null || value.length === 0) { + return null; + } + + const isValid = value.trim().length !== 1; + + return isValid + ? null + : { + message: 'LIBRARY.ERRORS.TITLE_TOO_SHORT' + }; + } + + private createSiteIdValidator(): (control: AbstractControl) => Promise { let timer; return (control: AbstractControl) => { if (timer) { clearTimeout(timer); } - return new Promise((resolve) => { - timer = setTimeout( - () => - this.sitesService.getSite(control.value).subscribe( - () => resolve({ message: 'LIBRARY.ERRORS.EXISTENT_SITE' }), - () => resolve(null) - ), - 300 - ); + timer = setTimeout(() => { + this.checkSite(control.value, resolve); + }, 300); }); }; } + + private checkSite(siteId: string, resolve: (result: ValidationErrors | null) => void): void { + this.sitesService.getSite(siteId).subscribe({ + next: () => resolve({ message: 'LIBRARY.ERRORS.EXISTENT_SITE' }), + error: () => resolve(null) + }); + } } diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index bfd41e488f..1efca447cb 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -659,7 +659,9 @@ "TITLE_TOO_SHORT": "Title must be at least 2 characters long", "ILLEGAL_CHARACTERS": "Use numbers and letters only", "ONLY_SPACES": "Library name can't contain only spaces", - "LIBRARY_UPDATE_ERROR": "There was an error updating library properties" + "LIBRARY_UPDATE_ERROR": "There was an error updating library properties", + "NAME_REQUIRED": "Library name is required", + "ID_REQUIRED": "Library ID is required" }, "SUCCESS": { "LIBRARY_UPDATED": "Library properties updated"