mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #860 from atmire/w2p-71894_metadatafields-byFieldName-search-endpoint
Item edit page metadatafields suggestion & validation
This commit is contained in:
@@ -123,7 +123,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
|||||||
/**
|
/**
|
||||||
* Check if the current page is entirely valid
|
* Check if the current page is entirely valid
|
||||||
*/
|
*/
|
||||||
protected isValid() {
|
public isValid() {
|
||||||
return this.objectUpdatesService.isValidPage(this.url);
|
return this.objectUpdatesService.isValidPage(this.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,14 +5,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="(editable | async)" class="field-container">
|
<div *ngIf="(editable | async)" class="field-container">
|
||||||
<ds-filter-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
<ds-filter-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||||
[(ngModel)]="metadata.key"
|
[(ngModel)]="metadata.key"
|
||||||
|
[url]="this.url"
|
||||||
|
[metadata]="this.metadata"
|
||||||
(submitSuggestion)="update(suggestionControl)"
|
(submitSuggestion)="update(suggestionControl)"
|
||||||
(clickSuggestion)="update(suggestionControl)"
|
(clickSuggestion)="update(suggestionControl)"
|
||||||
(typeSuggestion)="update(suggestionControl)"
|
(typeSuggestion)="update(suggestionControl)"
|
||||||
(dsClickOutside)="checkValidity(suggestionControl)"
|
(dsClickOutside)="checkValidity(suggestionControl)"
|
||||||
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
||||||
#suggestionControl="ngModel"
|
#suggestionControl="ngModel"
|
||||||
[dsInListValidator]="metadataFields"
|
|
||||||
[valid]="(valid | async) !== false"
|
[valid]="(valid | async) !== false"
|
||||||
dsAutoFocus autoFocusSelector=".suggestion_input"
|
dsAutoFocus autoFocusSelector=".suggestion_input"
|
||||||
[ngModelOptions]="{standalone: true}"
|
[ngModelOptions]="{standalone: true}"
|
||||||
@@ -46,12 +47,12 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)"
|
<button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)"
|
||||||
(click)="setEditable(true)" class="btn btn-outline-primary btn-sm"
|
(click)="setEditable(true)" class="btn btn-outline-primary btn-sm"
|
||||||
title="{{'item.edit.metadata.edit.buttons.edit' | translate}}">
|
title="{{'item.edit.metadata.edit.buttons.edit' | translate}}">
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="!(canSetUneditable() | async)" *ngIf="(editable | async)"
|
<button [disabled]="!(canSetUneditable() | async) || (valid | async) === false" *ngIf="(editable | async)"
|
||||||
(click)="setEditable(false)" class="btn btn-outline-success btn-sm"
|
(click)="setEditable(false)" class="btn btn-outline-success btn-sm"
|
||||||
title="{{'item.edit.metadata.edit.buttons.unedit' | translate}}">
|
title="{{'item.edit.metadata.edit.buttons.unedit' | translate}}">
|
||||||
<i class="fas fa-check fa-fw"></i>
|
<i class="fas fa-check fa-fw"></i>
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { getTestScheduler } from 'jasmine-marbles';
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { MetadataFieldDataService } from '../../../../core/data/metadata-field-data.service';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
@@ -14,9 +15,14 @@ import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'
|
|||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
import { SharedModule } from '../../../../shared/shared.module';
|
import {
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../../../shared/remote-data.utils';
|
||||||
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
|
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
|
||||||
|
import { FilterInputSuggestionsComponent } from '../../../../shared/input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
||||||
|
import { MockComponent, MockDirective } from 'ng-mocks';
|
||||||
|
import { DebounceDirective } from '../../../../shared/utils/debounce.directive';
|
||||||
|
|
||||||
let comp: EditInPlaceFieldComponent;
|
let comp: EditInPlaceFieldComponent;
|
||||||
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
||||||
@@ -25,17 +31,21 @@ let el: HTMLElement;
|
|||||||
let metadataFieldService;
|
let metadataFieldService;
|
||||||
let objectUpdatesService;
|
let objectUpdatesService;
|
||||||
let paginatedMetadataFields;
|
let paginatedMetadataFields;
|
||||||
const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' })
|
const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' });
|
||||||
|
const mdSchemaRD$ = createSuccessfulRemoteDataObject$(mdSchema);
|
||||||
const mdField1 = Object.assign(new MetadataField(), {
|
const mdField1 = Object.assign(new MetadataField(), {
|
||||||
schema: mdSchema,
|
schema: mdSchemaRD$,
|
||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'author'
|
qualifier: 'author'
|
||||||
});
|
});
|
||||||
const mdField2 = Object.assign(new MetadataField(), { schema: mdSchema, element: 'title' });
|
const mdField2 = Object.assign(new MetadataField(), {
|
||||||
|
schema: mdSchemaRD$,
|
||||||
|
element: 'title'
|
||||||
|
});
|
||||||
const mdField3 = Object.assign(new MetadataField(), {
|
const mdField3 = Object.assign(new MetadataField(), {
|
||||||
schema: mdSchema,
|
schema: mdSchemaRD$,
|
||||||
element: 'description',
|
element: 'description',
|
||||||
qualifier: 'abstract'
|
qualifier: 'abstract',
|
||||||
});
|
});
|
||||||
|
|
||||||
const metadatum = Object.assign(new MetadatumViewModel(), {
|
const metadatum = Object.assign(new MetadatumViewModel(), {
|
||||||
@@ -74,11 +84,16 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [FormsModule, SharedModule, TranslateModule.forRoot()],
|
imports: [FormsModule, TranslateModule.forRoot()],
|
||||||
declarations: [EditInPlaceFieldComponent],
|
declarations: [
|
||||||
|
EditInPlaceFieldComponent,
|
||||||
|
MockDirective(DebounceDirective),
|
||||||
|
MockComponent(FilterInputSuggestionsComponent)
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: metadataFieldService },
|
{ provide: RegistryService, useValue: metadataFieldService },
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
|
{ provide: MetadataFieldDataService, useValue: {} }
|
||||||
], schemas: [
|
], schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
]
|
]
|
||||||
@@ -94,13 +109,12 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
comp.url = url;
|
comp.url = url;
|
||||||
comp.fieldUpdate = fieldUpdate;
|
comp.fieldUpdate = fieldUpdate;
|
||||||
comp.metadata = metadatum;
|
comp.metadata = metadatum;
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.update();
|
comp.update();
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
||||||
@@ -112,6 +126,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
const editable = false;
|
const editable = false;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.setEditable(editable);
|
comp.setEditable(editable);
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct url and uuid and false', () => {
|
it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct url and uuid and false', () => {
|
||||||
@@ -121,7 +136,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('editable is true', () => {
|
describe('editable is true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('the div should contain input fields or textareas', () => {
|
it('the div should contain input fields or textareas', () => {
|
||||||
@@ -133,7 +148,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('editable is false', () => {
|
describe('editable is false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('the div should contain no input fields or textareas', () => {
|
it('the div should contain no input fields or textareas', () => {
|
||||||
@@ -145,7 +160,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('isValid is true', () => {
|
describe('isValid is true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.valid = observableOf(true);
|
objectUpdatesService.isValid.and.returnValue(observableOf(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('the div should not contain an error message', () => {
|
it('the div should not contain an error message', () => {
|
||||||
@@ -157,10 +172,10 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('isValid is false', () => {
|
describe('isValid is false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.valid = observableOf(false);
|
objectUpdatesService.isValid.and.returnValue(observableOf(false));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('the div should contain no input fields or textareas', () => {
|
it('there should be an error message', () => {
|
||||||
const errorMessages = de.queryAll(By.css('small.text-danger'));
|
const errorMessages = de.queryAll(By.css('small.text-danger'));
|
||||||
expect(errorMessages.length).toBeGreaterThan(0);
|
expect(errorMessages.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
@@ -170,6 +185,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('remove', () => {
|
describe('remove', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.remove();
|
comp.remove();
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
||||||
@@ -180,6 +196,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('removeChangesFromField', () => {
|
describe('removeChangesFromField', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.removeChangesFromField();
|
comp.removeChangesFromField();
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call removeChangesFromField on the objectUpdatesService with the correct url and uuid', () => {
|
it('it should call removeChangesFromField on the objectUpdatesService with the correct url and uuid', () => {
|
||||||
@@ -192,19 +209,19 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
const metadataFieldSuggestions: InputSuggestion[] =
|
const metadataFieldSuggestions: InputSuggestion[] =
|
||||||
[
|
[
|
||||||
{ displayValue: mdField1.toString().split('.').join('.​'), value: mdField1.toString() },
|
{ displayValue: ('dc.' + mdField1.toString()).split('.').join('.​'), value: ('dc.' + mdField1.toString()) },
|
||||||
{ displayValue: mdField2.toString().split('.').join('.​'), value: mdField2.toString() },
|
{ displayValue: ('dc.' + mdField2.toString()).split('.').join('.​'), value: ('dc.' + mdField2.toString()) },
|
||||||
{ displayValue: mdField3.toString().split('.').join('.​'), value: mdField3.toString() }
|
{ displayValue: ('dc.' + mdField3.toString()).split('.').join('.​'), value: ('dc.' + mdField3.toString()) }
|
||||||
];
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
comp.findMetadataFieldSuggestions(query);
|
comp.findMetadataFieldSuggestions(query);
|
||||||
|
tick();
|
||||||
});
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => {
|
it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => {
|
||||||
|
expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, followLink('schema'));
|
||||||
expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should set metadataFieldSuggestions to the right value', () => {
|
it('it should set metadataFieldSuggestions to the right value', () => {
|
||||||
@@ -216,7 +233,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('canSetEditable', () => {
|
describe('canSetEditable', () => {
|
||||||
describe('when editable is currently true', () => {
|
describe('when editable is currently true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canSetEditable should return an observable emitting false', () => {
|
it('canSetEditable should return an observable emitting false', () => {
|
||||||
@@ -227,12 +245,14 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when editable is currently false', () => {
|
describe('when editable is currently false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the fieldUpdate\'s changeType is currently not REMOVE', () => {
|
describe('when the fieldUpdate\'s changeType is currently not REMOVE', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('canSetEditable should return an observable emitting true', () => {
|
it('canSetEditable should return an observable emitting true', () => {
|
||||||
const expected = '(a|)';
|
const expected = '(a|)';
|
||||||
@@ -243,6 +263,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently REMOVE', () => {
|
describe('when the fieldUpdate\'s changeType is currently REMOVE', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.REMOVE;
|
comp.fieldUpdate.changeType = FieldChangeType.REMOVE;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('canSetEditable should return an observable emitting false', () => {
|
it('canSetEditable should return an observable emitting false', () => {
|
||||||
const expected = '(a|)';
|
const expected = '(a|)';
|
||||||
@@ -255,7 +276,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('canSetUneditable', () => {
|
describe('canSetUneditable', () => {
|
||||||
describe('when editable is currently true', () => {
|
describe('when editable is currently true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canSetUneditable should return an observable emitting true', () => {
|
it('canSetUneditable should return an observable emitting true', () => {
|
||||||
@@ -266,7 +288,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when editable is currently false', () => {
|
describe('when editable is currently false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canSetUneditable should return an observable emitting false', () => {
|
it('canSetUneditable should return an observable emitting false', () => {
|
||||||
@@ -278,7 +301,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when canSetEditable emits true', () => {
|
describe('when canSetEditable emits true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(true));
|
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -290,7 +313,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when canSetEditable emits false', () => {
|
describe('when canSetEditable emits false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(false));
|
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(false));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -302,7 +325,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when canSetUneditable emits true', () => {
|
describe('when canSetUneditable emits true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(true));
|
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -314,7 +337,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when canSetUneditable emits false', () => {
|
describe('when canSetUneditable emits false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(false));
|
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(false));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -372,6 +395,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently not REMOVE or ADD', () => {
|
describe('when the fieldUpdate\'s changeType is currently not REMOVE or ADD', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.UPDATE;
|
comp.fieldUpdate.changeType = FieldChangeType.UPDATE;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('canRemove should return an observable emitting true', () => {
|
it('canRemove should return an observable emitting true', () => {
|
||||||
const expected = '(a|)';
|
const expected = '(a|)';
|
||||||
@@ -382,6 +406,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently ADD', () => {
|
describe('when the fieldUpdate\'s changeType is currently ADD', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('canRemove should return an observable emitting false', () => {
|
it('canRemove should return an observable emitting false', () => {
|
||||||
const expected = '(a|)';
|
const expected = '(a|)';
|
||||||
@@ -394,7 +419,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when editable is currently true', () => {
|
describe('when editable is currently true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
comp.fieldUpdate.changeType = undefined;
|
comp.fieldUpdate.changeType = undefined;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -408,6 +433,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently ADD, UPDATE or REMOVE', () => {
|
describe('when the fieldUpdate\'s changeType is currently ADD, UPDATE or REMOVE', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canUndo should return an observable emitting true', () => {
|
it('canUndo should return an observable emitting true', () => {
|
||||||
@@ -419,6 +445,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently undefined', () => {
|
describe('when the fieldUpdate\'s changeType is currently undefined', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = undefined;
|
comp.fieldUpdate.changeType = undefined;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canUndo should return an observable emitting false', () => {
|
it('canUndo should return an observable emitting false', () => {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||||
|
import { metadataFieldsToString } from '../../../../core/shared/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
@@ -9,8 +10,8 @@ import { FieldUpdate } from '../../../../core/data/object-updates/object-updates
|
|||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { NgModel } from '@angular/forms';
|
import { NgModel } from '@angular/forms';
|
||||||
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
||||||
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
// tslint:disable-next-line:component-selector
|
// tslint:disable-next-line:component-selector
|
||||||
@@ -32,15 +33,10 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
@Input() url: string;
|
@Input() url: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* List of strings with all metadata field keys available
|
|
||||||
*/
|
|
||||||
@Input() metadataFields: string[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadatum of this field
|
* The metadatum of this field
|
||||||
*/
|
*/
|
||||||
metadata: MetadatumViewModel;
|
@Input() metadata: MetadatumViewModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits whether or not this field is currently editable
|
* Emits whether or not this field is currently editable
|
||||||
@@ -126,27 +122,34 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
* Ignores fields from metadata schemas "relation" and "relationship"
|
* Ignores fields from metadata schemas "relation" and "relationship"
|
||||||
* @param query The query to look for
|
* @param query The query to look for
|
||||||
*/
|
*/
|
||||||
findMetadataFieldSuggestions(query: string): void {
|
findMetadataFieldSuggestions(query: string) {
|
||||||
if (isNotEmpty(query)) {
|
if (isNotEmpty(query)) {
|
||||||
this.registryService.queryMetadataFields(query).pipe(
|
return this.registryService.queryMetadataFields(query, null, followLink('schema')).pipe(
|
||||||
// getSucceededRemoteData(),
|
metadataFieldsToString(),
|
||||||
take(1),
|
take(1))
|
||||||
map((data) => data.payload.page)
|
.subscribe((fieldNames: string[]) => {
|
||||||
).subscribe(
|
this.setInputSuggestions(fieldNames);
|
||||||
(fields: MetadataField[]) => this.metadataFieldSuggestions.next(
|
})
|
||||||
fields.map((field: MetadataField) => {
|
|
||||||
return {
|
|
||||||
displayValue: field.toString().split('.').join('.​'),
|
|
||||||
value: field.toString()
|
|
||||||
};
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
this.metadataFieldSuggestions.next([]);
|
this.metadataFieldSuggestions.next([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the list of input suggestion with the given Metadata fields, which all require a resolved MetadataSchema
|
||||||
|
* @param fields list of Metadata fields, which all require a resolved MetadataSchema
|
||||||
|
*/
|
||||||
|
setInputSuggestions(fields: string[]) {
|
||||||
|
this.metadataFieldSuggestions.next(
|
||||||
|
fields.map((fieldName: string) => {
|
||||||
|
return {
|
||||||
|
displayValue: fieldName.split('.').join('.​'),
|
||||||
|
value: fieldName
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a user should be allowed to edit this field
|
* Check if a user should be allowed to edit this field
|
||||||
* @return an observable that emits true when the user should be able to edit this field and false when they should not
|
* @return an observable that emits true when the user should be able to edit this field and false when they should not
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
|
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
||||||
ds-edit-in-place-field
|
ds-edit-in-place-field
|
||||||
[fieldUpdate]="updateValue || {}"
|
[fieldUpdate]="updateValue || {}"
|
||||||
[metadataFields]="metadataFields$ | async"
|
|
||||||
[url]="url"
|
[url]="url"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'table-warning': updateValue.changeType === 0,
|
'table-warning': updateValue.changeType === 0,
|
||||||
|
@@ -6,16 +6,14 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Identifiable } from '../../../core/data/object-updates/object-updates.reducer';
|
import { Identifiable } from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { first, map, switchMap, take, tap } from 'rxjs/operators';
|
import { first, switchMap, tap } from 'rxjs/operators';
|
||||||
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
|
||||||
import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models';
|
import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models';
|
||||||
import { Metadata } from '../../../core/shared/metadata.utils';
|
import { Metadata } from '../../../core/shared/metadata.utils';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
|
||||||
import { UpdateDataService } from '../../../core/data/update-data.service';
|
import { UpdateDataService } from '../../../core/data/update-data.service';
|
||||||
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||||
@@ -42,11 +40,6 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() updateService: UpdateDataService<Item>;
|
@Input() updateService: UpdateDataService<Item>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Observable with a list of strings with all existing metadata field keys
|
|
||||||
*/
|
|
||||||
metadataFields$: Observable<string[]>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public itemService: ItemDataService,
|
public itemService: ItemDataService,
|
||||||
public objectUpdatesService: ObjectUpdatesService,
|
public objectUpdatesService: ObjectUpdatesService,
|
||||||
@@ -54,7 +47,6 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
public notificationsService: NotificationsService,
|
public notificationsService: NotificationsService,
|
||||||
public translateService: TranslateService,
|
public translateService: TranslateService,
|
||||||
public route: ActivatedRoute,
|
public route: ActivatedRoute,
|
||||||
public metadataFieldService: RegistryService,
|
|
||||||
) {
|
) {
|
||||||
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
||||||
}
|
}
|
||||||
@@ -64,7 +56,6 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.metadataFields$ = this.findMetadataFields();
|
|
||||||
if (hasNoValue(this.updateService)) {
|
if (hasNoValue(this.updateService)) {
|
||||||
this.updateService = this.itemService;
|
this.updateService = this.itemService;
|
||||||
}
|
}
|
||||||
@@ -130,16 +121,6 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to request all metadata fields and convert them to a list of strings
|
|
||||||
*/
|
|
||||||
findMetadataFields(): Observable<string[]> {
|
|
||||||
return this.metadataFieldService.getAllMetadataFields().pipe(
|
|
||||||
getSucceededRemoteData(),
|
|
||||||
take(1),
|
|
||||||
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for empty metadata UUIDs and fix them (empty UUIDs would break the object-update service)
|
* Check for empty metadata UUIDs and fix them (empty UUIDs would break the object-update service)
|
||||||
*/
|
*/
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -27,6 +30,7 @@ import { RequestParam } from '../cache/models/request-param.model';
|
|||||||
export class MetadataFieldDataService extends DataService<MetadataField> {
|
export class MetadataFieldDataService extends DataService<MetadataField> {
|
||||||
protected linkPath = 'metadatafields';
|
protected linkPath = 'metadatafields';
|
||||||
protected searchBySchemaLinkPath = 'bySchema';
|
protected searchBySchemaLinkPath = 'bySchema';
|
||||||
|
protected searchByFieldNameLinkPath = 'byFieldName';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -53,6 +57,43 @@ export class MetadataFieldDataService extends DataService<MetadataField> {
|
|||||||
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
|
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find metadata fields with either the partial metadata field name (e.g. "dc.ti") as query or an exact match to
|
||||||
|
* at least the schema, element or qualifier
|
||||||
|
* @param schema optional; an exact match of the prefix of the metadata schema (e.g. "dc", "dcterms", "eperson")
|
||||||
|
* @param element optional; an exact match of the field's element (e.g. "contributor", "title")
|
||||||
|
* @param qualifier optional; an exact match of the field's qualifier (e.g. "author", "alternative")
|
||||||
|
* @param query optional (if any of schema, element or qualifier used) - part of the fully qualified field,
|
||||||
|
* should start with the start of the schema, element or qualifier (e.g. “dc.ti”, “contributor”, “auth”, “contributor.ot”)
|
||||||
|
* @param exactName optional; the exact fully qualified field, should use the syntax schema.element.qualifier or
|
||||||
|
* schema.element if no qualifier exists (e.g. "dc.title", "dc.contributor.author"). It will only return one value
|
||||||
|
* if there's an exact match
|
||||||
|
* @param options The options info used to retrieve the fields
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
searchByFieldNameParams(schema: string, element: string, qualifier: string, query: string, exactName: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
|
const optionParams = Object.assign(new FindListOptions(), options, {
|
||||||
|
searchParams: [
|
||||||
|
new RequestParam('schema', hasValue(schema) ? schema : ''),
|
||||||
|
new RequestParam('element', hasValue(element) ? element : ''),
|
||||||
|
new RequestParam('qualifier', hasValue(qualifier) ? qualifier : ''),
|
||||||
|
new RequestParam('query', hasValue(query) ? query : ''),
|
||||||
|
new RequestParam('exactName', hasValue(exactName) ? exactName : '')
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return this.searchBy(this.searchByFieldNameLinkPath, optionParams, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a specific metadata field by name.
|
||||||
|
* @param exactFieldName The exact fully qualified field, should use the syntax schema.element.qualifier or
|
||||||
|
* schema.element if no qualifier exists (e.g. "dc.title", "dc.contributor.author"). It will only return one value
|
||||||
|
* if there's an exact match, empty list if there is no exact match.
|
||||||
|
*/
|
||||||
|
findByExactFieldName(exactFieldName: string): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
|
return this.searchByFieldNameParams(null, null, null, null, exactFieldName, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all metadata field requests
|
* Clear all metadata field requests
|
||||||
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
||||||
|
@@ -68,8 +68,8 @@ export class MetadataField extends ListableObject implements HALResource {
|
|||||||
schema?: Observable<RemoteData<MetadataSchema>>;
|
schema?: Observable<RemoteData<MetadataSchema>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to print this metadata field as a string
|
* Method to print this metadata field as a string without the schema
|
||||||
* @param separator The separator between the schema, element and qualifier in the string
|
* @param separator The separator between element and qualifier in the string
|
||||||
*/
|
*/
|
||||||
toString(separator: string = '.'): string {
|
toString(separator: string = '.'): string {
|
||||||
let key = this.element;
|
let key = this.element;
|
||||||
|
@@ -30,7 +30,6 @@ import { MetadataSchemaDataService } from '../data/metadata-schema-data.service'
|
|||||||
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
|
||||||
|
|
||||||
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
||||||
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
||||||
@@ -90,20 +89,6 @@ export class RegistryService {
|
|||||||
return this.metadataFieldService.findBySchema(schema, options, ...linksToFollow);
|
return this.metadataFieldService.findBySchema(schema, options, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve all existing metadata fields as a paginated list
|
|
||||||
* @param options Options to determine which page of metadata fields should be requested
|
|
||||||
* When no options are provided, all metadata fields are requested in one large page
|
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
|
||||||
* @returns an observable that emits a remote data object with a page of metadata fields
|
|
||||||
*/
|
|
||||||
// TODO this is temporarily disabled. The performance is too bad.
|
|
||||||
// It is used down the line for validation. That validation will have to be rewritten against a new rest endpoint.
|
|
||||||
// Not by downloading the list of all fields.
|
|
||||||
public getAllMetadataFields(options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList<MetadataField>(null, []));
|
|
||||||
}
|
|
||||||
|
|
||||||
public editMetadataSchema(schema: MetadataSchema) {
|
public editMetadataSchema(schema: MetadataSchema) {
|
||||||
this.store.dispatch(new MetadataRegistryEditSchemaAction(schema));
|
this.store.dispatch(new MetadataRegistryEditSchemaAction(schema));
|
||||||
}
|
}
|
||||||
@@ -151,6 +136,7 @@ export class RegistryService {
|
|||||||
public getSelectedMetadataSchemas(): Observable<MetadataSchema[]> {
|
public getSelectedMetadataSchemas(): Observable<MetadataSchema[]> {
|
||||||
return this.store.pipe(select(selectedMetadataSchemasSelector));
|
return this.store.pipe(select(selectedMetadataSchemasSelector));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to start editing a metadata field, dispatches an edit field action
|
* Method to start editing a metadata field, dispatches an edit field action
|
||||||
* @param field The field that's being edited
|
* @param field The field that's being edited
|
||||||
@@ -165,12 +151,14 @@ export class RegistryService {
|
|||||||
public cancelEditMetadataField() {
|
public cancelEditMetadataField() {
|
||||||
this.store.dispatch(new MetadataRegistryCancelFieldAction());
|
this.store.dispatch(new MetadataRegistryCancelFieldAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to retrieve the metadata field that are currently being edited
|
* Method to retrieve the metadata field that are currently being edited
|
||||||
*/
|
*/
|
||||||
public getActiveMetadataField(): Observable<MetadataField> {
|
public getActiveMetadataField(): Observable<MetadataField> {
|
||||||
return this.store.pipe(select(editMetadataFieldSelector));
|
return this.store.pipe(select(editMetadataFieldSelector));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to select a metadata field, dispatches a select field action
|
* Method to select a metadata field, dispatches a select field action
|
||||||
* @param field The field that's being selected
|
* @param field The field that's being selected
|
||||||
@@ -178,6 +166,7 @@ export class RegistryService {
|
|||||||
public selectMetadataField(field: MetadataField) {
|
public selectMetadataField(field: MetadataField) {
|
||||||
this.store.dispatch(new MetadataRegistrySelectFieldAction(field));
|
this.store.dispatch(new MetadataRegistrySelectFieldAction(field));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to deselect a metadata field, dispatches a deselect field action
|
* Method to deselect a metadata field, dispatches a deselect field action
|
||||||
* @param field The field that's it being deselected
|
* @param field The field that's it being deselected
|
||||||
@@ -185,6 +174,7 @@ export class RegistryService {
|
|||||||
public deselectMetadataField(field: MetadataField) {
|
public deselectMetadataField(field: MetadataField) {
|
||||||
this.store.dispatch(new MetadataRegistryDeselectFieldAction(field));
|
this.store.dispatch(new MetadataRegistryDeselectFieldAction(field));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to deselect all currently selected metadata fields, dispatches a deselect all field action
|
* Method to deselect all currently selected metadata fields, dispatches a deselect all field action
|
||||||
*/
|
*/
|
||||||
@@ -213,7 +203,7 @@ export class RegistryService {
|
|||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.showNotifications(true, isUpdate, false, {prefix: schema.prefix});
|
this.showNotifications(true, isUpdate, false, { prefix: schema.prefix });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -244,7 +234,7 @@ export class RegistryService {
|
|||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.showNotifications(true, false, true, {field: field.toString()});
|
this.showNotifications(true, false, true, { field: field.toString() });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -259,7 +249,7 @@ export class RegistryService {
|
|||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.showNotifications(true, true, true, {field: field.toString()});
|
this.showNotifications(true, true, true, { field: field.toString() });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -271,6 +261,7 @@ export class RegistryService {
|
|||||||
public deleteMetadataField(id: number): Observable<RestResponse> {
|
public deleteMetadataField(id: number): Observable<RestResponse> {
|
||||||
return this.metadataFieldService.delete(`${id}`);
|
return this.metadataFieldService.delete(`${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that clears a cached metadata field request and returns its REST url
|
* Method that clears a cached metadata field request and returns its REST url
|
||||||
*/
|
*/
|
||||||
@@ -297,13 +288,11 @@ export class RegistryService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a filtered paginated list of metadata fields
|
* Retrieve a filtered paginated list of metadata fields
|
||||||
* @param query {string} The query to filter the field names by
|
* @param query {string} The query to use for the metadata field name, can be part of the fully qualified field,
|
||||||
|
* should start with the start of the schema, element or qualifier (e.g. “dc.ti”, “contributor”, “auth”, “contributor.ot”)
|
||||||
* @returns an observable that emits a remote data object with a page of metadata fields that match the query
|
* @returns an observable that emits a remote data object with a page of metadata fields that match the query
|
||||||
*/
|
*/
|
||||||
// TODO this is temporarily disabled. The performance is too bad.
|
queryMetadataFields(query: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
// Querying metadatafields will need to be implemented as a search endpoint on the rest api,
|
return this.metadataFieldService.searchByFieldNameParams(null, null, null, query, null, options, ...linksToFollow);
|
||||||
// not by downloading everything and preforming the query client side.
|
|
||||||
queryMetadataFields(query: string): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList<MetadataField>(null, []));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Router, UrlTree } from '@angular/router';
|
import { Router, UrlTree } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { filter, find, flatMap, map, take, tap } from 'rxjs/operators';
|
import { filter, find, flatMap, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { SearchResult } from '../../shared/search/search-result.model';
|
import { SearchResult } from '../../shared/search/search-result.model';
|
||||||
import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
|
import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
@@ -9,6 +9,8 @@ import { RemoteData } from '../data/remote-data';
|
|||||||
import { RestRequest } from '../data/request.models';
|
import { RestRequest } from '../data/request.models';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { BrowseDefinition } from './browse-definition.model';
|
import { BrowseDefinition } from './browse-definition.model';
|
||||||
import { DSpaceObject } from './dspace-object.model';
|
import { DSpaceObject } from './dspace-object.model';
|
||||||
import { getUnauthorizedRoute } from '../../app-routing-paths';
|
import { getUnauthorizedRoute } from '../../app-routing-paths';
|
||||||
@@ -265,3 +267,27 @@ export const paginatedListToArray = () =>
|
|||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((objectRD: RemoteData<PaginatedList<T>>) => objectRD.payload.page.filter((object: T) => hasValue(object)))
|
map((objectRD: RemoteData<PaginatedList<T>>) => objectRD.payload.page.filter((object: T) => hasValue(object)))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a list of metadata fields into an array of string representing their schema.element.qualifier string
|
||||||
|
*/
|
||||||
|
export const metadataFieldsToString = () =>
|
||||||
|
(source: Observable<RemoteData<PaginatedList<MetadataField>>>): Observable<string[]> =>
|
||||||
|
source.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
map((fieldRD: RemoteData<PaginatedList<MetadataField>>) => {
|
||||||
|
return fieldRD.payload.page.filter((object: MetadataField) => hasValue(object))
|
||||||
|
}),
|
||||||
|
switchMap((fields: MetadataField[]) => {
|
||||||
|
const fieldSchemaArray = fields.map((field: MetadataField) => {
|
||||||
|
return field.schema.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((schema: MetadataSchema) => ({ field, schema }))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return observableCombineLatest(fieldSchemaArray);
|
||||||
|
}),
|
||||||
|
map((fieldSchemaArray: Array<{ field: MetadataField, schema: MetadataSchema }>): string[] => {
|
||||||
|
return fieldSchemaArray.map((fieldSchema: { field: MetadataField, schema: MetadataSchema }) => fieldSchema.schema.prefix + '.' + fieldSchema.field.toString())
|
||||||
|
})
|
||||||
|
);
|
||||||
|
@@ -1,22 +1,24 @@
|
|||||||
<form #form="ngForm" (ngSubmit)="onSubmit(value)"
|
<form [formGroup]="form" (ngSubmit)="onSubmit(value)"
|
||||||
[action]="action" (keydown)="onKeydown($event)"
|
[action]="action" (keydown)="onKeydown($event)"
|
||||||
(keydown.arrowdown)="shiftFocusDown($event)"
|
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||||
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||||
(dsClickOutside)="close();">
|
(dsClickOutside)="checkIfValidInput(form);close();">
|
||||||
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
<input #inputField type="text" formControlName="metadataNameField" [(ngModel)]="value" id="name" [name]="name"
|
||||||
class="form-control suggestion_input"
|
class="form-control suggestion_input"
|
||||||
[ngClass]="{'is-invalid': !valid}"
|
[ngClass]="{'is-invalid': !valid}"
|
||||||
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
ng-model-options="{standalone: true}"
|
||||||
<input type="submit" class="d-none"/>
|
autocomplete="off">
|
||||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
<input type="submit" class="d-none"/>
|
||||||
<div class="dropdown-list">
|
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||||
<div *ngFor="let suggestionOption of suggestions">
|
<div class="dropdown-list">
|
||||||
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
<div *ngFor="let suggestionOption of suggestions">
|
||||||
<span [innerHTML]="suggestionOption.displayValue"></span>
|
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
||||||
</a>
|
<span [innerHTML]="suggestionOption.displayValue"></span>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@@ -3,9 +3,11 @@ import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angula
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { MetadataFieldDataService } from '../../../core/data/metadata-field-data.service';
|
||||||
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { FilterInputSuggestionsComponent } from './filter-input-suggestions.component';
|
import { FilterInputSuggestionsComponent } from './filter-input-suggestions.component';
|
||||||
|
|
||||||
describe('FilterInputSuggestionsComponent', () => {
|
describe('FilterInputSuggestionsComponent', () => {
|
||||||
@@ -14,19 +16,23 @@ describe('FilterInputSuggestionsComponent', () => {
|
|||||||
let fixture: ComponentFixture<FilterInputSuggestionsComponent>;
|
let fixture: ComponentFixture<FilterInputSuggestionsComponent>;
|
||||||
let de: DebugElement;
|
let de: DebugElement;
|
||||||
let el: HTMLElement;
|
let el: HTMLElement;
|
||||||
const suggestions = [{displayValue: 'suggestion uno', value: 'suggestion uno'}, {
|
const suggestions = [{ displayValue: 'suggestion uno', value: 'suggestion uno' }, {
|
||||||
displayValue: 'suggestion dos',
|
displayValue: 'suggestion dos',
|
||||||
value: 'suggestion dos'
|
value: 'suggestion dos'
|
||||||
}, {displayValue: 'suggestion tres', value: 'suggestion tres'}];
|
}, { displayValue: 'suggestion tres', value: 'suggestion tres' }];
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule, ReactiveFormsModule],
|
||||||
declarations: [FilterInputSuggestionsComponent],
|
declarations: [FilterInputSuggestionsComponent],
|
||||||
providers: [],
|
providers: [FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
{ provide: MetadataFieldDataService, useValue: {} },
|
||||||
|
{ provide: ObjectUpdatesService, useValue: {} },
|
||||||
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(FilterInputSuggestionsComponent, {
|
}).overrideComponent(FilterInputSuggestionsComponent, {
|
||||||
set: {changeDetection: ChangeDetectionStrategy.Default}
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
import { Component, forwardRef, Input } from '@angular/core';
|
import { Component, forwardRef, Input, OnInit } from '@angular/core';
|
||||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { FormControl, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
|
||||||
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { MetadatumViewModel } from '../../../core/shared/metadata.models';
|
||||||
|
import { MetadataFieldValidator } from '../../utils/metadatafield-validator.directive';
|
||||||
import { InputSuggestionsComponent } from '../input-suggestions.component';
|
import { InputSuggestionsComponent } from '../input-suggestions.component';
|
||||||
import { InputSuggestion } from '../input-suggestions.model';
|
import { InputSuggestion } from '../input-suggestions.model';
|
||||||
|
|
||||||
@@ -21,12 +24,39 @@ import { InputSuggestion } from '../input-suggestions.model';
|
|||||||
/**
|
/**
|
||||||
* Component representing a form with a autocomplete functionality
|
* Component representing a form with a autocomplete functionality
|
||||||
*/
|
*/
|
||||||
export class FilterInputSuggestionsComponent extends InputSuggestionsComponent {
|
export class FilterInputSuggestionsComponent extends InputSuggestionsComponent implements OnInit {
|
||||||
|
|
||||||
|
form: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current url of this page
|
||||||
|
*/
|
||||||
|
@Input() url: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadatum of this field
|
||||||
|
*/
|
||||||
|
@Input() metadata: MetadatumViewModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The suggestions that should be shown
|
* The suggestions that should be shown
|
||||||
*/
|
*/
|
||||||
@Input() suggestions: InputSuggestion[] = [];
|
@Input() suggestions: InputSuggestion[] = [];
|
||||||
|
|
||||||
|
constructor(private metadataFieldValidator: MetadataFieldValidator,
|
||||||
|
private objectUpdatesService: ObjectUpdatesService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.form = new FormGroup({
|
||||||
|
metadataNameField: new FormControl(this._value, {
|
||||||
|
asyncValidators: [this.metadataFieldValidator.validate.bind(this.metadataFieldValidator)],
|
||||||
|
validators: [Validators.required]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onSubmit(data) {
|
onSubmit(data) {
|
||||||
this.value = data;
|
this.value = data;
|
||||||
this.submitSuggestion.emit(data);
|
this.submitSuggestion.emit(data);
|
||||||
@@ -41,4 +71,14 @@ export class FilterInputSuggestionsComponent extends InputSuggestionsComponent {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the input is valid according to validator and send (in)valid state to store
|
||||||
|
* @param form Form with input
|
||||||
|
*/
|
||||||
|
checkIfValidInput(form) {
|
||||||
|
this.valid = !(form.get('metadataNameField').status === 'INVALID' && (form.get('metadataNameField').dirty || form.get('metadataNameField').touched));
|
||||||
|
this.objectUpdatesService.setValidFieldUpdate(this.url, this.metadata.uuid, this.valid);
|
||||||
|
return this.valid;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -15,5 +15,6 @@ form {
|
|||||||
position: relative;
|
position: relative;
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,7 @@ import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/fil
|
|||||||
import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component';
|
import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component';
|
||||||
import { EnumKeysPipe } from './utils/enum-keys-pipe';
|
import { EnumKeysPipe } from './utils/enum-keys-pipe';
|
||||||
import { FileSizePipe } from './utils/file-size-pipe';
|
import { FileSizePipe } from './utils/file-size-pipe';
|
||||||
|
import { MetadataFieldValidator } from './utils/metadatafield-validator.directive';
|
||||||
import { SafeUrlPipe } from './utils/safe-url-pipe';
|
import { SafeUrlPipe } from './utils/safe-url-pipe';
|
||||||
import { ConsolePipe } from './utils/console.pipe';
|
import { ConsolePipe } from './utils/console.pipe';
|
||||||
import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component';
|
import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component';
|
||||||
@@ -517,7 +518,8 @@ const DIRECTIVES = [
|
|||||||
FileValueAccessorDirective,
|
FileValueAccessorDirective,
|
||||||
FileValidator,
|
FileValidator,
|
||||||
ClaimedTaskActionsDirective,
|
ClaimedTaskActionsDirective,
|
||||||
NgForTrackByIdDirective
|
NgForTrackByIdDirective,
|
||||||
|
MetadataFieldValidator
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
62
src/app/shared/utils/metadatafield-validator.directive.ts
Normal file
62
src/app/shared/utils/metadatafield-validator.directive.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Directive, Injectable } from '@angular/core';
|
||||||
|
import { AbstractControl, AsyncValidator, NG_VALIDATORS, ValidationErrors } from '@angular/forms';
|
||||||
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { of as observableOf, timer as observableTimer, Observable } from 'rxjs';
|
||||||
|
import { MetadataFieldDataService } from '../../core/data/metadata-field-data.service';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { MetadataField } from '../../core/metadata/metadata-field.model';
|
||||||
|
import { getSucceededRemoteData } from '../../core/shared/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive for validating if a ngModel value is a valid metadata field
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: '[ngModel][dsMetadataFieldValidator]',
|
||||||
|
// We add our directive to the list of existing validators
|
||||||
|
providers: [
|
||||||
|
{ provide: NG_VALIDATORS, useExisting: MetadataFieldValidator, multi: true }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class MetadataFieldValidator implements AsyncValidator {
|
||||||
|
|
||||||
|
constructor(private metadataFieldService: MetadataFieldDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function that checks if the form control's value is currently valid
|
||||||
|
* @param control The FormControl
|
||||||
|
*/
|
||||||
|
validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
|
||||||
|
const resTimer = observableTimer(500).pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
if (!control.value) {
|
||||||
|
return observableOf({ invalidMetadataField: { value: control.value } });
|
||||||
|
}
|
||||||
|
const mdFieldNameParts = control.value.split('.');
|
||||||
|
if (mdFieldNameParts.length < 2) {
|
||||||
|
return observableOf({ invalidMetadataField: { value: control.value } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = this.metadataFieldService.findByExactFieldName(control.value)
|
||||||
|
.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
map((matchingFieldRD: RemoteData<PaginatedList<MetadataField>>) => {
|
||||||
|
if (matchingFieldRD.payload.pageInfo.totalElements === 0) {
|
||||||
|
return { invalidMetadataField: { value: control.value } };
|
||||||
|
} else if (matchingFieldRD.payload.pageInfo.totalElements === 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
res.pipe(take(1)).subscribe();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
resTimer.pipe(take(1)).subscribe();
|
||||||
|
return resTimer;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user