mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
[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
This commit is contained in:
@@ -11,21 +11,18 @@
|
||||
autocomplete="off"
|
||||
adf-auto-focus
|
||||
/>
|
||||
|
||||
<mat-hint *ngIf="libraryTitleExists">{{
|
||||
'LIBRARY.HINTS.SITE_TITLE_EXISTS' | translate
|
||||
}}</mat-hint>
|
||||
<mat-error *ngIf="form.controls['title'].hasError('maxlength')">
|
||||
{{ 'LIBRARY.ERRORS.TITLE_TOO_LONG' | translate }}
|
||||
</mat-error>
|
||||
|
||||
<mat-error *ngIf="form.controls['title'].hasError('minlength')">
|
||||
{{ 'LIBRARY.ERRORS.TITLE_TOO_SHORT' | translate }}
|
||||
</mat-error>
|
||||
|
||||
<mat-error *ngIf="form.controls['title'].errors?.message">
|
||||
{{ form.controls['title'].errors?.message | translate }}
|
||||
</mat-error>
|
||||
@if (libraryTitleExists) {
|
||||
<mat-hint>{{ 'LIBRARY.HINTS.SITE_TITLE_EXISTS' | translate }}</mat-hint>
|
||||
}
|
||||
@if (form.controls['title'].errors?.maxlength) {
|
||||
<mat-error>{{ 'LIBRARY.ERRORS.TITLE_TOO_LONG' | translate }}</mat-error>
|
||||
}
|
||||
@else if (form.controls['title'].errors?.message) {
|
||||
<mat-error>{{ form.controls['title'].errors?.message | translate }}</mat-error>
|
||||
}
|
||||
@else if (form.controls['title'].errors?.required) {
|
||||
<mat-error>{{ 'LIBRARY.ERRORS.NAME_REQUIRED' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="adf-library-dialog-form-field">
|
||||
@@ -37,13 +34,15 @@
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
<mat-error *ngIf="form.controls['id'].errors?.message">
|
||||
{{ form.controls['id'].errors?.message | translate }}
|
||||
</mat-error>
|
||||
|
||||
<mat-error *ngIf="form.controls['id'].hasError('maxlength')">
|
||||
{{ 'LIBRARY.ERRORS.ID_TOO_LONG' | translate }}
|
||||
</mat-error>
|
||||
@if (form.controls['id'].errors?.message) {
|
||||
<mat-error>{{ form.controls['id'].errors?.message | translate }}</mat-error>
|
||||
}
|
||||
@else if (form.controls['id'].errors?.maxlength) {
|
||||
<mat-error>{{ 'LIBRARY.ERRORS.ID_TOO_LONG' | translate }}</mat-error>
|
||||
}
|
||||
@else if (form.controls['id'].errors?.required) {
|
||||
<mat-error>{{ 'LIBRARY.ERRORS.ID_REQUIRED' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="adf-library-dialog-form-field adf-library-dialog-form-field-description">
|
||||
@@ -53,10 +52,9 @@
|
||||
rows="3"
|
||||
formControlName="description"
|
||||
></textarea>
|
||||
|
||||
<mat-error *ngIf="form.controls['description'].hasError('maxlength')">
|
||||
{{ 'LIBRARY.ERRORS.DESCRIPTION_TOO_LONG' | translate }}
|
||||
</mat-error>
|
||||
@if (form.controls['description'].errors?.maxlength) {
|
||||
<mat-error>{{ 'LIBRARY.ERRORS.DESCRIPTION_TOO_LONG' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-radio-group
|
||||
@@ -65,17 +63,18 @@
|
||||
[(ngModel)]="visibilityOption"
|
||||
(change)="visibilityChangeHandler($event)"
|
||||
>
|
||||
<mat-radio-button
|
||||
color="primary"
|
||||
class="adf-library-dialog-radio-group-button"
|
||||
[disabled]="option.disabled"
|
||||
*ngFor="let option of visibilityOptions"
|
||||
[attr.data-automation-id]="option.value"
|
||||
[value]="option.value"
|
||||
[checked]="visibilityOption.value === option.value"
|
||||
>
|
||||
{{ option.label | translate }}
|
||||
</mat-radio-button>
|
||||
@for (option of visibilityOptions; track option.value) {
|
||||
<mat-radio-button
|
||||
color="primary"
|
||||
class="adf-library-dialog-radio-group-button"
|
||||
[disabled]="option.disabled"
|
||||
[attr.data-automation-id]="option.value"
|
||||
[value]="option.value"
|
||||
[checked]="visibilityOption === option.value"
|
||||
>
|
||||
{{ option.label | translate }}
|
||||
</mat-radio-button>
|
||||
}
|
||||
</mat-radio-group>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
@@ -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<LibraryDialogComponent>;
|
||||
let component: LibraryDialogComponent;
|
||||
let sitesService: SitesService;
|
||||
let findSitesSpy;
|
||||
let findSitesSpy: jasmine.Spy<(term: string, opts?: FindQuery) => Promise<SitePaging>>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<any>();
|
||||
success = new EventEmitter<SiteEntry>();
|
||||
|
||||
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<SitePaging> {
|
||||
return this.queriesApi.findSites(libraryTitle, {
|
||||
maxItems: 1,
|
||||
fields: ['title']
|
||||
});
|
||||
private async findLibraryByTitle(libraryTitle: string): Promise<SitePaging> {
|
||||
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<ValidationErrors | null> {
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user