mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge pull request #3722 from alexandrevryghem/w2p-119915_made-edit-metadata-tab-fields-dynamic_contribute-main
Made edit metadata tab fields dynamic and added entity type support
This commit is contained in:
@@ -44,4 +44,10 @@ export enum Context {
|
|||||||
Bitstream = 'bitstream',
|
Bitstream = 'bitstream',
|
||||||
|
|
||||||
CoarNotify = 'coarNotify',
|
CoarNotify = 'coarNotify',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Edit Metadata field Context values that are used in the Edit Item Metadata tab.
|
||||||
|
*/
|
||||||
|
AddMetadata = 'addMetadata',
|
||||||
|
EditMetadata = 'editMetadata',
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
@for (mdValue of form.fields[mdField]; track mdValue; let idx = $index) {
|
@for (mdValue of form.fields[mdField]; track mdValue; let idx = $index) {
|
||||||
<ds-dso-edit-metadata-value role="presentation"
|
<ds-dso-edit-metadata-value role="presentation"
|
||||||
[dso]="dso"
|
[dso]="dso"
|
||||||
|
[context]="Context.EditMetadata"
|
||||||
[mdValue]="mdValue"
|
[mdValue]="mdValue"
|
||||||
[mdField]="mdField"
|
[mdField]="mdField"
|
||||||
[dsoType]="dsoType"
|
[dsoType]="dsoType"
|
||||||
|
@@ -15,6 +15,7 @@ import {
|
|||||||
Observable,
|
Observable,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { Context } from '../../../core/shared/context.model';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import {
|
import {
|
||||||
DsoEditMetadataChangeType,
|
DsoEditMetadataChangeType,
|
||||||
@@ -78,6 +79,8 @@ export class DsoEditMetadataFieldValuesComponent {
|
|||||||
*/
|
*/
|
||||||
public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType;
|
public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType;
|
||||||
|
|
||||||
|
public readonly Context = Context;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop a value into a new position
|
* Drop a value into a new position
|
||||||
* Update the form's value array for the current field to match the dropped position
|
* Update the form's value array for the current field to match the dropped position
|
||||||
|
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { Context } from '../../../core/shared/context.model';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { DsoEditMetadataValue } from '../dso-edit-metadata-form';
|
||||||
|
import { EditMetadataValueFieldType } from './dso-edit-metadata-field-type.enum';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base component for editing metadata fields.
|
||||||
|
*
|
||||||
|
* This abstract component is only designed to contain the common `@Input()` & `@Output()` fields, that the
|
||||||
|
* {@link DsoEditMetadataValueFieldLoaderComponent} passes to its dynamically generated components. This class should
|
||||||
|
* not contain any methods or any other type of logic. Such logic should instead be created in
|
||||||
|
* {@link DsoEditMetadataFieldService}.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-abstract-dso-edit-metadata-value-field',
|
||||||
|
template: '',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export abstract class AbstractDsoEditMetadataValueFieldComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional context
|
||||||
|
*/
|
||||||
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link DSpaceObject}
|
||||||
|
*/
|
||||||
|
@Input() dso: DSpaceObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the DSO, used to determines i18n messages
|
||||||
|
*/
|
||||||
|
@Input() dsoType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the field
|
||||||
|
*/
|
||||||
|
@Input() type: EditMetadataValueFieldType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata field
|
||||||
|
*/
|
||||||
|
@Input() mdField: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editable metadata value to show
|
||||||
|
*/
|
||||||
|
@Input() mdValue: DsoEditMetadataValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the user clicked confirm
|
||||||
|
*/
|
||||||
|
@Output() confirm: EventEmitter<boolean> = new EventEmitter();
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,61 @@
|
|||||||
|
@if (mdValue.editing) {
|
||||||
|
@if ((isAuthorityControlled$ | async) !== true || (enabledFreeTextEditing && (isSuggesterVocabulary$ | async) !== true)) {
|
||||||
|
<textarea class="form-control" rows="5" [(ngModel)]="mdValue.newValue.value"
|
||||||
|
[attr.aria-label]="(dsoType + '.edit.metadata.edit.value') | translate"
|
||||||
|
[dsDebounce]="300" (onDebounce)="confirm.emit(false)">
|
||||||
|
</textarea>
|
||||||
|
}
|
||||||
|
@if ((isScrollableVocabulary$ | async) && !enabledFreeTextEditing) {
|
||||||
|
<ds-dynamic-scrollable-dropdown [bindId]="mdField"
|
||||||
|
[group]="group"
|
||||||
|
[model]="getModel()"
|
||||||
|
(change)="onChangeAuthorityField($event)">
|
||||||
|
</ds-dynamic-scrollable-dropdown>
|
||||||
|
}
|
||||||
|
@if (((isHierarchicalVocabulary$ | async) && !enabledFreeTextEditing) || (isSuggesterVocabulary$ | async)) {
|
||||||
|
<ds-dynamic-onebox
|
||||||
|
[group]="group"
|
||||||
|
[model]="getModel()"
|
||||||
|
(change)="onChangeAuthorityField($event)">
|
||||||
|
</ds-dynamic-onebox>
|
||||||
|
}
|
||||||
|
@if ((isHierarchicalVocabulary$ | async) || (isScrollableVocabulary$ | async)) {
|
||||||
|
<button class="btn btn-secondary w-100 mt-2"
|
||||||
|
[title]="enabledFreeTextEditing ? dsoType + '.edit.metadata.edit.buttons.disable-free-text-editing' : dsoType + '.edit.metadata.edit.buttons.enable-free-text-editing' | translate"
|
||||||
|
(click)="toggleFreeTextEdition()">
|
||||||
|
<i class="fas fa-fw" [ngClass]="enabledFreeTextEditing ? 'fa-lock' : 'fa-unlock'"></i>
|
||||||
|
{{ (enabledFreeTextEditing ? dsoType + '.edit.metadata.edit.buttons.disable-free-text-editing' : dsoType + '.edit.metadata.edit.buttons.enable-free-text-editing') | translate }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if ((isAuthorityControlled$ | async) && (isSuggesterVocabulary$ | async)) {
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="btn-group w-75">
|
||||||
|
<i dsAuthorityConfidenceState
|
||||||
|
class="fas fa-fw p-0 me-1 mt-auto mb-auto"
|
||||||
|
aria-hidden="true"
|
||||||
|
[authorityValue]="mdValue.newValue.confidence"
|
||||||
|
[iconMode]="true"
|
||||||
|
></i>
|
||||||
|
<input class="form-control form-outline" data-test="authority-input" [(ngModel)]="mdValue.newValue.authority"
|
||||||
|
[disabled]="!editingAuthority"
|
||||||
|
[attr.aria-label]="(dsoType + '.edit.metadata.edit.authority.key') | translate"
|
||||||
|
(change)="onChangeAuthorityKey()"/>
|
||||||
|
@if (editingAuthority) {
|
||||||
|
<button class="btn btn-outline-success btn-sm ng-star-inserted" id="metadata-confirm-btn"
|
||||||
|
[title]="dsoType + '.edit.metadata.edit.buttons.close-authority-edition' | translate"
|
||||||
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.close-authority-edition' | translate }}"
|
||||||
|
(click)="onChangeEditingAuthorityStatus(false)">
|
||||||
|
<i class="fas fa-lock-open fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button class="btn btn-outline-secondary btn-sm ng-star-inserted" id="metadata-confirm-btn"
|
||||||
|
[title]="dsoType + '.edit.metadata.edit.buttons.open-authority-edition' | translate"
|
||||||
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.open-authority-edition' | translate }}"
|
||||||
|
(click)="onChangeEditingAuthorityStatus(true)">
|
||||||
|
<i class="fas fa-lock fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,360 @@
|
|||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
waitForAsync,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
||||||
|
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||||
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
import { ConfidenceType } from '../../../../core/shared/confidence-type';
|
||||||
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
||||||
|
import { Vocabulary } from '../../../../core/submission/vocabularies/models/vocabulary.model';
|
||||||
|
import { VocabularyService } from '../../../../core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { DynamicOneboxModel } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
|
||||||
|
import { DsDynamicScrollableDropdownComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
||||||
|
import { DynamicScrollableDropdownModel } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||||
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||||
|
import { VocabularyServiceStub } from '../../../../shared/testing/vocabulary-service.stub';
|
||||||
|
import { DsoEditMetadataValue } from '../../dso-edit-metadata-form';
|
||||||
|
import { DsoEditMetadataAuthorityFieldComponent } from './dso-edit-metadata-authority-field.component';
|
||||||
|
|
||||||
|
describe('DsoEditMetadataAuthorityFieldComponent', () => {
|
||||||
|
let component: DsoEditMetadataAuthorityFieldComponent;
|
||||||
|
let fixture: ComponentFixture<DsoEditMetadataAuthorityFieldComponent>;
|
||||||
|
|
||||||
|
let vocabularyService: any;
|
||||||
|
let itemService: ItemDataService;
|
||||||
|
let registryService: RegistryService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
|
||||||
|
let dso: DSpaceObject;
|
||||||
|
|
||||||
|
const collection = Object.assign(new Collection(), {
|
||||||
|
uuid: 'fake-uuid',
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
_links: {
|
||||||
|
self: { href: 'fake-item-url/item' },
|
||||||
|
},
|
||||||
|
id: 'item',
|
||||||
|
uuid: 'item',
|
||||||
|
owningCollection: createSuccessfulRemoteDataObject$(collection),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockVocabularyScrollable: Vocabulary = {
|
||||||
|
id: 'scrollable',
|
||||||
|
name: 'scrollable',
|
||||||
|
scrollable: true,
|
||||||
|
hierarchical: false,
|
||||||
|
preloadLevel: 0,
|
||||||
|
type: 'vocabulary',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'self',
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
href: 'entries',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockVocabularyHierarchical: Vocabulary = {
|
||||||
|
id: 'hierarchical',
|
||||||
|
name: 'hierarchical',
|
||||||
|
scrollable: false,
|
||||||
|
hierarchical: true,
|
||||||
|
preloadLevel: 2,
|
||||||
|
type: 'vocabulary',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'self',
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
href: 'entries',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockVocabularySuggester: Vocabulary = {
|
||||||
|
id: 'suggester',
|
||||||
|
name: 'suggester',
|
||||||
|
scrollable: false,
|
||||||
|
hierarchical: false,
|
||||||
|
preloadLevel: 0,
|
||||||
|
type: 'vocabulary',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'self',
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
href: 'entries',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let editMetadataValue: DsoEditMetadataValue;
|
||||||
|
let metadataValue: MetadataValue;
|
||||||
|
let metadataSchema: MetadataSchema;
|
||||||
|
let metadataFields: MetadataField[];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
itemService = jasmine.createSpyObj('itemService', {
|
||||||
|
findByHref: createSuccessfulRemoteDataObject$(item),
|
||||||
|
});
|
||||||
|
vocabularyService = new VocabularyServiceStub();
|
||||||
|
registryService = jasmine.createSpyObj('registryService', {
|
||||||
|
queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)),
|
||||||
|
});
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']);
|
||||||
|
|
||||||
|
metadataValue = Object.assign(new MetadataValue(), {
|
||||||
|
value: 'Regular Name',
|
||||||
|
language: 'en',
|
||||||
|
place: 0,
|
||||||
|
authority: undefined,
|
||||||
|
});
|
||||||
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
|
metadataSchema = Object.assign(new MetadataSchema(), {
|
||||||
|
id: 0,
|
||||||
|
prefix: 'metadata',
|
||||||
|
namespace: 'https://example.com/',
|
||||||
|
});
|
||||||
|
metadataFields = [
|
||||||
|
Object.assign(new MetadataField(), {
|
||||||
|
id: 0,
|
||||||
|
element: 'regular',
|
||||||
|
qualifier: null,
|
||||||
|
schema: createSuccessfulRemoteDataObject$(metadataSchema),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
dso = Object.assign(new DSpaceObject(), {
|
||||||
|
_links: {
|
||||||
|
self: { href: 'fake-dso-url/dso' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
DsoEditMetadataAuthorityFieldComponent,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: VocabularyService, useValue: vocabularyService },
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
{ provide: RegistryService, useValue: registryService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
],
|
||||||
|
}).overrideComponent(DsoEditMetadataAuthorityFieldComponent, {
|
||||||
|
remove: {
|
||||||
|
imports: [
|
||||||
|
DsDynamicScrollableDropdownComponent,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DsoEditMetadataAuthorityFieldComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.mdValue = editMetadataValue;
|
||||||
|
component.dso = dso;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the metadata field uses a scrollable vocabulary and is editing', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
spyOn(vocabularyService, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyScrollable));
|
||||||
|
metadataValue = Object.assign(new MetadataValue(), {
|
||||||
|
value: 'Authority Controlled value',
|
||||||
|
language: 'en',
|
||||||
|
place: 0,
|
||||||
|
authority: null,
|
||||||
|
});
|
||||||
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
|
editMetadataValue.editing = true;
|
||||||
|
component.mdValue = editMetadataValue;
|
||||||
|
component.mdField = 'metadata.scrollable';
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render the DsDynamicScrollableDropdownComponent', () => {
|
||||||
|
expect(vocabularyService.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-dynamic-scrollable-dropdown'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getModel should return a DynamicScrollableDropdownModel', () => {
|
||||||
|
const model = component.getModel();
|
||||||
|
|
||||||
|
expect(model instanceof DynamicScrollableDropdownModel).toBe(true);
|
||||||
|
expect(model.vocabularyOptions.name).toBe(mockVocabularyScrollable.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the metadata field uses a hierarchical vocabulary and is editing', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
spyOn(vocabularyService, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyHierarchical));
|
||||||
|
metadataValue = Object.assign(new MetadataValue(), {
|
||||||
|
value: 'Authority Controlled value',
|
||||||
|
language: 'en',
|
||||||
|
place: 0,
|
||||||
|
authority: null,
|
||||||
|
});
|
||||||
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
|
editMetadataValue.editing = true;
|
||||||
|
component.mdValue = editMetadataValue;
|
||||||
|
component.mdField = 'metadata.hierarchical';
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render the DsDynamicOneboxComponent', () => {
|
||||||
|
expect(vocabularyService.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getModel should return a DynamicOneboxModel', () => {
|
||||||
|
const model = component.getModel();
|
||||||
|
|
||||||
|
expect(model instanceof DynamicOneboxModel).toBe(true);
|
||||||
|
expect(model.vocabularyOptions.name).toBe(mockVocabularyHierarchical.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the metadata field uses a suggester vocabulary and is editing', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
spyOn(vocabularyService, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularySuggester));
|
||||||
|
spyOn(component.confirm, 'emit');
|
||||||
|
metadataValue = Object.assign(new MetadataValue(), {
|
||||||
|
value: 'Authority Controlled value',
|
||||||
|
language: 'en',
|
||||||
|
place: 0,
|
||||||
|
authority: 'authority-key',
|
||||||
|
confidence: ConfidenceType.CF_UNCERTAIN,
|
||||||
|
});
|
||||||
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
|
editMetadataValue.editing = true;
|
||||||
|
component.mdValue = editMetadataValue;
|
||||||
|
component.mdField = 'metadata.suggester';
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render the DsDynamicOneboxComponent', () => {
|
||||||
|
expect(vocabularyService.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getModel should return a DynamicOneboxModel', () => {
|
||||||
|
const model = component.getModel();
|
||||||
|
|
||||||
|
expect(model instanceof DynamicOneboxModel).toBe(true);
|
||||||
|
expect(model.vocabularyOptions.name).toBe(mockVocabularySuggester.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authority key edition', () => {
|
||||||
|
|
||||||
|
it('should update confidence to CF_NOVALUE when authority is cleared', () => {
|
||||||
|
component.mdValue.newValue.authority = '';
|
||||||
|
|
||||||
|
component.onChangeAuthorityKey();
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_NOVALUE);
|
||||||
|
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update confidence to CF_ACCEPTED when authority key is edited', () => {
|
||||||
|
component.mdValue.newValue.authority = 'newAuthority';
|
||||||
|
component.mdValue.originalValue.authority = 'oldAuthority';
|
||||||
|
|
||||||
|
component.onChangeAuthorityKey();
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED);
|
||||||
|
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update confidence when authority key remains the same', () => {
|
||||||
|
component.mdValue.newValue.authority = 'sameAuthority';
|
||||||
|
component.mdValue.originalValue.authority = 'sameAuthority';
|
||||||
|
|
||||||
|
component.onChangeAuthorityKey();
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNCERTAIN);
|
||||||
|
expect(component.confirm.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChangeEditingAuthorityStatus with true when clicking the lock button', () => {
|
||||||
|
spyOn(component, 'onChangeEditingAuthorityStatus');
|
||||||
|
const lockButton = fixture.nativeElement.querySelector('#metadata-confirm-btn');
|
||||||
|
|
||||||
|
lockButton.click();
|
||||||
|
|
||||||
|
expect(component.onChangeEditingAuthorityStatus).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable the input when editingAuthority is false', (done) => {
|
||||||
|
component.editingAuthority = false;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
const inputElement = fixture.nativeElement.querySelector('input[data-test="authority-input"]');
|
||||||
|
expect(inputElement.disabled).toBeTruthy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable the input when editingAuthority is true', (done) => {
|
||||||
|
component.editingAuthority = true;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
const inputElement = fixture.nativeElement.querySelector('input[data-test="authority-input"]');
|
||||||
|
expect(inputElement.disabled).toBeFalsy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update mdValue.newValue properties when authority is present', () => {
|
||||||
|
const event = {
|
||||||
|
value: 'Some value',
|
||||||
|
authority: 'Some authority',
|
||||||
|
};
|
||||||
|
|
||||||
|
component.onChangeAuthorityField(event);
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.value).toBe(event.value);
|
||||||
|
expect(component.mdValue.newValue.authority).toBe(event.authority);
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED);
|
||||||
|
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update mdValue.newValue properties when authority is not present', () => {
|
||||||
|
const event = {
|
||||||
|
value: 'Some value',
|
||||||
|
authority: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
component.onChangeAuthorityField(event);
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.value).toBe(event.value);
|
||||||
|
expect(component.mdValue.newValue.authority).toBeNull();
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNSET);
|
||||||
|
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,331 @@
|
|||||||
|
import {
|
||||||
|
AsyncPipe,
|
||||||
|
NgClass,
|
||||||
|
} from '@angular/common';
|
||||||
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
OnChanges,
|
||||||
|
OnInit,
|
||||||
|
SimpleChanges,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
FormsModule,
|
||||||
|
UntypedFormControl,
|
||||||
|
UntypedFormGroup,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {
|
||||||
|
TranslateModule,
|
||||||
|
TranslateService,
|
||||||
|
} from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
import {
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
tap,
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
|
import { ConfidenceType } from '../../../../core/shared/confidence-type';
|
||||||
|
import {
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
metadataFieldsToString,
|
||||||
|
} from '../../../../core/shared/operators';
|
||||||
|
import { Vocabulary } from '../../../../core/submission/vocabularies/models/vocabulary.model';
|
||||||
|
import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { DsDynamicOneboxComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
|
||||||
|
import {
|
||||||
|
DsDynamicOneboxModelConfig,
|
||||||
|
DynamicOneboxModel,
|
||||||
|
} from '../../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
|
||||||
|
import { DsDynamicScrollableDropdownComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
||||||
|
import {
|
||||||
|
DynamicScrollableDropdownModel,
|
||||||
|
DynamicScrollableDropdownModelConfig,
|
||||||
|
} from '../../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||||
|
import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/models/form-field-metadata-value.model';
|
||||||
|
import { AuthorityConfidenceStateDirective } from '../../../../shared/form/directives/authority-confidence-state.directive';
|
||||||
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { DebounceDirective } from '../../../../shared/utils/debounce.directive';
|
||||||
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { AbstractDsoEditMetadataValueFieldComponent } from '../abstract-dso-edit-metadata-value-field.component';
|
||||||
|
import { DsoEditMetadataFieldService } from '../dso-edit-metadata-field.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component used to gather input for authority controlled metadata fields
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-dso-edit-metadata-authority-field',
|
||||||
|
templateUrl: './dso-edit-metadata-authority-field.component.html',
|
||||||
|
styleUrls: ['./dso-edit-metadata-authority-field.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
DsDynamicScrollableDropdownComponent,
|
||||||
|
DsDynamicOneboxComponent,
|
||||||
|
AuthorityConfidenceStateDirective,
|
||||||
|
NgbTooltipModule,
|
||||||
|
AsyncPipe,
|
||||||
|
TranslateModule,
|
||||||
|
FormsModule,
|
||||||
|
NgClass,
|
||||||
|
DebounceDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DsoEditMetadataAuthorityFieldComponent extends AbstractDsoEditMetadataValueFieldComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the authority field is currently being edited
|
||||||
|
*/
|
||||||
|
public editingAuthority = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the free-text editing is enabled when scrollable dropdown or hierarchical vocabulary is used
|
||||||
|
*/
|
||||||
|
public enabledFreeTextEditing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field group used by authority field
|
||||||
|
*/
|
||||||
|
group = new UntypedFormGroup({ authorityField: new UntypedFormControl() });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model to use for editing authorities values
|
||||||
|
*/
|
||||||
|
private model$: BehaviorSubject<DynamicOneboxModel | DynamicScrollableDropdownModel> = new BehaviorSubject(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable with information about the authority vocabulary used
|
||||||
|
*/
|
||||||
|
private vocabulary$: Observable<Vocabulary>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observables with information about the authority vocabulary type used
|
||||||
|
*/
|
||||||
|
isAuthorityControlled$: Observable<boolean>;
|
||||||
|
isHierarchicalVocabulary$: Observable<boolean>;
|
||||||
|
isScrollableVocabulary$: Observable<boolean>;
|
||||||
|
isSuggesterVocabulary$: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected cdr: ChangeDetectorRef,
|
||||||
|
protected dsoEditMetadataFieldService: DsoEditMetadataFieldService,
|
||||||
|
protected itemService: ItemDataService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected registryService: RegistryService,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.initAuthorityProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise potential properties of a authority controlled metadata field
|
||||||
|
*/
|
||||||
|
initAuthorityProperties(): void {
|
||||||
|
this.vocabulary$ = this.dsoEditMetadataFieldService.findDsoFieldVocabulary(this.dso, this.mdField);
|
||||||
|
|
||||||
|
this.isAuthorityControlled$ = this.vocabulary$.pipe(
|
||||||
|
// Create the model used by the authority fields to ensure its existence when the field is initialized
|
||||||
|
tap((v: Vocabulary) => this.model$.next(this.createModel(v))),
|
||||||
|
map((result: Vocabulary) => isNotEmpty(result)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isHierarchicalVocabulary$ = this.vocabulary$.pipe(
|
||||||
|
map((result: Vocabulary) => isNotEmpty(result) && result.hierarchical),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isScrollableVocabulary$ = this.vocabulary$.pipe(
|
||||||
|
map((result: Vocabulary) => isNotEmpty(result) && result.scrollable),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isSuggesterVocabulary$ = this.vocabulary$.pipe(
|
||||||
|
map((result: Vocabulary) => isNotEmpty(result) && !result.hierarchical && !result.scrollable),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model based on the
|
||||||
|
* vocabulary used.
|
||||||
|
*/
|
||||||
|
private createModel(vocabulary: Vocabulary): DynamicOneboxModel | DynamicScrollableDropdownModel {
|
||||||
|
if (isNotEmpty(vocabulary)) {
|
||||||
|
let formFieldValue: FormFieldMetadataValueObject | string;
|
||||||
|
if (isNotEmpty(this.mdValue.newValue.value)) {
|
||||||
|
formFieldValue = new FormFieldMetadataValueObject();
|
||||||
|
formFieldValue.value = this.mdValue.newValue.value;
|
||||||
|
formFieldValue.display = this.mdValue.newValue.value;
|
||||||
|
if (this.mdValue.newValue.authority) {
|
||||||
|
formFieldValue.authority = this.mdValue.newValue.authority;
|
||||||
|
formFieldValue.confidence = this.mdValue.newValue.confidence;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formFieldValue = this.mdValue.newValue.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vocabularyOptions = vocabulary ? {
|
||||||
|
closed: false,
|
||||||
|
name: vocabulary.name,
|
||||||
|
} as VocabularyOptions : null;
|
||||||
|
|
||||||
|
if (!vocabulary.scrollable) {
|
||||||
|
const model: DsDynamicOneboxModelConfig = {
|
||||||
|
id: 'authorityField',
|
||||||
|
label: `${this.dsoType}.edit.metadata.edit.value`,
|
||||||
|
vocabularyOptions: vocabularyOptions,
|
||||||
|
metadataFields: [this.mdField],
|
||||||
|
value: formFieldValue,
|
||||||
|
repeatable: false,
|
||||||
|
submissionId: 'edit-metadata',
|
||||||
|
hasSelectableMetadata: false,
|
||||||
|
};
|
||||||
|
return new DynamicOneboxModel(model);
|
||||||
|
} else {
|
||||||
|
const model: DynamicScrollableDropdownModelConfig = {
|
||||||
|
id: 'authorityField',
|
||||||
|
label: `${this.dsoType}.edit.metadata.edit.value`,
|
||||||
|
placeholder: `${this.dsoType}.edit.metadata.edit.value`,
|
||||||
|
vocabularyOptions: vocabularyOptions,
|
||||||
|
metadataFields: [this.mdField],
|
||||||
|
value: formFieldValue,
|
||||||
|
repeatable: false,
|
||||||
|
submissionId: 'edit-metadata',
|
||||||
|
hasSelectableMetadata: false,
|
||||||
|
maxOptions: 10,
|
||||||
|
};
|
||||||
|
return new DynamicScrollableDropdownModel(model);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change callback for the component. Check if the mdField has changed to retrieve whether it is metadata
|
||||||
|
* that uses a controlled vocabulary and update the related properties
|
||||||
|
*
|
||||||
|
* @param {SimpleChanges} changes
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (isNotEmpty(changes.mdField) && !changes.mdField.firstChange) {
|
||||||
|
if (isNotEmpty(changes.mdField.currentValue)) {
|
||||||
|
if (isNotEmpty(changes.mdField.previousValue) &&
|
||||||
|
changes.mdField.previousValue !== changes.mdField.currentValue) {
|
||||||
|
// Clear authority value in case it has been assigned with the previous metadataField used
|
||||||
|
this.mdValue.newValue.authority = null;
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only ask if the current mdField have a period character to reduce request
|
||||||
|
if (changes.mdField.currentValue.includes('.')) {
|
||||||
|
this.validateMetadataField().subscribe((isValid: boolean) => {
|
||||||
|
if (isValid) {
|
||||||
|
this.initAuthorityProperties();
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the metadata field to check if it exists on the server and return an observable boolean for success/error
|
||||||
|
*/
|
||||||
|
validateMetadataField(): Observable<boolean> {
|
||||||
|
return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((rd) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
return observableOf(rd).pipe(
|
||||||
|
metadataFieldsToString(),
|
||||||
|
take(1),
|
||||||
|
map((fields: string[]) => fields.indexOf(this.mdField) > -1),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translate.instant(`${this.dsoType}.edit.metadata.metadatafield.error`), rd.errorMessage);
|
||||||
|
return [false];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the change of authority field value updating the authority key and confidence as necessary
|
||||||
|
*/
|
||||||
|
onChangeAuthorityField(event): void {
|
||||||
|
if (event) {
|
||||||
|
this.mdValue.newValue.value = event.value;
|
||||||
|
if (event.authority) {
|
||||||
|
this.mdValue.newValue.authority = event.authority;
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED;
|
||||||
|
} else {
|
||||||
|
this.mdValue.newValue.authority = null;
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
||||||
|
}
|
||||||
|
this.confirm.emit(false);
|
||||||
|
} else {
|
||||||
|
// The event is undefined when the user clears the selection in scrollable dropdown
|
||||||
|
this.mdValue.newValue.value = '';
|
||||||
|
this.mdValue.newValue.authority = null;
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
||||||
|
this.confirm.emit(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model used
|
||||||
|
* for the authority field
|
||||||
|
*/
|
||||||
|
getModel(): DynamicOneboxModel | DynamicScrollableDropdownModel {
|
||||||
|
return this.model$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the status of the editingAuthority property
|
||||||
|
* @param status
|
||||||
|
*/
|
||||||
|
onChangeEditingAuthorityStatus(status: boolean) {
|
||||||
|
this.editingAuthority = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the change in authority value, updating the confidence as necessary.
|
||||||
|
* If the authority key is cleared, the confidence is set to {@link ConfidenceType.CF_NOVALUE}.
|
||||||
|
* If the authority key is edited and differs from the original, the confidence is set to {@link ConfidenceType.CF_ACCEPTED}.
|
||||||
|
*/
|
||||||
|
onChangeAuthorityKey() {
|
||||||
|
if (this.mdValue.newValue.authority === '') {
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_NOVALUE;
|
||||||
|
this.confirm.emit(false);
|
||||||
|
} else if (this.mdValue.newValue.authority !== this.mdValue.originalValue.authority) {
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED;
|
||||||
|
this.confirm.emit(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the free-text editing mode
|
||||||
|
*/
|
||||||
|
toggleFreeTextEdition() {
|
||||||
|
if (this.enabledFreeTextEditing) {
|
||||||
|
if (this.getModel().value !== this.mdValue.newValue.value) {
|
||||||
|
// Reload the model to adapt it to the new possible value modified during free text editing
|
||||||
|
this.initAuthorityProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.enabledFreeTextEditing = !this.enabledFreeTextEditing;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
<select class="form-select" [(ngModel)]="mdValue?.newValue.value" (ngModelChange)="confirm.emit(false)"
|
||||||
|
[attr.aria-label]="(dsoType + '.edit.metadata.edit.value') | translate">
|
||||||
|
@for (entity of (entities$ | async); track entity.label) {
|
||||||
|
<option [value]="entity.label === 'none' ? undefined : entity.label">
|
||||||
|
{{ entity.label }}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { EntityTypeDataService } from '../../../../core/data/entity-type-data.service';
|
||||||
|
import { EntityTypeDataServiceStub } from '../../../../shared/testing/entity-type-data.service.stub';
|
||||||
|
import { DsoEditMetadataEntityFieldComponent } from './dso-edit-metadata-entity-field.component';
|
||||||
|
|
||||||
|
describe('DsoEditMetadataEntityFieldComponent', () => {
|
||||||
|
let component: DsoEditMetadataEntityFieldComponent;
|
||||||
|
let fixture: ComponentFixture<DsoEditMetadataEntityFieldComponent>;
|
||||||
|
|
||||||
|
let entityTypeService: EntityTypeDataServiceStub;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
entityTypeService = new EntityTypeDataServiceStub();
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
DsoEditMetadataEntityFieldComponent,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: EntityTypeDataService, useValue: entityTypeService },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DsoEditMetadataEntityFieldComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,48 @@
|
|||||||
|
import { AsyncPipe } from '@angular/common';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { EntityTypeDataService } from '../../../../core/data/entity-type-data.service';
|
||||||
|
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
|
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
|
||||||
|
import { AbstractDsoEditMetadataValueFieldComponent } from '../abstract-dso-edit-metadata-value-field.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component used to gather input for entity-type metadata fields
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-dso-edit-metadata-entity-field',
|
||||||
|
templateUrl: './dso-edit-metadata-entity-field.component.html',
|
||||||
|
styleUrls: ['./dso-edit-metadata-entity-field.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DsoEditMetadataEntityFieldComponent extends AbstractDsoEditMetadataValueFieldComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of all the existing entity types
|
||||||
|
*/
|
||||||
|
entities$: Observable<ItemType[]>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected entityTypeService: EntityTypeDataService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.entities$ = this.entityTypeService.findAll({ elementsPerPage: 100, currentPage: 1 }).pipe(
|
||||||
|
getFirstSucceededRemoteListPayload(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* The edit metadata field tab types
|
||||||
|
*/
|
||||||
|
export enum EditMetadataValueFieldType {
|
||||||
|
PLAIN_TEXT = 'PLAIN_TEXT',
|
||||||
|
ENTITY_TYPE = 'ENTITY_TYPE',
|
||||||
|
AUTHORITY = 'AUTHORITY',
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub';
|
||||||
|
import { VocabularyServiceStub } from '../../../shared/testing/vocabulary-service.stub';
|
||||||
|
import { DsoEditMetadataFieldService } from './dso-edit-metadata-field.service';
|
||||||
|
|
||||||
|
describe('DsoEditMetadataFieldService', () => {
|
||||||
|
let service: DsoEditMetadataFieldService;
|
||||||
|
|
||||||
|
let itemService: ItemDataServiceStub;
|
||||||
|
let vocabularyService: VocabularyServiceStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
itemService = new ItemDataServiceStub();
|
||||||
|
vocabularyService = new VocabularyServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
{ provide: VocabularyService, useValue: vocabularyService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(DsoEditMetadataFieldService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,56 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
|
import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model';
|
||||||
|
import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service containing all the common logic for the components generated by the
|
||||||
|
* {@link DsoEditMetadataValueFieldLoaderComponent}.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class DsoEditMetadataFieldService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected itemService: ItemDataService,
|
||||||
|
protected vocabularyService: VocabularyService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the vocabulary of the given {@link mdField} for the given item.
|
||||||
|
*
|
||||||
|
* @param dso The item
|
||||||
|
* @param mdField The metadata field
|
||||||
|
*/
|
||||||
|
findDsoFieldVocabulary(dso: DSpaceObject, mdField: string): Observable<Vocabulary> {
|
||||||
|
if (isNotEmpty(mdField)) {
|
||||||
|
const owningCollection$: Observable<Collection> = this.itemService.findByHref(dso._links.self.href, true, true, followLink('owningCollection')).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((item: Item) => item.owningCollection),
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return owningCollection$.pipe(
|
||||||
|
switchMap((c: Collection) => this.vocabularyService.getVocabularyByMetadataAndCollection(mdField, c.uuid).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<textarea [(ngModel)]="mdValue?.newValue.value"
|
||||||
|
[attr.aria-label]="(dsoType + '.edit.metadata.edit.value') | translate"
|
||||||
|
[dsDebounce]="300"
|
||||||
|
(onDebounce)="confirm.emit(false)"
|
||||||
|
class="form-control"
|
||||||
|
rows="5">
|
||||||
|
</textarea>
|
@@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DsoEditMetadataTextFieldComponent } from './dso-edit-metadata-text-field.component';
|
||||||
|
|
||||||
|
describe('DsoEditMetadataTextFieldComponent', () => {
|
||||||
|
let component: DsoEditMetadataTextFieldComponent;
|
||||||
|
let fixture: ComponentFixture<DsoEditMetadataTextFieldComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
DsoEditMetadataTextFieldComponent,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DsoEditMetadataTextFieldComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DebounceDirective } from '../../../../shared/utils/debounce.directive';
|
||||||
|
import { AbstractDsoEditMetadataValueFieldComponent } from '../abstract-dso-edit-metadata-value-field.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component used to gather input for plain-text metadata fields
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-dso-edit-metadata-text-field',
|
||||||
|
templateUrl: './dso-edit-metadata-text-field.component.html',
|
||||||
|
styleUrls: ['./dso-edit-metadata-text-field.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
DebounceDirective,
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DsoEditMetadataTextFieldComponent extends AbstractDsoEditMetadataValueFieldComponent {
|
||||||
|
}
|
@@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
|
import { AbstractComponentLoaderComponent } from '../../../../shared/abstract-component-loader/abstract-component-loader.component';
|
||||||
|
import { DynamicComponentLoaderDirective } from '../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
|
||||||
|
import { DsoEditMetadataValue } from '../../dso-edit-metadata-form';
|
||||||
|
import { EditMetadataValueFieldType } from '../dso-edit-metadata-field-type.enum';
|
||||||
|
import { getDsoEditMetadataValueFieldComponent } from './dso-edit-metadata-value-field.decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component responsible for dynamically loading and rendering the appropriate edit metadata value field components
|
||||||
|
* based on the type of the metadata field ({@link EditMetadataValueFieldType}) and the place where it's used
|
||||||
|
* ({@link Context}).
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-dso-edit-metadata-value-field-loader',
|
||||||
|
templateUrl: '../../../../shared/abstract-component-loader/abstract-component-loader.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
DynamicComponentLoaderDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DsoEditMetadataValueFieldLoaderComponent extends AbstractComponentLoaderComponent<Component> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional context
|
||||||
|
*/
|
||||||
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link DSpaceObject}
|
||||||
|
*/
|
||||||
|
@Input() dso: DSpaceObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the DSO, used to determines i18n messages
|
||||||
|
*/
|
||||||
|
@Input() dsoType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the field
|
||||||
|
*/
|
||||||
|
@Input() type: EditMetadataValueFieldType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata field
|
||||||
|
*/
|
||||||
|
@Input() mdField: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editable metadata value to show
|
||||||
|
*/
|
||||||
|
@Input() mdValue: DsoEditMetadataValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the user clicked confirm
|
||||||
|
*/
|
||||||
|
@Output() confirm: EventEmitter<boolean> = new EventEmitter();
|
||||||
|
|
||||||
|
protected inputNamesDependentForComponent: (keyof this & string)[] = [
|
||||||
|
'context',
|
||||||
|
'type',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected inputNames: (keyof this & string)[] = [
|
||||||
|
'context',
|
||||||
|
'dso',
|
||||||
|
'dsoType',
|
||||||
|
'type',
|
||||||
|
'mdField',
|
||||||
|
'mdValue',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected outputNames: (keyof this & string)[] = [
|
||||||
|
'confirm',
|
||||||
|
];
|
||||||
|
|
||||||
|
public getComponent(): GenericConstructor<Component> {
|
||||||
|
return getDsoEditMetadataValueFieldComponent(this.type, this.context, this.themeService.getThemeName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,59 @@
|
|||||||
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
|
import {
|
||||||
|
DEFAULT_CONTEXT,
|
||||||
|
DEFAULT_THEME,
|
||||||
|
resolveTheme,
|
||||||
|
} from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { DsoEditMetadataAuthorityFieldComponent } from '../dso-edit-metadata-authority-field/dso-edit-metadata-authority-field.component';
|
||||||
|
import { DsoEditMetadataEntityFieldComponent } from '../dso-edit-metadata-entity-field/dso-edit-metadata-entity-field.component';
|
||||||
|
import { EditMetadataValueFieldType } from '../dso-edit-metadata-field-type.enum';
|
||||||
|
import { DsoEditMetadataTextFieldComponent } from '../dso-edit-metadata-text-field/dso-edit-metadata-text-field.component';
|
||||||
|
|
||||||
|
export type MetadataValueFieldComponent =
|
||||||
|
typeof DsoEditMetadataTextFieldComponent |
|
||||||
|
typeof DsoEditMetadataEntityFieldComponent |
|
||||||
|
typeof DsoEditMetadataAuthorityFieldComponent;
|
||||||
|
|
||||||
|
export const map = new Map<EditMetadataValueFieldType, Map<Context, Map<string, MetadataValueFieldComponent>>>([
|
||||||
|
[EditMetadataValueFieldType.PLAIN_TEXT, new Map([
|
||||||
|
[DEFAULT_CONTEXT, new Map([[DEFAULT_THEME, DsoEditMetadataTextFieldComponent]])],
|
||||||
|
])],
|
||||||
|
[EditMetadataValueFieldType.ENTITY_TYPE, new Map([
|
||||||
|
[DEFAULT_CONTEXT, new Map([[DEFAULT_THEME, DsoEditMetadataEntityFieldComponent]])],
|
||||||
|
])],
|
||||||
|
[EditMetadataValueFieldType.AUTHORITY, new Map([
|
||||||
|
[DEFAULT_CONTEXT, new Map([[DEFAULT_THEME, DsoEditMetadataAuthorityFieldComponent]])],
|
||||||
|
])],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const DEFAULT_EDIT_METADATA_FIELD_TYPE = EditMetadataValueFieldType.PLAIN_TEXT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter to retrieve a matching component by entity type, metadata representation and context
|
||||||
|
*
|
||||||
|
* @param type The edit metadata field type
|
||||||
|
* @param context The context to match
|
||||||
|
* @param theme the theme to match
|
||||||
|
*/
|
||||||
|
export function getDsoEditMetadataValueFieldComponent(type: EditMetadataValueFieldType, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) {
|
||||||
|
if (type) {
|
||||||
|
const mapForEntity = map.get(type);
|
||||||
|
if (hasValue(mapForEntity)) {
|
||||||
|
const contextMap = mapForEntity.get(context);
|
||||||
|
if (hasValue(contextMap)) {
|
||||||
|
const match = resolveTheme(contextMap, theme);
|
||||||
|
if (hasValue(match)) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
if (hasValue(contextMap.get(DEFAULT_THEME))) {
|
||||||
|
return contextMap.get(DEFAULT_THEME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasValue(mapForEntity.get(DEFAULT_CONTEXT)) && hasValue(mapForEntity.get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) {
|
||||||
|
return mapForEntity.get(DEFAULT_CONTEXT).get(DEFAULT_THEME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map.get(DEFAULT_EDIT_METADATA_FIELD_TYPE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME);
|
||||||
|
}
|
@@ -1,37 +1,22 @@
|
|||||||
<div class="d-flex flex-row ds-value-row" *ngVar="metadataService.isVirtual(mdValue.newValue) as isVirtual" role="row"
|
@let isVirtual = metadataService.isVirtual(mdValue.newValue);
|
||||||
|
<div class="d-flex flex-row ds-value-row" role="row"
|
||||||
cdkDrag (cdkDragStarted)="dragging.emit(true)" (cdkDragEnded)="dragging.emit(false)"
|
cdkDrag (cdkDragStarted)="dragging.emit(true)" (cdkDragEnded)="dragging.emit(false)"
|
||||||
[ngClass]="{ 'ds-warning': mdValue.reordered || mdValue.change === DsoEditMetadataChangeTypeEnum.UPDATE, 'ds-danger': mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE, 'ds-success': mdValue.change === DsoEditMetadataChangeTypeEnum.ADD, 'h-100': isOnlyValue }">
|
[ngClass]="{ 'ds-warning': mdValue.reordered || mdValue.change === DsoEditMetadataChangeTypeEnum.UPDATE, 'ds-danger': mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE, 'ds-success': mdValue.change === DsoEditMetadataChangeTypeEnum.ADD, 'h-100': isOnlyValue }">
|
||||||
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex flex-column" *ngVar="(mdRepresentation$ | async) as mdRepresentation" role="cell">
|
@let mdRepresentation = (mdRepresentation$ | async);
|
||||||
|
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex flex-column" role="cell">
|
||||||
@if (!mdValue.editing && !mdRepresentation) {
|
@if (!mdValue.editing && !mdRepresentation) {
|
||||||
<div class="dont-break-out preserve-line-breaks">{{ mdValue.newValue.value }}</div>
|
<div class="dont-break-out preserve-line-breaks">{{ mdValue.newValue.value }}</div>
|
||||||
}
|
}
|
||||||
@if (mdValue.editing && !mdRepresentation && ((isAuthorityControlled() | async) !== true || (enabledFreeTextEditing && (isSuggesterVocabulary() | async) !== true))) {
|
@if (mdValue.editing && !mdRepresentation) {
|
||||||
<textarea class="form-control" rows="5" [(ngModel)]="mdValue.newValue.value"
|
<ds-dso-edit-metadata-value-field-loader [context]="context"
|
||||||
[attr.aria-label]="(dsoType + '.edit.metadata.edit.value') | translate"
|
[dso]="dso"
|
||||||
[dsDebounce]="300" (onDebounce)="confirm.emit(false)"></textarea>
|
[dsoType]="dsoType"
|
||||||
}
|
[mdField]="mdField"
|
||||||
@if (mdValue.editing && (isScrollableVocabulary() | async) && !enabledFreeTextEditing) {
|
[mdValue]="mdValue"
|
||||||
<ds-dynamic-scrollable-dropdown
|
[type]="fieldType$ | async"
|
||||||
[bindId]="mdField"
|
(confirm)="confirm.emit($event)"
|
||||||
[group]="group"
|
class="w-100">
|
||||||
[model]="getModel()"
|
</ds-dso-edit-metadata-value-field-loader>
|
||||||
(change)="onChangeAuthorityField($event)">
|
|
||||||
</ds-dynamic-scrollable-dropdown>
|
|
||||||
}
|
|
||||||
@if (mdValue.editing && (((isHierarchicalVocabulary() | async) && !enabledFreeTextEditing) || (isSuggesterVocabulary() | async))) {
|
|
||||||
<ds-dynamic-onebox
|
|
||||||
[group]="group"
|
|
||||||
[model]="getModel()"
|
|
||||||
(change)="onChangeAuthorityField($event)">
|
|
||||||
</ds-dynamic-onebox>
|
|
||||||
}
|
|
||||||
@if (mdValue.editing && ((isScrollableVocabulary() | async) || (isHierarchicalVocabulary() | async))) {
|
|
||||||
<button class="btn btn-secondary mt-2"
|
|
||||||
[title]="enabledFreeTextEditing ? dsoType + '.edit.metadata.edit.buttons.disable-free-text-editing' : dsoType + '.edit.metadata.edit.buttons.enable-free-text-editing' | translate"
|
|
||||||
(click)="toggleFreeTextEdition()">
|
|
||||||
<i class="fas fa-fw" [ngClass]="enabledFreeTextEditing ? 'fa-lock' : 'fa-unlock'"></i>
|
|
||||||
{{ (enabledFreeTextEditing ? dsoType + '.edit.metadata.edit.buttons.disable-free-text-editing' : dsoType + '.edit.metadata.edit.buttons.enable-free-text-editing') | translate }}
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
@if (!isVirtual && !mdValue.editing && mdValue.newValue.authority && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_UNSET && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_NOVALUE) {
|
@if (!isVirtual && !mdValue.editing && mdValue.newValue.authority && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_UNSET && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_NOVALUE) {
|
||||||
<div>
|
<div>
|
||||||
@@ -46,88 +31,62 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if ( mdValue.editing && (isAuthorityControlled() | async) && (isSuggesterVocabulary() | async)) {
|
|
||||||
<div class="mt-2">
|
|
||||||
<div class="btn-group w-75">
|
|
||||||
<i dsAuthorityConfidenceState
|
|
||||||
class="fas fa-fw p-0 me-1 mt-auto mb-auto"
|
|
||||||
aria-hidden="true"
|
|
||||||
[authorityValue]="mdValue.newValue.confidence"
|
|
||||||
[iconMode]="true"
|
|
||||||
></i>
|
|
||||||
<input class="form-control form-outline" data-test="authority-input" [(ngModel)]="mdValue.newValue.authority" [disabled]="!editingAuthority"
|
|
||||||
[attr.aria-label]="(dsoType + '.edit.metadata.edit.authority.key') | translate"
|
|
||||||
(change)="onChangeAuthorityKey()" />
|
|
||||||
@if (!editingAuthority) {
|
|
||||||
<button class="btn btn-outline-secondary btn-sm ng-star-inserted" id="metadata-confirm-btn"
|
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.open-authority-edition' | translate"
|
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.open-authority-edition' | translate }}"
|
|
||||||
(click)="onChangeEditingAuthorityStatus(true)">
|
|
||||||
<i class="fas fa-lock fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
@if (editingAuthority) {
|
|
||||||
<button class="btn btn-outline-success btn-sm ng-star-inserted" id="metadata-confirm-btn"
|
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.close-authority-edition' | translate"
|
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.close-authority-edition' | translate }}"
|
|
||||||
(click)="onChangeEditingAuthorityStatus(false)">
|
|
||||||
<i class="fas fa-lock-open fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (mdRepresentation) {
|
@if (mdRepresentation) {
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a class="me-2" target="_blank" [routerLink]="mdRepresentationItemRoute$ | async">{{ mdRepresentationName$ | async }}</a>
|
<a class="me-2" target="_blank"
|
||||||
|
[routerLink]="mdRepresentationItemRoute$ | async">{{ mdRepresentationName$ | async }}</a>
|
||||||
<ds-type-badge [object]="mdRepresentation"></ds-type-badge>
|
<ds-type-badge [object]="mdRepresentation"></ds-type-badge>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="ds-flex-cell ds-lang-cell" role="cell">
|
<div class="ds-flex-cell ds-lang-cell" role="cell">
|
||||||
@if (!mdValue.editing) {
|
|
||||||
<div class="dont-break-out preserve-line-breaks">{{ mdValue.newValue.language }}</div>
|
|
||||||
}
|
|
||||||
@if (mdValue.editing) {
|
@if (mdValue.editing) {
|
||||||
<input class="form-control" type="text" [(ngModel)]="mdValue.newValue.language"
|
<input class="form-control" type="text" [(ngModel)]="mdValue.newValue.language"
|
||||||
[attr.aria-label]="(dsoType + '.edit.metadata.edit.language') | translate"
|
[attr.aria-label]="(dsoType + '.edit.metadata.edit.language') | translate"
|
||||||
[dsDebounce]="300" (onDebounce)="confirm.emit(false)"/>
|
[dsDebounce]="300" (onDebounce)="confirm.emit(false)"/>
|
||||||
|
} @else {
|
||||||
|
<div class="dont-break-out preserve-line-breaks">{{ mdValue.newValue.language }}</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center ds-flex-cell ds-edit-cell" role="cell">
|
<div class="text-center ds-flex-cell ds-edit-cell" role="cell">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<div class="edit-field">
|
<div class="edit-field">
|
||||||
<div class="btn-group edit-buttons" [ngbTooltip]="isVirtual ? (dsoType + '.edit.metadata.edit.buttons.virtual' | translate) : null">
|
<div class="btn-group edit-buttons"
|
||||||
@if (!mdValue.editing) {
|
[ngbTooltip]="isVirtual ? (dsoType + '.edit.metadata.edit.buttons.virtual' | translate) : null">
|
||||||
<button class="btn btn-outline-primary btn-sm ng-star-inserted" data-test="metadata-edit-btn"
|
@let saving = (saving$ | async);
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.edit' | translate"
|
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}"
|
|
||||||
[dsBtnDisabled]="isVirtual || mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || (saving$ | async)" (click)="edit.emit()">
|
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
@if (mdValue.editing) {
|
@if (mdValue.editing) {
|
||||||
<button class="btn btn-outline-success btn-sm ng-star-inserted" data-test="metadata-confirm-btn"
|
<button class="btn btn-outline-success btn-sm ng-star-inserted" data-test="metadata-confirm-btn"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.confirm' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.confirm' | translate"
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}"
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}"
|
||||||
[dsBtnDisabled]="isVirtual || (saving$ | async)" (click)="confirm.emit(true)">
|
[dsBtnDisabled]="isVirtual || saving" (click)="confirm.emit(true)">
|
||||||
<i class="fas fa-check fa-fw"></i>
|
<i class="fas fa-check fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button class="btn btn-outline-primary btn-sm ng-star-inserted" data-test="metadata-edit-btn"
|
||||||
|
[title]="dsoType + '.edit.metadata.edit.buttons.edit' | translate"
|
||||||
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}"
|
||||||
|
[dsBtnDisabled]="isVirtual || mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || saving"
|
||||||
|
(click)="edit.emit()">
|
||||||
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
<button class="btn btn-outline-danger btn-sm" data-test="metadata-remove-btn"
|
<button class="btn btn-outline-danger btn-sm" data-test="metadata-remove-btn"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.remove' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.remove' | translate"
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}"
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}"
|
||||||
[dsBtnDisabled]="isVirtual || (mdValue.change && mdValue.change !== DsoEditMetadataChangeTypeEnum.ADD) || mdValue.editing || (saving$ | async)" (click)="remove.emit()">
|
[dsBtnDisabled]="isVirtual || (mdValue.change && mdValue.change !== DsoEditMetadataChangeTypeEnum.ADD) || mdValue.editing || saving"
|
||||||
|
(click)="remove.emit()">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-warning btn-sm" data-test="metadata-undo-btn"
|
<button class="btn btn-outline-warning btn-sm" data-test="metadata-undo-btn"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.undo' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.undo' | translate"
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}"
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}"
|
||||||
[dsBtnDisabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()">
|
[dsBtnDisabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || saving"
|
||||||
|
(click)="undo.emit()">
|
||||||
<i class="fas fa-undo-alt fa-fw"></i>
|
<i class="fas fa-undo-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn" *ngVar="(isOnlyValue || (saving$ | async)) as disabled"
|
<button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn"
|
||||||
cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [dsBtnDisabled]="disabled"
|
cdkDragHandle [cdkDragHandleDisabled]="isOnlyValue || saving"
|
||||||
|
[class.disabled]="isOnlyValue || saving" [dsBtnDisabled]="isOnlyValue || saving"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
|
||||||
<i class="fas fa-grip-vertical fa-fw"></i>
|
<i class="fas fa-grip-vertical fa-fw"></i>
|
||||||
|
@@ -8,42 +8,27 @@ import {
|
|||||||
waitForAsync,
|
waitForAsync,
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterModule } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { MetadataField } from 'src/app/core/metadata/metadata-field.model';
|
|
||||||
import { MetadataSchema } from 'src/app/core/metadata/metadata-schema.model';
|
|
||||||
import { RegistryService } from 'src/app/core/registry/registry.service';
|
|
||||||
import { ConfidenceType } from 'src/app/core/shared/confidence-type';
|
|
||||||
import { Vocabulary } from 'src/app/core/submission/vocabularies/models/vocabulary.model';
|
|
||||||
import { VocabularyService } from 'src/app/core/submission/vocabularies/vocabulary.service';
|
|
||||||
import { DynamicOneboxModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
|
|
||||||
import { DynamicScrollableDropdownModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
|
||||||
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
|
|
||||||
import { createPaginatedList } from 'src/app/shared/testing/utils.test';
|
|
||||||
import { VocabularyServiceStub } from 'src/app/shared/testing/vocabulary-service.stub';
|
|
||||||
|
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
|
||||||
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
|
||||||
import { Item } from '../../../core/shared/item.model';
|
|
||||||
import {
|
import {
|
||||||
MetadataValue,
|
MetadataValue,
|
||||||
VIRTUAL_METADATA_PREFIX,
|
VIRTUAL_METADATA_PREFIX,
|
||||||
} from '../../../core/shared/metadata.models';
|
} from '../../../core/shared/metadata.models';
|
||||||
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||||
import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
|
|
||||||
import { DsDynamicScrollableDropdownComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
|
||||||
import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component';
|
import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { DsoEditMetadataFieldServiceStub } from '../../../shared/testing/dso-edit-metadata-field.service.stub';
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
import {
|
import {
|
||||||
DsoEditMetadataChangeType,
|
DsoEditMetadataChangeType,
|
||||||
DsoEditMetadataValue,
|
DsoEditMetadataValue,
|
||||||
} from '../dso-edit-metadata-form';
|
} from '../dso-edit-metadata-form';
|
||||||
|
import { DsoEditMetadataFieldService } from '../dso-edit-metadata-value-field/dso-edit-metadata-field.service';
|
||||||
|
import { DsoEditMetadataValueFieldLoaderComponent } from '../dso-edit-metadata-value-field/dso-edit-metadata-value-field-loader/dso-edit-metadata-value-field-loader.component';
|
||||||
import { DsoEditMetadataValueComponent } from './dso-edit-metadata-value.component';
|
import { DsoEditMetadataValueComponent } from './dso-edit-metadata-value.component';
|
||||||
|
|
||||||
const EDIT_BTN = 'edit';
|
const EDIT_BTN = 'edit';
|
||||||
@@ -58,97 +43,12 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
|
|
||||||
let relationshipService: RelationshipDataService;
|
let relationshipService: RelationshipDataService;
|
||||||
let dsoNameService: DSONameService;
|
let dsoNameService: DSONameService;
|
||||||
let vocabularyServiceStub: any;
|
let dsoEditMetadataFieldService: DsoEditMetadataFieldServiceStub;
|
||||||
let itemService: ItemDataService;
|
|
||||||
let registryService: RegistryService;
|
|
||||||
let notificationsService: NotificationsService;
|
|
||||||
|
|
||||||
let editMetadataValue: DsoEditMetadataValue;
|
let editMetadataValue: DsoEditMetadataValue;
|
||||||
let metadataValue: MetadataValue;
|
let metadataValue: MetadataValue;
|
||||||
let dso: DSpaceObject;
|
|
||||||
|
|
||||||
const collection = Object.assign(new Collection(), {
|
|
||||||
uuid: 'fake-uuid',
|
|
||||||
});
|
|
||||||
|
|
||||||
const item = Object.assign(new Item(), {
|
|
||||||
_links: {
|
|
||||||
self: { href: 'fake-item-url/item' },
|
|
||||||
},
|
|
||||||
id: 'item',
|
|
||||||
uuid: 'item',
|
|
||||||
owningCollection: createSuccessfulRemoteDataObject$(collection),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockVocabularyScrollable: Vocabulary = {
|
|
||||||
id: 'scrollable',
|
|
||||||
name: 'scrollable',
|
|
||||||
scrollable: true,
|
|
||||||
hierarchical: false,
|
|
||||||
preloadLevel: 0,
|
|
||||||
type: 'vocabulary',
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href: 'self',
|
|
||||||
},
|
|
||||||
entries: {
|
|
||||||
href: 'entries',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockVocabularyHierarchical: Vocabulary = {
|
|
||||||
id: 'hierarchical',
|
|
||||||
name: 'hierarchical',
|
|
||||||
scrollable: false,
|
|
||||||
hierarchical: true,
|
|
||||||
preloadLevel: 2,
|
|
||||||
type: 'vocabulary',
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href: 'self',
|
|
||||||
},
|
|
||||||
entries: {
|
|
||||||
href: 'entries',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockVocabularySuggester: Vocabulary = {
|
|
||||||
id: 'suggester',
|
|
||||||
name: 'suggester',
|
|
||||||
scrollable: false,
|
|
||||||
hierarchical: false,
|
|
||||||
preloadLevel: 0,
|
|
||||||
type: 'vocabulary',
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href: 'self',
|
|
||||||
},
|
|
||||||
entries: {
|
|
||||||
href: 'entries',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let metadataSchema: MetadataSchema;
|
|
||||||
let metadataFields: MetadataField[];
|
|
||||||
|
|
||||||
function initServices(): void {
|
function initServices(): void {
|
||||||
metadataSchema = Object.assign(new MetadataSchema(), {
|
|
||||||
id: 0,
|
|
||||||
prefix: 'metadata',
|
|
||||||
namespace: 'http://example.com/',
|
|
||||||
});
|
|
||||||
metadataFields = [
|
|
||||||
Object.assign(new MetadataField(), {
|
|
||||||
id: 0,
|
|
||||||
element: 'regular',
|
|
||||||
qualifier: null,
|
|
||||||
schema: createSuccessfulRemoteDataObject$(metadataSchema),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
relationshipService = jasmine.createSpyObj('relationshipService', {
|
relationshipService = jasmine.createSpyObj('relationshipService', {
|
||||||
resolveMetadataRepresentation: of(
|
resolveMetadataRepresentation: of(
|
||||||
new ItemMetadataRepresentation(metadataValue),
|
new ItemMetadataRepresentation(metadataValue),
|
||||||
@@ -157,14 +57,7 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
dsoNameService = jasmine.createSpyObj('dsoNameService', {
|
dsoNameService = jasmine.createSpyObj('dsoNameService', {
|
||||||
getName: 'Related Name',
|
getName: 'Related Name',
|
||||||
});
|
});
|
||||||
itemService = jasmine.createSpyObj('itemService', {
|
dsoEditMetadataFieldService = new DsoEditMetadataFieldServiceStub();
|
||||||
findByHref: createSuccessfulRemoteDataObject$(item),
|
|
||||||
});
|
|
||||||
vocabularyServiceStub = new VocabularyServiceStub();
|
|
||||||
registryService = jasmine.createSpyObj('registryService', {
|
|
||||||
queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)),
|
|
||||||
});
|
|
||||||
notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(async () => {
|
beforeEach(waitForAsync(async () => {
|
||||||
@@ -175,18 +68,13 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
authority: undefined,
|
authority: undefined,
|
||||||
});
|
});
|
||||||
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
dso = Object.assign(new DSpaceObject(), {
|
|
||||||
_links: {
|
|
||||||
self: { href: 'fake-dso-url/dso' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
initServices();
|
initServices();
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
RouterTestingModule.withRoutes([]),
|
RouterModule.forRoot([]),
|
||||||
DsoEditMetadataValueComponent,
|
DsoEditMetadataValueComponent,
|
||||||
VarDirective,
|
VarDirective,
|
||||||
BtnDisabledDirective,
|
BtnDisabledDirective,
|
||||||
@@ -194,16 +82,16 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: RelationshipDataService, useValue: relationshipService },
|
{ provide: RelationshipDataService, useValue: relationshipService },
|
||||||
{ provide: DSONameService, useValue: dsoNameService },
|
{ provide: DSONameService, useValue: dsoNameService },
|
||||||
{ provide: VocabularyService, useValue: vocabularyServiceStub },
|
{ provide: DsoEditMetadataFieldService, useValue: dsoEditMetadataFieldService },
|
||||||
{ provide: ItemDataService, useValue: itemService },
|
|
||||||
{ provide: RegistryService, useValue: registryService },
|
|
||||||
{ provide: NotificationsService, useValue: notificationsService },
|
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(DsoEditMetadataValueComponent, {
|
.overrideComponent(DsoEditMetadataValueComponent, {
|
||||||
remove: {
|
remove: {
|
||||||
imports: [DsDynamicOneboxComponent, DsDynamicScrollableDropdownComponent, ThemedTypeBadgeComponent],
|
imports: [
|
||||||
|
DsoEditMetadataValueFieldLoaderComponent,
|
||||||
|
ThemedTypeBadgeComponent,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
@@ -213,7 +101,6 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
fixture = TestBed.createComponent(DsoEditMetadataValueComponent);
|
fixture = TestBed.createComponent(DsoEditMetadataValueComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.mdValue = editMetadataValue;
|
component.mdValue = editMetadataValue;
|
||||||
component.dso = dso;
|
|
||||||
component.saving$ = of(false);
|
component.saving$ = of(false);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -299,219 +186,6 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
assertButton(DRAG_BTN, true, false);
|
assertButton(DRAG_BTN, true, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the metadata field not uses a vocabulary and is editing', () => {
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(null, 204));
|
|
||||||
metadataValue = Object.assign(new MetadataValue(), {
|
|
||||||
value: 'Regular value',
|
|
||||||
language: 'en',
|
|
||||||
place: 0,
|
|
||||||
authority: null,
|
|
||||||
});
|
|
||||||
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
|
||||||
editMetadataValue.editing = true;
|
|
||||||
component.mdValue = editMetadataValue;
|
|
||||||
component.mdField = 'metadata.regular';
|
|
||||||
component.ngOnInit();
|
|
||||||
fixture.detectChanges();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should render a textarea', () => {
|
|
||||||
expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
|
||||||
expect(fixture.debugElement.query(By.css('textarea'))).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the metadata field uses a scrollable vocabulary and is editing', () => {
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyScrollable));
|
|
||||||
metadataValue = Object.assign(new MetadataValue(), {
|
|
||||||
value: 'Authority Controlled value',
|
|
||||||
language: 'en',
|
|
||||||
place: 0,
|
|
||||||
authority: null,
|
|
||||||
});
|
|
||||||
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
|
||||||
editMetadataValue.editing = true;
|
|
||||||
component.mdValue = editMetadataValue;
|
|
||||||
component.mdField = 'metadata.scrollable';
|
|
||||||
component.ngOnInit();
|
|
||||||
fixture.detectChanges();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should render the DsDynamicScrollableDropdownComponent', () => {
|
|
||||||
expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
|
||||||
expect(fixture.debugElement.query(By.css('ds-dynamic-scrollable-dropdown'))).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getModel should return a DynamicScrollableDropdownModel', () => {
|
|
||||||
const model = component.getModel();
|
|
||||||
|
|
||||||
expect(model instanceof DynamicScrollableDropdownModel).toBe(true);
|
|
||||||
expect(model.vocabularyOptions.name).toBe(mockVocabularyScrollable.name);
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the metadata field uses a hierarchical vocabulary and is editing', () => {
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyHierarchical));
|
|
||||||
metadataValue = Object.assign(new MetadataValue(), {
|
|
||||||
value: 'Authority Controlled value',
|
|
||||||
language: 'en',
|
|
||||||
place: 0,
|
|
||||||
authority: null,
|
|
||||||
});
|
|
||||||
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
|
||||||
editMetadataValue.editing = true;
|
|
||||||
component.mdValue = editMetadataValue;
|
|
||||||
component.mdField = 'metadata.hierarchical';
|
|
||||||
component.ngOnInit();
|
|
||||||
fixture.detectChanges();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should render the DsDynamicOneboxComponent', () => {
|
|
||||||
expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
|
||||||
expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getModel should return a DynamicOneboxModel', () => {
|
|
||||||
const model = component.getModel();
|
|
||||||
|
|
||||||
expect(model instanceof DynamicOneboxModel).toBe(true);
|
|
||||||
expect(model.vocabularyOptions.name).toBe(mockVocabularyHierarchical.name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the metadata field uses a suggester vocabulary and is editing', () => {
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularySuggester));
|
|
||||||
spyOn(component.confirm, 'emit');
|
|
||||||
metadataValue = Object.assign(new MetadataValue(), {
|
|
||||||
value: 'Authority Controlled value',
|
|
||||||
language: 'en',
|
|
||||||
place: 0,
|
|
||||||
authority: 'authority-key',
|
|
||||||
confidence: ConfidenceType.CF_UNCERTAIN,
|
|
||||||
});
|
|
||||||
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
|
||||||
editMetadataValue.editing = true;
|
|
||||||
component.mdValue = editMetadataValue;
|
|
||||||
component.mdField = 'metadata.suggester';
|
|
||||||
component.ngOnInit();
|
|
||||||
fixture.detectChanges();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should render the DsDynamicOneboxComponent', () => {
|
|
||||||
expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
|
||||||
expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getModel should return a DynamicOneboxModel', () => {
|
|
||||||
const model = component.getModel();
|
|
||||||
|
|
||||||
expect(model instanceof DynamicOneboxModel).toBe(true);
|
|
||||||
expect(model.vocabularyOptions.name).toBe(mockVocabularySuggester.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authority key edition', () => {
|
|
||||||
|
|
||||||
it('should update confidence to CF_NOVALUE when authority is cleared', () => {
|
|
||||||
component.mdValue.newValue.authority = '';
|
|
||||||
|
|
||||||
component.onChangeAuthorityKey();
|
|
||||||
|
|
||||||
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_NOVALUE);
|
|
||||||
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update confidence to CF_ACCEPTED when authority key is edited', () => {
|
|
||||||
component.mdValue.newValue.authority = 'newAuthority';
|
|
||||||
component.mdValue.originalValue.authority = 'oldAuthority';
|
|
||||||
|
|
||||||
component.onChangeAuthorityKey();
|
|
||||||
|
|
||||||
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED);
|
|
||||||
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not update confidence when authority key remains the same', () => {
|
|
||||||
component.mdValue.newValue.authority = 'sameAuthority';
|
|
||||||
component.mdValue.originalValue.authority = 'sameAuthority';
|
|
||||||
|
|
||||||
component.onChangeAuthorityKey();
|
|
||||||
|
|
||||||
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNCERTAIN);
|
|
||||||
expect(component.confirm.emit).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call onChangeEditingAuthorityStatus with true when clicking the lock button', () => {
|
|
||||||
spyOn(component, 'onChangeEditingAuthorityStatus');
|
|
||||||
const lockButton = fixture.nativeElement.querySelector('#metadata-confirm-btn');
|
|
||||||
|
|
||||||
lockButton.click();
|
|
||||||
|
|
||||||
expect(component.onChangeEditingAuthorityStatus).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disable the input when editingAuthority is false', (done) => {
|
|
||||||
component.editingAuthority = false;
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
const inputElement = fixture.nativeElement.querySelector('input[data-test="authority-input"]');
|
|
||||||
expect(inputElement.disabled).toBeTruthy();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should enable the input when editingAuthority is true', (done) => {
|
|
||||||
component.editingAuthority = true;
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
const inputElement = fixture.nativeElement.querySelector('input[data-test="authority-input"]');
|
|
||||||
expect(inputElement.disabled).toBeFalsy();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update mdValue.newValue properties when authority is present', () => {
|
|
||||||
const event = {
|
|
||||||
value: 'Some value',
|
|
||||||
authority: 'Some authority',
|
|
||||||
};
|
|
||||||
|
|
||||||
component.onChangeAuthorityField(event);
|
|
||||||
|
|
||||||
expect(component.mdValue.newValue.value).toBe(event.value);
|
|
||||||
expect(component.mdValue.newValue.authority).toBe(event.authority);
|
|
||||||
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED);
|
|
||||||
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update mdValue.newValue properties when authority is not present', () => {
|
|
||||||
const event = {
|
|
||||||
value: 'Some value',
|
|
||||||
authority: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
component.onChangeAuthorityField(event);
|
|
||||||
|
|
||||||
expect(component.mdValue.newValue.value).toBe(event.value);
|
|
||||||
expect(component.mdValue.newValue.authority).toBeNull();
|
|
||||||
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNSET);
|
|
||||||
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
function assertButton(name: string, exists: boolean, disabled: boolean = false): void {
|
function assertButton(name: string, exists: boolean, disabled: boolean = false): void {
|
||||||
describe(`${name} button`, () => {
|
describe(`${name} button`, () => {
|
||||||
let btn: DebugElement;
|
let btn: DebugElement;
|
||||||
|
@@ -7,7 +7,6 @@ import {
|
|||||||
NgClass,
|
NgClass,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
@@ -16,96 +15,67 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { FormsModule } from '@angular/forms';
|
||||||
FormsModule,
|
|
||||||
UntypedFormControl,
|
|
||||||
UntypedFormGroup,
|
|
||||||
} from '@angular/forms';
|
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
TranslateModule,
|
|
||||||
TranslateService,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import {
|
|
||||||
BehaviorSubject,
|
|
||||||
EMPTY,
|
EMPTY,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import { map } from 'rxjs/operators';
|
||||||
map,
|
|
||||||
switchMap,
|
|
||||||
take,
|
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
import { RegistryService } from 'src/app/core/registry/registry.service';
|
|
||||||
import { VocabularyService } from 'src/app/core/submission/vocabularies/vocabulary.service';
|
|
||||||
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
|
|
||||||
|
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
|
||||||
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||||
import { MetadataService } from '../../../core/metadata/metadata.service';
|
import { MetadataService } from '../../../core/metadata/metadata.service';
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
|
||||||
import { ConfidenceType } from '../../../core/shared/confidence-type';
|
import { ConfidenceType } from '../../../core/shared/confidence-type';
|
||||||
|
import { Context } from '../../../core/shared/context.model';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
|
||||||
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
import {
|
import {
|
||||||
MetadataRepresentation,
|
MetadataRepresentation,
|
||||||
MetadataRepresentationType,
|
MetadataRepresentationType,
|
||||||
} from '../../../core/shared/metadata-representation/metadata-representation.model';
|
} from '../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
import {
|
|
||||||
getFirstCompletedRemoteData,
|
|
||||||
getFirstSucceededRemoteData,
|
|
||||||
getFirstSucceededRemoteDataPayload,
|
|
||||||
getRemoteDataPayload,
|
|
||||||
metadataFieldsToString,
|
|
||||||
} from '../../../core/shared/operators';
|
|
||||||
import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model';
|
import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model';
|
||||||
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
|
||||||
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
|
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
|
||||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||||
import { isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
|
|
||||||
import {
|
|
||||||
DsDynamicOneboxModelConfig,
|
|
||||||
DynamicOneboxModel,
|
|
||||||
} from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
|
|
||||||
import { DsDynamicScrollableDropdownComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
|
||||||
import {
|
|
||||||
DynamicScrollableDropdownModel,
|
|
||||||
DynamicScrollableDropdownModelConfig,
|
|
||||||
} from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
|
||||||
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
|
|
||||||
import { AuthorityConfidenceStateDirective } from '../../../shared/form/directives/authority-confidence-state.directive';
|
import { AuthorityConfidenceStateDirective } from '../../../shared/form/directives/authority-confidence-state.directive';
|
||||||
import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component';
|
import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component';
|
||||||
import { DebounceDirective } from '../../../shared/utils/debounce.directive';
|
import { DebounceDirective } from '../../../shared/utils/debounce.directive';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
|
||||||
import {
|
import {
|
||||||
DsoEditMetadataChangeType,
|
DsoEditMetadataChangeType,
|
||||||
DsoEditMetadataValue,
|
DsoEditMetadataValue,
|
||||||
} from '../dso-edit-metadata-form';
|
} from '../dso-edit-metadata-form';
|
||||||
|
import { DsoEditMetadataFieldService } from '../dso-edit-metadata-value-field/dso-edit-metadata-field.service';
|
||||||
|
import { EditMetadataValueFieldType } from '../dso-edit-metadata-value-field/dso-edit-metadata-field-type.enum';
|
||||||
|
import { DsoEditMetadataValueFieldLoaderComponent } from '../dso-edit-metadata-value-field/dso-edit-metadata-value-field-loader/dso-edit-metadata-value-field-loader.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dso-edit-metadata-value',
|
selector: 'ds-dso-edit-metadata-value',
|
||||||
styleUrls: ['./dso-edit-metadata-value.component.scss', '../dso-edit-metadata-shared/dso-edit-metadata-cells.scss'],
|
styleUrls: ['./dso-edit-metadata-value.component.scss', '../dso-edit-metadata-shared/dso-edit-metadata-cells.scss'],
|
||||||
templateUrl: './dso-edit-metadata-value.component.html',
|
templateUrl: './dso-edit-metadata-value.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [VarDirective, CdkDrag, NgClass, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, DsDynamicScrollableDropdownComponent, DsDynamicOneboxComponent, AuthorityConfidenceStateDirective, BtnDisabledDirective],
|
imports: [CdkDrag, NgClass, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, AuthorityConfidenceStateDirective, BtnDisabledDirective, DsoEditMetadataValueFieldLoaderComponent],
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component displaying a single editable row for a metadata value
|
* Component displaying a single editable row for a metadata value
|
||||||
*/
|
*/
|
||||||
export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
|
@Input() context: Context;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The parent {@link DSpaceObject} to display a metadata form for
|
* The parent {@link DSpaceObject} to display a metadata form for
|
||||||
* Also used to determine metadata-representations in case of virtual metadata
|
* Also used to determine metadata-representations in case of virtual metadata
|
||||||
*/
|
*/
|
||||||
@Input() dso: DSpaceObject;
|
@Input() dso: DSpaceObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata field that is being edited
|
||||||
|
*/
|
||||||
|
@Input() mdField: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editable metadata value to show
|
* Editable metadata value to show
|
||||||
*/
|
*/
|
||||||
@@ -129,11 +99,6 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
@Input() isOnlyValue = false;
|
@Input() isOnlyValue = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* MetadataField to edit
|
|
||||||
*/
|
|
||||||
@Input() mdField?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits when the user clicked edit
|
* Emits when the user clicked edit
|
||||||
*/
|
*/
|
||||||
@@ -165,12 +130,6 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType;
|
public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType;
|
||||||
|
|
||||||
/**
|
|
||||||
* The ConfidenceType enumeration for access in the component's template
|
|
||||||
* @type {ConfidenceType}
|
|
||||||
*/
|
|
||||||
public ConfidenceTypeEnum = ConfidenceType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The item this metadata value represents in case it's virtual (if any, otherwise null)
|
* The item this metadata value represents in case it's virtual (if any, otherwise null)
|
||||||
*/
|
*/
|
||||||
@@ -187,56 +146,28 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
|||||||
mdRepresentationName$: Observable<string | null>;
|
mdRepresentationName$: Observable<string | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the authority field is currently being edited
|
* The type of edit field that should be displayed
|
||||||
*/
|
*/
|
||||||
public editingAuthority = false;
|
fieldType$: Observable<EditMetadataValueFieldType>;
|
||||||
|
|
||||||
|
readonly ConfidenceTypeEnum = ConfidenceType;
|
||||||
/**
|
|
||||||
* Whether or not the free-text editing is enabled when scrollable dropdown or hierarchical vocabulary is used
|
|
||||||
*/
|
|
||||||
public enabledFreeTextEditing = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Field group used by authority field
|
|
||||||
* @type {UntypedFormGroup}
|
|
||||||
*/
|
|
||||||
group = new UntypedFormGroup({ authorityField : new UntypedFormControl() });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Model to use for editing authorities values
|
|
||||||
*/
|
|
||||||
private model$: BehaviorSubject<DynamicOneboxModel | DynamicScrollableDropdownModel> = new BehaviorSubject(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observable with information about the authority vocabulary used
|
|
||||||
*/
|
|
||||||
private vocabulary$: Observable<Vocabulary>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observables with information about the authority vocabulary type used
|
|
||||||
*/
|
|
||||||
private isAuthorityControlled$: Observable<boolean>;
|
|
||||||
private isHierarchicalVocabulary$: Observable<boolean>;
|
|
||||||
private isScrollableVocabulary$: Observable<boolean>;
|
|
||||||
private isSuggesterVocabulary$: Observable<boolean>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected relationshipService: RelationshipDataService,
|
protected relationshipService: RelationshipDataService,
|
||||||
protected dsoNameService: DSONameService,
|
protected dsoNameService: DSONameService,
|
||||||
protected vocabularyService: VocabularyService,
|
|
||||||
protected itemService: ItemDataService,
|
|
||||||
protected cdr: ChangeDetectorRef,
|
|
||||||
protected registryService: RegistryService,
|
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected translate: TranslateService,
|
|
||||||
protected metadataService: MetadataService,
|
protected metadataService: MetadataService,
|
||||||
|
protected dsoEditMetadataFieldService: DsoEditMetadataFieldService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.initVirtualProperties();
|
this.initVirtualProperties();
|
||||||
this.initAuthorityProperties();
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes.mdField) {
|
||||||
|
this.fieldType$ = this.getFieldType();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,252 +190,20 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialise potential properties of a authority controlled metadata field
|
* Retrieves the {@link EditMetadataValueFieldType} to be displayed for the current field while in edit mode.
|
||||||
*/
|
*/
|
||||||
initAuthorityProperties(): void {
|
getFieldType(): Observable<EditMetadataValueFieldType> {
|
||||||
|
return this.dsoEditMetadataFieldService.findDsoFieldVocabulary(this.dso, this.mdField).pipe(
|
||||||
if (isNotEmpty(this.mdField)) {
|
map((vocabulary: Vocabulary) => {
|
||||||
|
if (hasValue(vocabulary)) {
|
||||||
const owningCollection$: Observable<Collection> = this.itemService.findByHref(this.dso._links.self.href, true, true, followLink('owningCollection'))
|
return EditMetadataValueFieldType.AUTHORITY;
|
||||||
.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
switchMap((item: Item) => item.owningCollection),
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.vocabulary$ = owningCollection$.pipe(
|
|
||||||
switchMap((c: Collection) => this.vocabularyService
|
|
||||||
.getVocabularyByMetadataAndCollection(this.mdField, c.uuid)
|
|
||||||
.pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload(),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.vocabulary$ = observableOf(undefined);
|
|
||||||
}
|
}
|
||||||
|
if (this.mdField === 'dspace.entity.type') {
|
||||||
this.isAuthorityControlled$ = this.vocabulary$.pipe(
|
return EditMetadataValueFieldType.ENTITY_TYPE;
|
||||||
// Create the model used by the authority fields to ensure its existence when the field is initialized
|
|
||||||
tap((v: Vocabulary) => this.model$.next(this.createModel(v))),
|
|
||||||
map((result: Vocabulary) => isNotEmpty(result)),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.isHierarchicalVocabulary$ = this.vocabulary$.pipe(
|
|
||||||
map((result: Vocabulary) => isNotEmpty(result) && result.hierarchical),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.isScrollableVocabulary$ = this.vocabulary$.pipe(
|
|
||||||
map((result: Vocabulary) => isNotEmpty(result) && result.scrollable),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.isSuggesterVocabulary$ = this.vocabulary$.pipe(
|
|
||||||
map((result: Vocabulary) => isNotEmpty(result) && !result.hierarchical && !result.scrollable),
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model based on the
|
|
||||||
* vocabulary used.
|
|
||||||
*/
|
|
||||||
private createModel(vocabulary: Vocabulary): DynamicOneboxModel | DynamicScrollableDropdownModel {
|
|
||||||
if (isNotEmpty(vocabulary)) {
|
|
||||||
let formFieldValue;
|
|
||||||
if (isNotEmpty(this.mdValue.newValue.value)) {
|
|
||||||
formFieldValue = new FormFieldMetadataValueObject();
|
|
||||||
formFieldValue.value = this.mdValue.newValue.value;
|
|
||||||
formFieldValue.display = this.mdValue.newValue.value;
|
|
||||||
if (this.mdValue.newValue.authority) {
|
|
||||||
formFieldValue.authority = this.mdValue.newValue.authority;
|
|
||||||
formFieldValue.confidence = this.mdValue.newValue.confidence;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
formFieldValue = this.mdValue.newValue.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vocabularyOptions = vocabulary ? {
|
|
||||||
closed: false,
|
|
||||||
name: vocabulary.name,
|
|
||||||
} as VocabularyOptions : null;
|
|
||||||
|
|
||||||
if (!vocabulary.scrollable) {
|
|
||||||
const model: DsDynamicOneboxModelConfig = {
|
|
||||||
id: 'authorityField',
|
|
||||||
label: `${this.dsoType}.edit.metadata.edit.value`,
|
|
||||||
vocabularyOptions: vocabularyOptions,
|
|
||||||
metadataFields: [this.mdField],
|
|
||||||
value: formFieldValue,
|
|
||||||
repeatable: false,
|
|
||||||
submissionId: 'edit-metadata',
|
|
||||||
hasSelectableMetadata: false,
|
|
||||||
};
|
|
||||||
return new DynamicOneboxModel(model);
|
|
||||||
} else {
|
|
||||||
const model: DynamicScrollableDropdownModelConfig = {
|
|
||||||
id: 'authorityField',
|
|
||||||
label: `${this.dsoType}.edit.metadata.edit.value`,
|
|
||||||
placeholder: `${this.dsoType}.edit.metadata.edit.value`,
|
|
||||||
vocabularyOptions: vocabularyOptions,
|
|
||||||
metadataFields: [this.mdField],
|
|
||||||
value: formFieldValue,
|
|
||||||
repeatable: false,
|
|
||||||
submissionId: 'edit-metadata',
|
|
||||||
hasSelectableMetadata: false,
|
|
||||||
maxOptions: 10,
|
|
||||||
};
|
|
||||||
return new DynamicScrollableDropdownModel(model);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change callback for the component. Check if the mdField has changed to retrieve whether it is metadata
|
|
||||||
* that uses a controlled vocabulary and update the related properties
|
|
||||||
*
|
|
||||||
* @param {SimpleChanges} changes
|
|
||||||
*/
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
|
||||||
if (isNotEmpty(changes.mdField) && !changes.mdField.firstChange) {
|
|
||||||
if (isNotEmpty(changes.mdField.currentValue) ) {
|
|
||||||
if (isNotEmpty(changes.mdField.previousValue) &&
|
|
||||||
changes.mdField.previousValue !== changes.mdField.currentValue) {
|
|
||||||
// Clear authority value in case it has been assigned with the previous metadataField used
|
|
||||||
this.mdValue.newValue.authority = null;
|
|
||||||
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only ask if the current mdField have a period character to reduce request
|
|
||||||
if (changes.mdField.currentValue.includes('.')) {
|
|
||||||
this.validateMetadataField().subscribe((isValid: boolean) => {
|
|
||||||
if (isValid) {
|
|
||||||
this.initAuthorityProperties();
|
|
||||||
this.cdr.detectChanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the metadata field to check if it exists on the server and return an observable boolean for success/error
|
|
||||||
*/
|
|
||||||
validateMetadataField(): Observable<boolean> {
|
|
||||||
return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
switchMap((rd) => {
|
|
||||||
if (rd.hasSucceeded) {
|
|
||||||
return observableOf(rd).pipe(
|
|
||||||
metadataFieldsToString(),
|
|
||||||
take(1),
|
|
||||||
map((fields: string[]) => fields.indexOf(this.mdField) > -1),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.notificationsService.error(this.translate.instant(`${this.dsoType}.edit.metadata.metadatafield.error`), rd.errorMessage);
|
|
||||||
return [false];
|
|
||||||
}
|
}
|
||||||
|
return EditMetadataValueFieldType.PLAIN_TEXT;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this field use a authority vocabulary
|
|
||||||
*/
|
|
||||||
isAuthorityControlled(): Observable<boolean> {
|
|
||||||
return this.isAuthorityControlled$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if configured vocabulary is Hierarchical or not
|
|
||||||
*/
|
|
||||||
isHierarchicalVocabulary(): Observable<boolean> {
|
|
||||||
return this.isHierarchicalVocabulary$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if configured vocabulary is Scrollable or not
|
|
||||||
*/
|
|
||||||
isScrollableVocabulary(): Observable<boolean> {
|
|
||||||
return this.isScrollableVocabulary$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if configured vocabulary is Suggester or not
|
|
||||||
* (a vocabulary not Scrollable and not Hierarchical that uses an autocomplete field)
|
|
||||||
*/
|
|
||||||
isSuggesterVocabulary(): Observable<boolean> {
|
|
||||||
return this.isSuggesterVocabulary$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process the change of authority field value updating the authority key and confidence as necessary
|
|
||||||
*/
|
|
||||||
onChangeAuthorityField(event): void {
|
|
||||||
if (event) {
|
|
||||||
this.mdValue.newValue.value = event.value;
|
|
||||||
if (event.authority) {
|
|
||||||
this.mdValue.newValue.authority = event.authority;
|
|
||||||
this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED;
|
|
||||||
} else {
|
|
||||||
this.mdValue.newValue.authority = null;
|
|
||||||
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
|
||||||
}
|
|
||||||
this.confirm.emit(false);
|
|
||||||
} else {
|
|
||||||
// The event is undefined when the user clears the selection in scrollable dropdown
|
|
||||||
this.mdValue.newValue.value = '';
|
|
||||||
this.mdValue.newValue.authority = null;
|
|
||||||
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
|
||||||
this.confirm.emit(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model used
|
|
||||||
* for the authority field
|
|
||||||
*/
|
|
||||||
getModel(): DynamicOneboxModel | DynamicScrollableDropdownModel {
|
|
||||||
return this.model$.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the status of the editingAuthority property
|
|
||||||
* @param status
|
|
||||||
*/
|
|
||||||
onChangeEditingAuthorityStatus(status: boolean) {
|
|
||||||
this.editingAuthority = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes the change in authority value, updating the confidence as necessary.
|
|
||||||
* If the authority key is cleared, the confidence is set to {@link ConfidenceType.CF_NOVALUE}.
|
|
||||||
* If the authority key is edited and differs from the original, the confidence is set to {@link ConfidenceType.CF_ACCEPTED}.
|
|
||||||
*/
|
|
||||||
onChangeAuthorityKey() {
|
|
||||||
if (this.mdValue.newValue.authority === '') {
|
|
||||||
this.mdValue.newValue.confidence = ConfidenceType.CF_NOVALUE;
|
|
||||||
this.confirm.emit(false);
|
|
||||||
} else if (this.mdValue.newValue.authority !== this.mdValue.originalValue.authority) {
|
|
||||||
this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED;
|
|
||||||
this.confirm.emit(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the free-text ediitng mode
|
|
||||||
*/
|
|
||||||
toggleFreeTextEdition() {
|
|
||||||
if (this.enabledFreeTextEditing) {
|
|
||||||
if (this.getModel().value !== this.mdValue.newValue.value) {
|
|
||||||
// Reload the model to adapt it to the new possible value modified during free text editing
|
|
||||||
this.initAuthorityProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.enabledFreeTextEditing = !this.enabledFreeTextEditing;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -43,8 +43,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1 ds-drop-list" role="cell">
|
<div class="flex-grow-1 ds-drop-list" role="cell">
|
||||||
<div role="table">
|
<div role="table">
|
||||||
<ds-dso-edit-metadata-value-headers role="presentation" [dsoType]="dsoType"></ds-dso-edit-metadata-value-headers>
|
<ds-dso-edit-metadata-value-headers role="presentation"
|
||||||
|
[dsoType]="dsoType"></ds-dso-edit-metadata-value-headers>
|
||||||
<ds-dso-edit-metadata-value [dso]="dso"
|
<ds-dso-edit-metadata-value [dso]="dso"
|
||||||
|
[context]="Context.AddMetadata"
|
||||||
[mdValue]="form.newValue"
|
[mdValue]="form.newValue"
|
||||||
[dsoType]="dsoType"
|
[dsoType]="dsoType"
|
||||||
[saving$]="savingOrLoadingFieldValidation$"
|
[saving$]="savingOrLoadingFieldValidation$"
|
||||||
@@ -93,7 +95,8 @@
|
|||||||
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
||||||
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
||||||
(click)="reinstate()">
|
(click)="reinstate()">
|
||||||
<i class="fas fa-undo-alt" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}
|
<i class="fas fa-undo-alt"
|
||||||
|
aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button class="btn btn-primary" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
|
<button class="btn btn-primary" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
|
||||||
|
@@ -38,6 +38,7 @@ import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analy
|
|||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { UpdateDataService } from '../../core/data/update-data.service';
|
import { UpdateDataService } from '../../core/data/update-data.service';
|
||||||
import { lazyDataService } from '../../core/lazy-data-service';
|
import { lazyDataService } from '../../core/lazy-data-service';
|
||||||
|
import { Context } from '../../core/shared/context.model';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
@@ -142,6 +143,8 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
dsoUpdateSubscription: Subscription;
|
dsoUpdateSubscription: Subscription;
|
||||||
|
|
||||||
|
public readonly Context = Context;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute,
|
constructor(protected route: ActivatedRoute,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected translateService: TranslateService,
|
protected translateService: TranslateService,
|
||||||
|
@@ -1,17 +1,16 @@
|
|||||||
<div #sdRef="ngbDropdown" ngbDropdown display="dynamic" placement="bottom-right" class="w-100">
|
<div #sdRef="ngbDropdown" ngbDropdown display="dynamic" placement="bottom-right" class="w-100">
|
||||||
<div class="position-relative right-addon"
|
<div class="position-relative right-addon"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
[attr.aria-label]="model.label"
|
[attr.aria-label]="model.label | translate"
|
||||||
[attr.aria-owns]="'combobox_' + id + '_listbox'"
|
[attr.aria-owns]="'combobox_' + id + '_listbox'"
|
||||||
[attr.aria-expanded]="sdRef.isOpen()"
|
[attr.aria-expanded]="sdRef.isOpen()"
|
||||||
[attr.aria-activedescendant]="(currentValue | async) ? 'combobox_' + id + '_selected' : null">
|
[attr.aria-activedescendant]="(currentValue | async) ? 'combobox_' + id + '_selected' : null">
|
||||||
@if (!model.readOnly) {
|
|
||||||
<i ngbDropdownToggle class="position-absolute scrollable-dropdown-toggle"
|
|
||||||
aria-hidden="true"></i>
|
|
||||||
}
|
|
||||||
@if (model.readOnly) {
|
@if (model.readOnly) {
|
||||||
<i class="dropdown-toggle position-absolute toggle-icon"
|
<i class="dropdown-toggle position-absolute toggle-icon"
|
||||||
aria-hidden="true"></i>
|
aria-hidden="true"></i>
|
||||||
|
} @else {
|
||||||
|
<i ngbDropdownToggle class="position-absolute scrollable-dropdown-toggle"
|
||||||
|
aria-hidden="true"></i>
|
||||||
}
|
}
|
||||||
<input class="form-control"
|
<input class="form-control"
|
||||||
[attr.aria-controls]="'combobox_' + id + '_listbox'"
|
[attr.aria-controls]="'combobox_' + id + '_listbox'"
|
||||||
@@ -33,11 +32,11 @@
|
|||||||
|
|
||||||
<div #dropdownMenu ngbDropdownMenu
|
<div #dropdownMenu ngbDropdownMenu
|
||||||
class="dropdown-menu scrollable-dropdown-menu w-100"
|
class="dropdown-menu scrollable-dropdown-menu w-100"
|
||||||
[attr.aria-label]="model.placeholder">
|
[attr.aria-label]="model.placeholder | translate">
|
||||||
<div class="scrollable-menu"
|
<div class="scrollable-menu"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
[id]="'combobox_' + id + '_listbox'"
|
[id]="'combobox_' + id + '_listbox'"
|
||||||
[attr.aria-label]="model.placeholder"
|
[attr.aria-label]="model.placeholder | translate"
|
||||||
infiniteScroll
|
infiniteScroll
|
||||||
[infiniteScrollDistance]="2"
|
[infiniteScrollDistance]="2"
|
||||||
[infiniteScrollThrottle]="50"
|
[infiniteScrollThrottle]="50"
|
||||||
|
@@ -29,10 +29,6 @@
|
|||||||
padding: 0.70rem 0.70rem 0 0.70rem ;
|
padding: 0.70rem 0.70rem 0 0.70rem ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable-dropdown-input[readonly]{
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-icon {
|
.toggle-icon {
|
||||||
padding: 0.7rem 0.7rem 0 0.7rem;
|
padding: 0.7rem 0.7rem 0 0.7rem;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,18 @@
|
|||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { Vocabulary } from '../../core/submission/vocabularies/models/vocabulary.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub class of {@link DsoEditMetadataFieldService}
|
||||||
|
*/
|
||||||
|
export class DsoEditMetadataFieldServiceStub {
|
||||||
|
|
||||||
|
findDsoFieldVocabulary(_dso: DSpaceObject, _mdField: string): Observable<Vocabulary> {
|
||||||
|
return observableOf(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,5 +1,20 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
|
import { FollowLinkConfig } from '../utils/follow-link-config.model';
|
||||||
|
import { createPaginatedList } from './utils.test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stub class of {@link EntityTypeDataService}
|
* Stub class of {@link EntityTypeDataService}
|
||||||
*/
|
*/
|
||||||
export class EntityTypeDataServiceStub {
|
export class EntityTypeDataServiceStub {
|
||||||
|
|
||||||
|
public findAll(_options?: FindListOptions, _useCachedVersionIfAvailable?: boolean, _reRequestOnStale?: boolean, ..._linksToFollow: FollowLinkConfig<ItemType>[]): Observable<RemoteData<PaginatedList<ItemType>>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(createPaginatedList());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user