diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts
index 02cf168387..42b6d1f133 100644
--- a/src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts
+++ b/src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts
@@ -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: () => {
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts
index 25502a27c8..4364b0234a 100644
--- a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts
+++ b/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts
@@ -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: () => {
diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html
index 51f4c650af..e4fdc60a1e 100644
--- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html
+++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html
@@ -1,3 +1,4 @@
+
{{"item.edit.metadata.metadatafield.invalid" | translate}}
-
+ |
{{metadata?.value}}
@@ -40,16 +41,20 @@
(onDebounce)="update()"/>
|
-
-
-
-
-
-
+
+
+
+
+
+
|
\ No newline at end of file
diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts
index aad94931dd..927a4a96b1 100644
--- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts
+++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts
@@ -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;
@@ -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);
});
});
diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts
index b85b558cfd..d0b35b7a82 100644
--- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts
+++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts
@@ -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 = new BehaviorSubject([]);
- formControl: FormControl;
+ /**
+ * List of strings with all metadata field keys available
+ */
+ metadataFields: Observable;
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 {
return this.metadataFieldService.getAllMetadataFields().pipe(
getSucceededRemoteData(),
diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html
index bc8e52d48b..36d18372bd 100644
--- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html
+++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html
@@ -26,9 +26,9 @@
{{'item.edit.metadata.headers.field' | translate}} |
- {{'item.edit.metadata.headers.value' | translate}} |
+ {{'item.edit.metadata.headers.value' | translate}} |
{{'item.edit.metadata.headers.language' | translate}} |
- {{'item.edit.metadata.headers.edit' | translate}} |
+ {{'item.edit.metadata.headers.edit' | translate}} |
{
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)
}
);
diff --git a/src/app/core/data/object-updates/object-updates.actions.ts b/src/app/core/data/object-updates/object-updates.actions.ts
index c1b35d07b4..7381188892 100644
--- a/src/app/core/data/object-updates/object-updates.actions.ts
+++ b/src/app/core/data/object-updates/object-updates.actions.ts
@@ -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'),
diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts
index 377b3ab1b4..f5698b9b78 100644
--- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts
+++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts
@@ -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);
diff --git a/src/app/core/data/object-updates/object-updates.service.spec.ts b/src/app/core/data/object-updates/object-updates.service.spec.ts
index 4f0393c641..e9fc4652b0 100644
--- a/src/app/core/data/object-updates/object-updates.service.spec.ts
+++ b/src/app/core/data/object-updates/object-updates.service.spec.ts
@@ -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);
});
diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts
index 6136d31ac0..85e17b5b2f 100644
--- a/src/app/core/data/object-updates/object-updates.service.ts
+++ b/src/app/core/data/object-updates/object-updates.service.ts
@@ -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 state[url]);
}
+function filterByUrlAndUUIDFieldStateSelector(url: string, uuid: string): MemoizedSelector {
+ 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 {
+ 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 {
- 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 {
- 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()
)
}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index c3e7051304..6bdeabec43 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -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({
diff --git a/src/app/shared/utils/in-list-validator.directive.ts b/src/app/shared/utils/in-list-validator.directive.ts
new file mode 100644
index 0000000000..621ae93b83
--- /dev/null
+++ b/src/app/shared/utils/in-list-validator.directive.ts
@@ -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;
+ }
+}
diff --git a/src/app/shared/utils/validator.functions.ts b/src/app/shared/utils/validator.functions.ts
index 55fe498747..3ad660731a 100644
--- a/src/app/shared/utils/validator.functions.ts
+++ b/src/app/shared/utils/validator.functions.ts
@@ -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}} };
}
|