mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
59334: edit item metadata finished
This commit is contained in:
@@ -20,7 +20,8 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema)
|
||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
||||
cancelEditMetadataSchema: () => {}
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
|
@@ -27,7 +27,8 @@ describe('MetadataFieldFormComponent', () => {
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field)
|
||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
||||
cancelEditMetadataSchema: () => {},
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
<script src="edit-in-place-field.component.ts"></script>
|
||||
<div [ngClass]="{
|
||||
'table-warning': fieldUpdate.changeType === 0,
|
||||
'table-danger': fieldUpdate.changeType === 2,
|
||||
@@ -11,18 +12,18 @@
|
||||
<div *ngIf="(editable | async)" class="field-container">
|
||||
<ds-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||
[(ngModel)]="metadata.key"
|
||||
(submitSuggestion)="update()"
|
||||
(clickSuggestion)="update()"
|
||||
(typeSuggestion)="update()"
|
||||
(submitSuggestion)="update(suggestionControl.control)"
|
||||
(clickSuggestion)="update(suggestionControl.control)"
|
||||
(typeSuggestion)="update(suggestionControl.control)"
|
||||
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
||||
[formControl]="formControl"
|
||||
ngDefaultControl
|
||||
#suggestionControl="ngModel"
|
||||
[dsInListValidator]="metadataFields | async"
|
||||
></ds-input-suggestions>
|
||||
</div>
|
||||
<small class="text-danger"
|
||||
*ngIf="!(valid | async)">{{"item.edit.metadata.metadatafield.invalid" | translate}}</small>
|
||||
</td>
|
||||
<td class="col-7">
|
||||
<td class="col-6">
|
||||
<div *ngIf="!(editable | async)">
|
||||
<span>{{metadata?.value}}</span>
|
||||
</div>
|
||||
@@ -40,16 +41,20 @@
|
||||
(onDebounce)="update()"/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-1 text-center">
|
||||
<div>
|
||||
<i *ngIf="canSetEditable() | async" class="fas fa-edit fa-fw text-primary"
|
||||
(click)="setEditable(true)"></i>
|
||||
<i *ngIf="canSetUneditable() | async" class="fas fa-check fa-fw text-success"
|
||||
(click)="setEditable(false)"></i>
|
||||
<i *ngIf="canRemove() | async" class="fas fa-trash-alt fa-fw text-danger"
|
||||
(click)="remove()"></i>
|
||||
<i *ngIf="canUndo() | async" class="fas fa-undo-alt fa-fw text-warning"
|
||||
(click)="removeChangesFromField()"></i>
|
||||
<td class="col-2 text-center">
|
||||
<div class="btn-group">
|
||||
<button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)" (click)="setEditable(true)" class="btn btn-light btn-sm">
|
||||
<i class="fas fa-edit fa-fw text-primary"></i>
|
||||
</button>
|
||||
<button [disabled]="!(canSetUneditable() | async)" *ngIf="(editable | async)" (click)="setEditable(false)" class="btn btn-light btn-sm">
|
||||
<i class="fas fa-check fa-fw text-success"></i>
|
||||
</button>
|
||||
<button [disabled]="!(canRemove() | async)" (click)="remove()" class="btn btn-light btn-sm">
|
||||
<i class="fas fa-trash-alt fa-fw text-danger"></i>
|
||||
</button>
|
||||
<button [disabled]="!(canUndo() | async)" (click)="removeChangesFromField()" class="btn btn-light btn-sm">
|
||||
<i class="fas fa-undo-alt fa-fw text-warning"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</div>
|
@@ -16,6 +16,7 @@ import { InputSuggestion } from '../../../../shared/input-suggestions/input-sugg
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
let comp: EditInPlaceFieldComponent;
|
||||
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
||||
@@ -58,20 +59,23 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
paginatedMetadataFields = new PaginatedList(undefined, [mdField1, mdField2, mdField3]);
|
||||
|
||||
metadataFieldService = jasmine.createSpyObj({
|
||||
queryMetadataFields: observableOf(new RemoteData(false, false, true, undefined, paginatedMetadataFields))
|
||||
queryMetadataFields: observableOf(new RemoteData(false, false, true, undefined, paginatedMetadataFields)),
|
||||
getAllMetadataFields: observableOf(new RemoteData(false, false, true, undefined, paginatedMetadataFields))
|
||||
});
|
||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||
{
|
||||
saveChangeFieldUpdate: {},
|
||||
saveRemoveFieldUpdate: {},
|
||||
setEditableFieldUpdate: {},
|
||||
setValidFieldUpdate: {},
|
||||
removeSingleFieldUpdate: {},
|
||||
isEditable: observableOf(false) // should always return something --> its in ngOnInit
|
||||
isEditable: observableOf(false), // should always return something --> its in ngOnInit
|
||||
isValid: observableOf(true) // should always return something --> its in ngOnInit
|
||||
}
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [FormsModule, SharedModule],
|
||||
imports: [FormsModule, SharedModule, TranslateModule.forRoot()],
|
||||
declarations: [EditInPlaceFieldComponent],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: metadataFieldService },
|
||||
@@ -170,13 +174,27 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
describe('isValid is true', () => {
|
||||
beforeEach(() => {
|
||||
comp.remove();
|
||||
comp.valid = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should not contain an error message', () => {
|
||||
const errorMessages = de.queryAll(By.css('small.text-danger'));
|
||||
expect(errorMessages.length).toBe(0);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValid is false', () => {
|
||||
beforeEach(() => {
|
||||
comp.valid = observableOf(false);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should contain no input fields or textareas', () => {
|
||||
const errorMessages = de.queryAll(By.css('small.text-danger'));
|
||||
expect(errorMessages.length).toBeGreaterThan(0);
|
||||
|
||||
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct route and metadata', () => {
|
||||
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(route, metadatum);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,6 +208,16 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeChangesFromField', () => {
|
||||
beforeEach(() => {
|
||||
comp.removeChangesFromField();
|
||||
});
|
||||
|
||||
it('it should call removeChangesFromField on the objectUpdatesService with the correct route and uuid', () => {
|
||||
expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(route, metadatum.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findMetadataFieldSuggestions', () => {
|
||||
const query = 'query string';
|
||||
|
||||
@@ -281,44 +309,49 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
|
||||
describe('when canSetEditable emits true', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(false);
|
||||
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(true));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should contain a edit icon', () => {
|
||||
const editIcon = de.query(By.css('i.fa-edit'));
|
||||
expect(editIcon).not.toBeNull();
|
||||
it('the div should have an enabled button with an edit icon', () => {
|
||||
const editIcon = de.query(By.css('i.fa-edit')).parent.nativeElement.disabled;
|
||||
expect(editIcon).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canSetEditable emits false', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(false);
|
||||
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(false));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should not contain a edit icon', () => {
|
||||
const editIcon = de.query(By.css('i.fa-edit'));
|
||||
expect(editIcon).toBeNull();
|
||||
it('the div should have a disabled button with an edit icon', () => {
|
||||
const editIcon = de.query(By.css('i.fa-edit')).parent.nativeElement.disabled;
|
||||
expect(editIcon).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canSetUneditable emits true', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(true);
|
||||
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(true));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should contain a check icon', () => {
|
||||
const checkIcon = de.query(By.css('i.fa-check'));
|
||||
expect(checkIcon).not.toBeNull();
|
||||
it('the div should have an enabled button with a check icon', () => {
|
||||
const checkButtonAttrs = de.query(By.css('i.fa-check')).parent.nativeElement.disabled;
|
||||
expect(checkButtonAttrs).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canSetUneditable emits false', () => {
|
||||
beforeEach(() => {
|
||||
comp.editable = observableOf(true);
|
||||
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(false));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should not contain a check icon', () => {
|
||||
const checkIcon = de.query(By.css('i.fa-check'));
|
||||
expect(checkIcon).toBeNull();
|
||||
it('the div should have a disabled button with a check icon', () => {
|
||||
const checkButtonAttrs = de.query(By.css('i.fa-check')).parent.nativeElement.disabled;
|
||||
expect(checkButtonAttrs).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -327,9 +360,9 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
spyOn(comp, 'canRemove').and.returnValue(observableOf(true));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should contain a trash icon', () => {
|
||||
const trashIcon = de.query(By.css('i.fa-trash-alt'));
|
||||
expect(trashIcon).not.toBeNull();
|
||||
it('the div should have an enabled button with a trash icon', () => {
|
||||
const trashButtonAttrs = de.query(By.css('i.fa-trash-alt')).parent.nativeElement.disabled;
|
||||
expect(trashButtonAttrs).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -338,9 +371,9 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
spyOn(comp, 'canRemove').and.returnValue(observableOf(false));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should not contain a trash icon', () => {
|
||||
const trashIcon = de.query(By.css('i.fa-trash-alt'));
|
||||
expect(trashIcon).toBeNull();
|
||||
it('the div should have a disabled button with a trash icon', () => {
|
||||
const trashButtonAttrs = de.query(By.css('i.fa-trash-alt')).parent.nativeElement.disabled;
|
||||
expect(trashButtonAttrs).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -349,9 +382,9 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
spyOn(comp, 'canUndo').and.returnValue(observableOf(true));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should contain a undo icon', () => {
|
||||
const undoIcon = de.query(By.css('i.fa-undo-alt'));
|
||||
expect(undoIcon).not.toBeNull();
|
||||
it('the div should have an enabled button with an undo icon', () => {
|
||||
const undoIcon = de.query(By.css('i.fa-undo-alt')).parent.nativeElement.disabled;
|
||||
expect(undoIcon).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,9 +393,9 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
spyOn(comp, 'canUndo').and.returnValue(observableOf(false));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('the div should not contain a undo icon', () => {
|
||||
const undoIcon = de.query(By.css('i.fa-undo-alt'));
|
||||
expect(undoIcon).toBeNull();
|
||||
it('the div should have a disabled button with an undo icon', () => {
|
||||
const undoIcon = de.query(By.css('i.fa-undo-alt')).parent.nativeElement.disabled;
|
||||
expect(undoIcon).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,19 +1,18 @@
|
||||
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||
import { Metadatum } from '../../../../core/shared/metadatum.model';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||
import { inListValidator } from '../../../../shared/utils/validator.functions';
|
||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { FormControl, ValidationErrors, ValidatorFn } from '@angular/forms';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-in-place-field',
|
||||
@@ -51,7 +50,10 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
||||
*/
|
||||
metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]);
|
||||
|
||||
formControl: FormControl;
|
||||
/**
|
||||
* List of strings with all metadata field keys available
|
||||
*/
|
||||
metadataFields: Observable<string[]>;
|
||||
|
||||
constructor(
|
||||
private metadataFieldService: RegistryService,
|
||||
@@ -61,23 +63,21 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
||||
|
||||
/**
|
||||
* Sets up an observable that keeps track of the current editable and valid state of this field
|
||||
* Also creates a form control object for the input suggestions
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.editable = this.objectUpdatesService.isEditable(this.route, this.metadata.uuid);
|
||||
this.valid = this.objectUpdatesService.isValid(this.route, this.metadata.uuid);
|
||||
this.findMetadataFields().pipe(take(1)).subscribe((metadataFields: string[]) => {
|
||||
const validator = inListValidator(metadataFields);
|
||||
this.formControl = new FormControl('', validator);
|
||||
});
|
||||
this.metadataFields = this.findMetadataFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a new change update for this field to the object updates service
|
||||
*/
|
||||
update() {
|
||||
update(control?: FormControl) {
|
||||
this.objectUpdatesService.saveChangeFieldUpdate(this.route, this.metadata);
|
||||
this.objectUpdatesService.setValidFieldUpdate(this.route, this.metadata.uuid, this.formControl.valid);
|
||||
if (hasValue(control)) {
|
||||
this.objectUpdatesService.setValidFieldUpdate(this.route, this.metadata.uuid, control.valid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +131,9 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to request all metadata fields and convert them to a list of strings
|
||||
*/
|
||||
findMetadataFields(): Observable<string[]> {
|
||||
return this.metadataFieldService.getAllMetadataFields().pipe(
|
||||
getSucceededRemoteData(),
|
||||
|
@@ -26,9 +26,9 @@
|
||||
<tbody>
|
||||
<tr class="d-flex">
|
||||
<th class="col-3">{{'item.edit.metadata.headers.field' | translate}}</th>
|
||||
<th class="col-7">{{'item.edit.metadata.headers.value' | translate}}</th>
|
||||
<th class="col-6">{{'item.edit.metadata.headers.value' | translate}}</th>
|
||||
<th class="col-1 text-center">{{'item.edit.metadata.headers.language' | translate}}</th>
|
||||
<th class="col-1 text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
||||
<th class="col-2 text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
||||
</tr>
|
||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate">
|
||||
<ds-edit-in-place-field [fieldUpdate]="updateValue || {}"
|
||||
|
@@ -99,7 +99,8 @@ describe('ItemMetadataComponent', () => {
|
||||
getUpdatedFields: observableOf([metadatum1, metadatum2, metadatum3]),
|
||||
getLastModified: observableOf(date),
|
||||
hasUpdates: observableOf(true),
|
||||
isReinstatable: observableOf(false) // should always return something --> its in ngOnInit
|
||||
isReinstatable: observableOf(false), // should always return something --> its in ngOnInit
|
||||
isValidPage: observableOf(true)
|
||||
}
|
||||
);
|
||||
|
||||
|
@@ -2,10 +2,10 @@ import { type } from '../../../shared/ngrx/type';
|
||||
import { Action } from '@ngrx/store';
|
||||
import { Identifiable } from './object-updates.reducer';
|
||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||
|
||||
/**
|
||||
* The list of ObjectUpdatesAction type definitions
|
||||
*/
|
||||
|
||||
export const ObjectUpdatesActionTypes = {
|
||||
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
||||
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
InitializeFieldsAction,
|
||||
ReinstateObjectUpdatesAction,
|
||||
RemoveFieldUpdateAction, RemoveObjectUpdatesAction,
|
||||
SetEditableFieldUpdateAction
|
||||
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
||||
} from './object-updates.actions';
|
||||
import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer';
|
||||
|
||||
@@ -54,22 +54,25 @@ describe('objectUpdatesReducer', () => {
|
||||
fieldStates: {
|
||||
[identifiable1.uuid]: {
|
||||
editable: true,
|
||||
isNew: false
|
||||
isNew: false,
|
||||
isValid: true
|
||||
},
|
||||
[identifiable2.uuid]: {
|
||||
editable: false,
|
||||
isNew: true
|
||||
isNew: true,
|
||||
isValid: true
|
||||
},
|
||||
[identifiable3.uuid]: {
|
||||
editable: false,
|
||||
isNew: false
|
||||
isNew: false,
|
||||
isValid: false
|
||||
},
|
||||
},
|
||||
fieldUpdates: {
|
||||
[identifiable2.uuid]: {
|
||||
field: {
|
||||
uuid: identifiable2.uuid,
|
||||
key: 'dc.title',
|
||||
key: 'dc.titl',
|
||||
language: null,
|
||||
value: 'New title'
|
||||
},
|
||||
@@ -85,15 +88,18 @@ describe('objectUpdatesReducer', () => {
|
||||
fieldStates: {
|
||||
[identifiable1.uuid]: {
|
||||
editable: true,
|
||||
isNew: false
|
||||
isNew: false,
|
||||
isValid: true
|
||||
},
|
||||
[identifiable2.uuid]: {
|
||||
editable: false,
|
||||
isNew: true
|
||||
isNew: true,
|
||||
isValid: true
|
||||
},
|
||||
[identifiable3.uuid]: {
|
||||
editable: false,
|
||||
isNew: false
|
||||
isNew: false,
|
||||
isValid: true
|
||||
},
|
||||
},
|
||||
lastModified: modDate
|
||||
@@ -102,22 +108,25 @@ describe('objectUpdatesReducer', () => {
|
||||
fieldStates: {
|
||||
[identifiable1.uuid]: {
|
||||
editable: true,
|
||||
isNew: false
|
||||
isNew: false,
|
||||
isValid: true
|
||||
},
|
||||
[identifiable2.uuid]: {
|
||||
editable: false,
|
||||
isNew: true
|
||||
isNew: true,
|
||||
isValid: true
|
||||
},
|
||||
[identifiable3.uuid]: {
|
||||
editable: false,
|
||||
isNew: false
|
||||
isNew: false,
|
||||
isValid: false
|
||||
},
|
||||
},
|
||||
fieldUpdates: {
|
||||
[identifiable2.uuid]: {
|
||||
field: {
|
||||
uuid: identifiable2.uuid,
|
||||
key: 'dc.title',
|
||||
key: 'dc.titl',
|
||||
language: null,
|
||||
value: 'New title'
|
||||
},
|
||||
@@ -194,11 +203,13 @@ describe('objectUpdatesReducer', () => {
|
||||
fieldStates: {
|
||||
[identifiable1.uuid]: {
|
||||
editable: false,
|
||||
isNew: false
|
||||
isNew: false,
|
||||
isValid: true
|
||||
},
|
||||
[identifiable3.uuid]: {
|
||||
editable: false,
|
||||
isNew: false
|
||||
isNew: false,
|
||||
isValid: true
|
||||
},
|
||||
},
|
||||
fieldUpdates: {},
|
||||
@@ -216,6 +227,13 @@ describe('objectUpdatesReducer', () => {
|
||||
expect(newState[url].fieldStates[identifiable3.uuid].editable).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set the given field\'s fieldStates when the SET_VALID_FIELD action is dispatched, based on the payload', () => {
|
||||
const action = new SetValidFieldUpdateAction(url, identifiable3.uuid, false);
|
||||
|
||||
const newState = objectUpdatesReducer(testState, action);
|
||||
expect(newState[url].fieldStates[identifiable3.uuid].isValid).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should add a given field\'s update to the state when the ADD_FIELD action is dispatched, based on the payload', () => {
|
||||
const action = new AddFieldUpdateAction(url, identifiable1update, FieldChangeType.UPDATE);
|
||||
|
||||
|
@@ -32,9 +32,9 @@ describe('ObjectUpdatesService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const fieldStates = {
|
||||
[identifiable1.uuid]: { editable: false, isNew: false },
|
||||
[identifiable2.uuid]: { editable: true, isNew: false },
|
||||
[identifiable3.uuid]: { editable: true, isNew: true },
|
||||
[identifiable1.uuid]: { editable: false, isNew: false, isValid: true },
|
||||
[identifiable2.uuid]: { editable: true, isNew: false, isValid: false },
|
||||
[identifiable3.uuid]: { editable: true, isNew: true, isValid: true },
|
||||
};
|
||||
|
||||
const objectEntry = {
|
||||
@@ -45,6 +45,9 @@ describe('ObjectUpdatesService', () => {
|
||||
service = new ObjectUpdatesService(store);
|
||||
|
||||
spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry));
|
||||
spyOn(service as any, 'getFieldState').and.callFake((uuid) => {
|
||||
return observableOf(fieldStates[uuid]);
|
||||
});
|
||||
spyOn(service as any, 'saveFieldUpdate');
|
||||
});
|
||||
|
||||
@@ -75,7 +78,7 @@ describe('ObjectUpdatesService', () => {
|
||||
describe('isEditable', () => {
|
||||
it('should return false if this identifiable is currently not editable in the store', () => {
|
||||
const result$ = service.isEditable(url, identifiable1.uuid);
|
||||
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||
expect((service as any).getFieldState).toHaveBeenCalledWith(url, identifiable1.uuid);
|
||||
result$.subscribe((result) => {
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
@@ -83,7 +86,25 @@ describe('ObjectUpdatesService', () => {
|
||||
|
||||
it('should return true if this identifiable is currently editable in the store', () => {
|
||||
const result$ = service.isEditable(url, identifiable2.uuid);
|
||||
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||
expect((service as any).getFieldState).toHaveBeenCalledWith(url, identifiable2.uuid);
|
||||
result$.subscribe((result) => {
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValid', () => {
|
||||
it('should return false if this identifiable is currently not valid in the store', () => {
|
||||
const result$ = service.isValid(url, identifiable2.uuid);
|
||||
expect((service as any).getFieldState).toHaveBeenCalledWith(url, identifiable2.uuid);
|
||||
result$.subscribe((result) => {
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true if this identifiable is currently valid in the store', () => {
|
||||
const result$ = service.isValid(url, identifiable1.uuid);
|
||||
expect((service as any).getFieldState).toHaveBeenCalledWith(url, identifiable1.uuid);
|
||||
result$.subscribe((result) => {
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
@@ -18,7 +18,7 @@ import {
|
||||
RemoveFieldUpdateAction,
|
||||
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
||||
} from './object-updates.actions';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||
|
||||
@@ -30,6 +30,10 @@ function filterByUrlObjectUpdatesStateSelector(url: string): MemoizedSelector<Co
|
||||
return createSelector(objectUpdatesStateSelector(), (state: ObjectUpdatesState) => state[url]);
|
||||
}
|
||||
|
||||
function filterByUrlAndUUIDFieldStateSelector(url: string, uuid: string): MemoizedSelector<CoreState, FieldState> {
|
||||
return createSelector(filterByUrlObjectUpdatesStateSelector(url), (state: ObjectUpdatesEntry) => state.fieldStates[uuid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Service that dispatches and reads from the ObjectUpdates' state in the store
|
||||
*/
|
||||
@@ -67,6 +71,15 @@ export class ObjectUpdatesService {
|
||||
return this.store.pipe(select(filterByUrlObjectUpdatesStateSelector(url)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the getFieldState state for a specific URL and UUID
|
||||
* @param url The URL to filter by
|
||||
* @param uuid The field's UUID to filter by
|
||||
*/
|
||||
private getFieldState(url: string, uuid: string): Observable<FieldState> {
|
||||
return this.store.pipe(select(filterByUrlAndUUIDFieldStateSelector(url, uuid)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that combines the state's updates with the initial values (when there's no update) to create
|
||||
* a FieldUpdates object
|
||||
@@ -95,11 +108,11 @@ export class ObjectUpdatesService {
|
||||
* @param uuid The UUID of the field
|
||||
*/
|
||||
isEditable(url: string, uuid: string): Observable<boolean> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(
|
||||
filter((objectEntry) => hasValue(objectEntry.fieldStates[uuid])),
|
||||
map((objectEntry) => objectEntry.fieldStates[uuid].editable
|
||||
)
|
||||
const fieldState$ = this.getFieldState(url, uuid);
|
||||
return fieldState$.pipe(
|
||||
filter((fieldState) => hasValue(fieldState)),
|
||||
map((fieldState) => fieldState.editable),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,11 +122,11 @@ export class ObjectUpdatesService {
|
||||
* @param uuid The UUID of the field
|
||||
*/
|
||||
isValid(url: string, uuid: string): Observable<boolean> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(
|
||||
filter((objectEntry) => hasValue(objectEntry.fieldStates[uuid])),
|
||||
map((objectEntry) => objectEntry.fieldStates[uuid].isValid
|
||||
)
|
||||
const fieldState$ = this.getFieldState(url, uuid);
|
||||
return fieldState$.pipe(
|
||||
filter((fieldState) => hasValue(fieldState)),
|
||||
map((fieldState) => fieldState.isValid),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,7 +139,8 @@ export class ObjectUpdatesService {
|
||||
return objectUpdates.pipe(
|
||||
map((entry: ObjectUpdatesEntry) => {
|
||||
return Object.values(entry.fieldStates).findIndex((state: FieldState) => !state.isValid) < 0
|
||||
})
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -92,6 +92,7 @@ import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-co
|
||||
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
||||
import { ObjectValuesPipe } from './utils/object-values-pipe';
|
||||
import { InListValidator } from './utils/in-list-validator.directive';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -197,7 +198,8 @@ const DIRECTIVES = [
|
||||
VarDirective,
|
||||
DragClickDirective,
|
||||
DebounceDirective,
|
||||
ClickOutsideDirective
|
||||
ClickOutsideDirective,
|
||||
InListValidator
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
32
src/app/shared/utils/in-list-validator.directive.ts
Normal file
32
src/app/shared/utils/in-list-validator.directive.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Directive, Input } from '@angular/core';
|
||||
import { FormControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
|
||||
import { inListValidator } from './validator.functions';
|
||||
|
||||
/**
|
||||
* Directive for validating if a ngModel value is in a given list
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ngModel][dsInListValidator]',
|
||||
// We add our directive to the list of existing validators
|
||||
providers: [
|
||||
{ provide: NG_VALIDATORS, useExisting: InListValidator, multi: true }
|
||||
]
|
||||
})
|
||||
export class InListValidator implements Validator {
|
||||
/**
|
||||
* The list to look in
|
||||
*/
|
||||
@Input()
|
||||
dsInListValidator: string[];
|
||||
|
||||
/**
|
||||
* The function that checks if the form control's value is currently valid
|
||||
* @param c The FormControl
|
||||
*/
|
||||
validate(c: FormControl): ValidationErrors | null {
|
||||
if (this.dsInListValidator !== null) {
|
||||
return inListValidator(this.dsInListValidator)(c);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -1,7 +1,11 @@
|
||||
import { AbstractControl, ValidatorFn } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* Returns a validator function to check if the control's value is in a given list
|
||||
* @param list The list to look in
|
||||
*/
|
||||
export function inListValidator(list: string[]): ValidatorFn {
|
||||
return (control: AbstractControl): {[key: string]: any} | null => {
|
||||
const contains = list.indexOf(control.value) > 0;
|
||||
const contains = list.indexOf(control.value) > -1;
|
||||
return contains ? null : {inList: {value: control.value}} };
|
||||
}
|
||||
|
Reference in New Issue
Block a user