1
0

97742: Removing old item-metadata component & adding support for itemtemplate dso-edit-metadata

This commit is contained in:
Kristof De Langhe
2023-01-04 11:55:37 +01:00
parent 533d833225
commit c4de31ec4d
16 changed files with 114 additions and 1329 deletions

View File

@@ -15,6 +15,7 @@ import { StatisticsModule } from '../statistics/statistics.module';
import { CollectionFormModule } from './collection-form/collection-form.module';
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
import { ComcolModule } from '../shared/comcol/comcol.module';
import { DsoSharedModule } from '../dso-shared/dso-shared.module';
@NgModule({
imports: [
@@ -24,7 +25,8 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
StatisticsModule.forRoot(),
EditItemPageModule,
CollectionFormModule,
ComcolModule
ComcolModule,
DsoSharedModule,
],
declarations: [
CollectionPageComponent,

View File

@@ -3,7 +3,7 @@
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
<ng-container *ngIf="itemRD?.hasSucceeded">
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
<ds-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-item-metadata>
<ds-dso-edit-metadata [updateDataService]="itemTemplateService" [dso]="itemRD?.payload"></ds-dso-edit-metadata>
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
</ng-container>
<ds-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-loading>

View File

@@ -58,7 +58,7 @@
</ds-dso-edit-metadata-field-values>
</div>
<div *ngIf="isEmpty">
<div *ngIf="isEmpty && !form.newValue">
<ds-alert [content]="dsoType + '.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
</div>
<div class="button-row bottom d-inline-block w-100">

View File

@@ -45,7 +45,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
* Resolved update data-service for the given DSpaceObject (depending on its type, e.g. ItemDataService for an Item)
* Used to send the PATCH request
*/
updateDataService: UpdateDataService<DSpaceObject>;
@Input() updateDataService: UpdateDataService<DSpaceObject>;
/**
* Type of the DSpaceObject in String
@@ -143,11 +143,13 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
} else {
type = this.dso.type;
}
if (hasNoValue(this.updateDataService)) {
const provider = getDataServiceFor(type);
this.updateDataService = Injector.create({
providers: [],
parent: this.parentInjector
}).get(provider);
}
this.dsoType = type.value;
}

View File

@@ -7,16 +7,18 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { RegistryService } from '../../../core/registry/registry.service';
import { MetadataField } from '../../../core/metadata/metadata-field.model';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { By } from '@angular/platform-browser';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
describe('MetadataFieldSelectorComponent', () => {
let component: MetadataFieldSelectorComponent;
let fixture: ComponentFixture<MetadataFieldSelectorComponent>;
let registryService: RegistryService;
let notificationsService: NotificationsService;
let metadataSchema: MetadataSchema;
let metadataFields: MetadataField[];
@@ -45,12 +47,14 @@ describe('MetadataFieldSelectorComponent', () => {
registryService = jasmine.createSpyObj('registryService', {
queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)),
});
notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']);
TestBed.configureTestingModule({
declarations: [MetadataFieldSelectorComponent, VarDirective],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
providers: [
{ provide: RegistryService, useValue: registryService },
{ provide: NotificationsService, useValue: notificationsService },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@@ -99,5 +103,20 @@ describe('MetadataFieldSelectorComponent', () => {
done();
});
});
describe('when querying the metadata fields returns an error response', () => {
beforeEach(() => {
(registryService.queryMetadataFields as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Failed'));
});
it('should return an observable false and show a notification', (done) => {
component.mdField = 'dc.description.abstract';
component.validate().subscribe((result) => {
expect(result).toBeFalse();
expect(notificationsService.error).toHaveBeenCalled();
done();
});
});
});
});
});

View File

@@ -12,7 +12,7 @@ import {
import { switchMap, debounceTime, distinctUntilChanged, map, tap, take } from 'rxjs/operators';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import {
getAllSucceededRemoteData, getFirstSucceededRemoteData,
getAllSucceededRemoteData, getFirstCompletedRemoteData, getFirstSucceededRemoteData,
metadataFieldsToString
} from '../../../core/shared/operators';
import { Observable } from 'rxjs/internal/Observable';
@@ -21,6 +21,9 @@ import { FormControl } from '@angular/forms';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { hasValue } from '../../../shared/empty.util';
import { Subscription } from 'rxjs/internal/Subscription';
import { of } from 'rxjs/internal/observable/of';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'ds-metadata-field-selector',
@@ -97,7 +100,9 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
*/
subs: Subscription[] = [];
constructor(protected registryService: RegistryService) {
constructor(protected registryService: RegistryService,
protected notificationsService: NotificationsService,
protected translate: TranslateService) {
}
/**
@@ -148,12 +153,21 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
*/
validate(): Observable<boolean> {
return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe(
getFirstSucceededRemoteData(),
getFirstCompletedRemoteData(),
switchMap((rd) => {
if (rd.hasSucceeded) {
return of(rd).pipe(
metadataFieldsToString(),
take(1),
map((fields: string[]) => fields.indexOf(this.mdField) > -1),
tap((exists: boolean) => this.showInvalid = !exists),
);
} else {
this.notificationsService.error(this.translate.instant(`${this.dsoType}.edit.metadata.metadatafield.error`), rd.errorMessage);
return [false];
}
}),
);
}
/**

View File

@@ -14,8 +14,6 @@ import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract
import { ItemPrivateComponent } from './item-private/item-private.component';
import { ItemPublicComponent } from './item-public/item-public.component';
import { ItemDeleteComponent } from './item-delete/item-delete.component';
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.component';
import { SearchPageModule } from '../../search-page/search-page.module';
@@ -63,15 +61,12 @@ import { DsoSharedModule } from '../../dso-shared/dso-shared.module';
ItemPublicComponent,
ItemDeleteComponent,
ItemStatusComponent,
ItemMetadataComponent,
ItemRelationshipsComponent,
ItemBitstreamsComponent,
ItemVersionHistoryComponent,
EditInPlaceFieldComponent,
ItemEditBitstreamComponent,
ItemEditBitstreamBundleComponent,
PaginatedDragAndDropBitstreamListComponent,
EditInPlaceFieldComponent,
EditRelationshipComponent,
EditRelationshipListComponent,
ItemCollectionMapperComponent,
@@ -84,9 +79,6 @@ import { DsoSharedModule } from '../../dso-shared/dso-shared.module';
BundleDataService,
ObjectValuesPipe
],
exports: [
ItemMetadataComponent
]
})
export class EditItemPageModule {

View File

@@ -1,71 +0,0 @@
<td>
<div class="metadata-field">
<div *ngIf="!(editable | async)">
<span >{{metadata?.key?.split('.').join('.&#8203;')}}</span>
</div>
<div *ngIf="(editable | async)" class="field-container">
<ds-validation-suggestions [disable]="fieldUpdate.changeType != 1" [suggestions]="(metadataFieldSuggestions | async)"
[(ngModel)]="metadata.key"
[url]="this.url"
[metadata]="this.metadata"
(submitSuggestion)="update(suggestionControl)"
(clickSuggestion)="update(suggestionControl)"
(typeSuggestion)="update(suggestionControl)"
(dsClickOutside)="checkValidity(suggestionControl)"
(findSuggestions)="findMetadataFieldSuggestions($event)"
#suggestionControl="ngModel"
[valid]="(valid | async) !== false"
dsAutoFocus autoFocusSelector=".suggestion_input"
[ngModelOptions]="{standalone: true}"
></ds-validation-suggestions>
</div>
<small class="text-danger"
*ngIf="(valid | async) === false">{{"item.edit.metadata.metadatafield.invalid" | translate}}</small>
</div>
</td>
<td class="w-100">
<div class="value-field">
<div *ngIf="!(editable | async)">
<span class="dont-break-out">{{metadata?.value}}</span>
</div>
<div *ngIf="(editable | async)" class="field-container">
<textarea class="form-control" type="textarea" attr.aria-labelledby="fieldValue" [(ngModel)]="metadata.value" [dsDebounce]
(onDebounce)="update()"></textarea>
</div>
</div>
</td>
<td class="text-center">
<div class="language-field">
<div *ngIf="!(editable | async)">
<span>{{metadata?.language}}</span>
</div>
<div *ngIf="(editable | async)" class="field-container">
<input class="form-control" type="text" attr.aria-labelledby="fieldLang" [(ngModel)]="metadata.language" [dsDebounce]
(onDebounce)="update()"/>
</div>
</div>
</td>
<td class="text-center">
<div class="btn-group edit-field">
<button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)"
(click)="setEditable(true)" class="btn btn-outline-primary btn-sm"
title="{{'item.edit.metadata.edit.buttons.edit' | translate}}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button [disabled]="!(canSetUneditable() | async) || (valid | async) === false" *ngIf="(editable | async)"
(click)="setEditable(false)" class="btn btn-outline-success btn-sm"
title="{{'item.edit.metadata.edit.buttons.unedit' | translate}}">
<i class="fas fa-check fa-fw"></i>
</button>
<button [disabled]="!(canRemove() | async)" (click)="remove()"
class="btn btn-outline-danger btn-sm"
title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
<button [disabled]="!(canUndo() | async)" (click)="removeChangesFromField()"
class="btn btn-outline-warning btn-sm"
title="{{'item.edit.metadata.edit.buttons.undo' | translate}}">
<i class="fas fa-undo-alt fa-fw"></i>
</button>
</div>
</td>

View File

@@ -1,13 +0,0 @@
.btn[disabled] {
color: var(--bs-gray-600);
border-color: var(--bs-gray-600);
z-index: 0; // prevent border colors jumping on hover
}
.metadata-field {
width: var(--ds-edit-item-metadata-field-width);
}
.language-field {
width: var(--ds-edit-item-language-field-width);
}

View File

@@ -1,505 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { getTestScheduler } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { MetadataFieldDataService } from '../../../../core/data/metadata-field-data.service';
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
import { RegistryService } from '../../../../core/registry/registry.service';
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { followLink } from '../../../../shared/utils/follow-link-config.model';
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
import { MockComponent, MockDirective } from 'ng-mocks';
import { DebounceDirective } from '../../../../shared/utils/debounce.directive';
import { ValidationSuggestionsComponent } from '../../../../shared/input-suggestions/validation-suggestions/validation-suggestions.component';
let comp: EditInPlaceFieldComponent;
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
let de: DebugElement;
let el: HTMLElement;
let metadataFieldService;
let objectUpdatesService;
let paginatedMetadataFields;
const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' });
const mdSchemaRD$ = createSuccessfulRemoteDataObject$(mdSchema);
const mdField1 = Object.assign(new MetadataField(), {
schema: mdSchemaRD$,
element: 'contributor',
qualifier: 'author'
});
const mdField2 = Object.assign(new MetadataField(), {
schema: mdSchemaRD$,
element: 'title'
});
const mdField3 = Object.assign(new MetadataField(), {
schema: mdSchemaRD$,
element: 'description',
qualifier: 'abstract',
});
const metadatum = Object.assign(new MetadatumViewModel(), {
key: 'dc.description.abstract',
value: 'Example abstract',
language: 'en'
});
const url = 'http://test-url.com/test-url';
const fieldUpdate = {
field: metadatum,
changeType: undefined
};
let scheduler: TestScheduler;
describe('EditInPlaceFieldComponent', () => {
beforeEach(waitForAsync(() => {
scheduler = getTestScheduler();
paginatedMetadataFields = buildPaginatedList(undefined, [mdField1, mdField2, mdField3]);
metadataFieldService = jasmine.createSpyObj({
queryMetadataFields: createSuccessfulRemoteDataObject$(paginatedMetadataFields),
});
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
{
saveChangeFieldUpdate: {},
saveRemoveFieldUpdate: {},
setEditableFieldUpdate: {},
setValidFieldUpdate: {},
removeSingleFieldUpdate: {},
isEditable: observableOf(false), // should always return something --> its in ngOnInit
isValid: observableOf(true) // should always return something --> its in ngOnInit
}
);
TestBed.configureTestingModule({
imports: [FormsModule, TranslateModule.forRoot()],
declarations: [
EditInPlaceFieldComponent,
MockDirective(DebounceDirective),
MockComponent(ValidationSuggestionsComponent)
],
providers: [
{ provide: RegistryService, useValue: metadataFieldService },
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
{ provide: MetadataFieldDataService, useValue: {} }
], schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EditInPlaceFieldComponent);
comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance
de = fixture.debugElement;
el = de.nativeElement;
comp.url = url;
comp.fieldUpdate = fieldUpdate;
comp.metadata = metadatum;
});
describe('update', () => {
beforeEach(() => {
comp.update();
fixture.detectChanges();
});
it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
expect(objectUpdatesService.saveChangeFieldUpdate).toHaveBeenCalledWith(url, metadatum);
});
});
describe('setEditable', () => {
const editable = false;
beforeEach(() => {
comp.setEditable(editable);
fixture.detectChanges();
});
it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct url and uuid and false', () => {
expect(objectUpdatesService.setEditableFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid, editable);
});
});
describe('editable is true', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
fixture.detectChanges();
});
it('the div should contain input fields or textareas', () => {
const inputField = de.queryAll(By.css('input'));
const textAreas = de.queryAll(By.css('textarea'));
expect(inputField.length + textAreas.length).toBeGreaterThan(0);
});
});
describe('editable is false', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
fixture.detectChanges();
});
it('the div should contain no input fields or textareas', () => {
const inputField = de.queryAll(By.css('input'));
const textAreas = de.queryAll(By.css('textarea'));
expect(inputField.length + textAreas.length).toBe(0);
});
});
describe('isValid is true', () => {
beforeEach(() => {
objectUpdatesService.isValid.and.returnValue(observableOf(true));
fixture.detectChanges();
});
it('the div should not contain an error message', () => {
const errorMessages = de.queryAll(By.css('small.text-danger'));
expect(errorMessages.length).toBe(0);
});
});
describe('isValid is false', () => {
beforeEach(() => {
objectUpdatesService.isValid.and.returnValue(observableOf(false));
fixture.detectChanges();
});
it('there should be an error message', () => {
const errorMessages = de.queryAll(By.css('small.text-danger'));
expect(errorMessages.length).toBeGreaterThan(0);
});
});
describe('remove', () => {
beforeEach(() => {
comp.remove();
fixture.detectChanges();
});
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, metadatum);
});
});
describe('removeChangesFromField', () => {
beforeEach(() => {
comp.removeChangesFromField();
fixture.detectChanges();
});
it('it should call removeChangesFromField on the objectUpdatesService with the correct url and uuid', () => {
expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, metadatum.uuid);
});
});
describe('findMetadataFieldSuggestions', () => {
const query = 'query string';
const metadataFieldSuggestions: InputSuggestion[] =
[
{
displayValue: ('dc.' + mdField1.toString()).split('.').join('.&#8203;'),
value: ('dc.' + mdField1.toString())
},
{
displayValue: ('dc.' + mdField2.toString()).split('.').join('.&#8203;'),
value: ('dc.' + mdField2.toString())
},
{
displayValue: ('dc.' + mdField3.toString()).split('.').join('.&#8203;'),
value: ('dc.' + mdField3.toString())
}
];
beforeEach(fakeAsync(() => {
comp.findMetadataFieldSuggestions(query);
tick();
fixture.detectChanges();
}));
it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => {
expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, true, false, followLink('schema'));
});
it('it should set metadataFieldSuggestions to the right value', () => {
const expected = 'a';
scheduler.expectObservable(comp.metadataFieldSuggestions).toBe(expected, { a: metadataFieldSuggestions });
});
});
describe('canSetEditable', () => {
describe('when editable is currently true', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
fixture.detectChanges();
});
it('canSetEditable should return an observable emitting false', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canSetEditable()).toBe(expected, { a: false });
});
});
describe('when editable is currently false', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
fixture.detectChanges();
});
describe('when the fieldUpdate\'s changeType is currently not REMOVE', () => {
beforeEach(() => {
comp.fieldUpdate.changeType = FieldChangeType.ADD;
fixture.detectChanges();
});
it('canSetEditable should return an observable emitting true', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canSetEditable()).toBe(expected, { a: true });
});
});
describe('when the fieldUpdate\'s changeType is currently REMOVE', () => {
beforeEach(() => {
comp.fieldUpdate.changeType = FieldChangeType.REMOVE;
fixture.detectChanges();
});
it('canSetEditable should return an observable emitting false', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canSetEditable()).toBe(expected, { a: false });
});
});
});
});
describe('canSetUneditable', () => {
describe('when editable is currently true', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
fixture.detectChanges();
});
it('canSetUneditable should return an observable emitting true', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canSetUneditable()).toBe(expected, { a: true });
});
});
describe('when editable is currently false', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
fixture.detectChanges();
});
it('canSetUneditable should return an observable emitting false', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canSetUneditable()).toBe(expected, { a: false });
});
});
});
describe('when canSetEditable emits true', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(true));
fixture.detectChanges();
});
it('the div should have an enabled button with an edit icon', () => {
const editIcon = de.query(By.css('i.fa-edit')).parent.nativeElement.disabled;
expect(editIcon).toBe(false);
});
});
describe('when canSetEditable emits false', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(false));
fixture.detectChanges();
});
it('the div should have a disabled button with an edit icon', () => {
const editIcon = de.query(By.css('i.fa-edit')).parent.nativeElement.disabled;
expect(editIcon).toBe(true);
});
});
describe('when canSetUneditable emits true', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(true));
fixture.detectChanges();
});
it('the div should have an enabled button with a check icon', () => {
const checkButtonAttrs = de.query(By.css('i.fa-check')).parent.nativeElement.disabled;
expect(checkButtonAttrs).toBe(false);
});
});
describe('when canSetUneditable emits false', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(false));
fixture.detectChanges();
});
it('the div should have a disabled button with a check icon', () => {
const checkButtonAttrs = de.query(By.css('i.fa-check')).parent.nativeElement.disabled;
expect(checkButtonAttrs).toBe(true);
});
});
describe('when canRemove emits true', () => {
beforeEach(() => {
spyOn(comp, 'canRemove').and.returnValue(observableOf(true));
fixture.detectChanges();
});
it('the div should have an enabled button with a trash icon', () => {
const trashButtonAttrs = de.query(By.css('i.fa-trash-alt')).parent.nativeElement.disabled;
expect(trashButtonAttrs).toBe(false);
});
});
describe('when canRemove emits false', () => {
beforeEach(() => {
spyOn(comp, 'canRemove').and.returnValue(observableOf(false));
fixture.detectChanges();
});
it('the div should have a disabled button with a trash icon', () => {
const trashButtonAttrs = de.query(By.css('i.fa-trash-alt')).parent.nativeElement.disabled;
expect(trashButtonAttrs).toBe(true);
});
});
describe('when canUndo emits true', () => {
beforeEach(() => {
spyOn(comp, 'canUndo').and.returnValue(observableOf(true));
fixture.detectChanges();
});
it('the div should have an enabled button with an undo icon', () => {
const undoIcon = de.query(By.css('i.fa-undo-alt')).parent.nativeElement.disabled;
expect(undoIcon).toBe(false);
});
});
describe('when canUndo emits false', () => {
beforeEach(() => {
spyOn(comp, 'canUndo').and.returnValue(observableOf(false));
fixture.detectChanges();
});
it('the div should have a disabled button with an undo icon', () => {
const undoIcon = de.query(By.css('i.fa-undo-alt')).parent.nativeElement.disabled;
expect(undoIcon).toBe(true);
});
});
describe('canRemove', () => {
describe('when the fieldUpdate\'s changeType is currently not REMOVE or ADD', () => {
beforeEach(() => {
comp.fieldUpdate.changeType = FieldChangeType.UPDATE;
fixture.detectChanges();
});
it('canRemove should return an observable emitting true', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canRemove()).toBe(expected, { a: true });
});
});
describe('when the fieldUpdate\'s changeType is currently ADD', () => {
beforeEach(() => {
comp.fieldUpdate.changeType = FieldChangeType.ADD;
fixture.detectChanges();
});
it('canRemove should return an observable emitting false', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canRemove()).toBe(expected, { a: false });
});
});
});
describe('canUndo', () => {
describe('when editable is currently true', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
comp.fieldUpdate.changeType = undefined;
fixture.detectChanges();
});
it('canUndo should return an observable emitting true', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canUndo()).toBe(expected, { a: true });
});
});
describe('when editable is currently false', () => {
describe('when the fieldUpdate\'s changeType is currently ADD, UPDATE or REMOVE', () => {
beforeEach(() => {
comp.fieldUpdate.changeType = FieldChangeType.ADD;
fixture.detectChanges();
});
it('canUndo should return an observable emitting true', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canUndo()).toBe(expected, { a: true });
});
});
describe('when the fieldUpdate\'s changeType is currently undefined', () => {
beforeEach(() => {
comp.fieldUpdate.changeType = undefined;
fixture.detectChanges();
});
it('canUndo should return an observable emitting false', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.canUndo()).toBe(expected, { a: false });
});
});
});
});
describe('canEditMetadataField', () => {
describe('when the fieldUpdate\'s changeType is currently ADD', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
comp.fieldUpdate.changeType = FieldChangeType.ADD;
fixture.detectChanges();
});
it('can edit metadata field', () => {
const disabledMetadataField = fixture.debugElement.query(By.css('ds-validation-suggestions'))
.componentInstance.disable;
expect(disabledMetadataField).toBe(false);
});
});
describe('when the fieldUpdate\'s changeType is currently REMOVE', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
comp.fieldUpdate.changeType = FieldChangeType.REMOVE;
fixture.detectChanges();
});
it('can edit metadata field', () => {
const disabledMetadataField = fixture.debugElement.query(By.css('ds-validation-suggestions'))
.componentInstance.disable;
expect(disabledMetadataField).toBe(true);
});
});
describe('when the fieldUpdate\'s changeType is currently UPDATE', () => {
beforeEach(() => {
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
comp.fieldUpdate.changeType = FieldChangeType.UPDATE;
fixture.detectChanges();
});
it('can edit metadata field', () => {
const disabledMetadataField = fixture.debugElement.query(By.css('ds-validation-suggestions'))
.componentInstance.disable;
expect(disabledMetadataField).toBe(true);
});
});
});
});

View File

@@ -1,201 +0,0 @@
import { Component, Input, OnChanges, OnInit } from '@angular/core';
import {
metadataFieldsToString,
getFirstSucceededRemoteData
} from '../../../../core/shared/operators';
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { RegistryService } from '../../../../core/registry/registry.service';
import { cloneDeep } from 'lodash';
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { NgModel } from '@angular/forms';
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
import { followLink } from '../../../../shared/utils/follow-link-config.model';
@Component({
// tslint:disable-next-line:component-selector
selector: '[ds-edit-in-place-field]',
styleUrls: ['./edit-in-place-field.component.scss'],
templateUrl: './edit-in-place-field.component.html',
})
/**
* Component that displays a single metadatum of an item on the edit page
*/
export class EditInPlaceFieldComponent implements OnInit, OnChanges {
/**
* The current field, value and state of the metadatum
*/
@Input() fieldUpdate: FieldUpdate;
/**
* The current url of this page
*/
@Input() url: string;
/**
* The metadatum of this field
*/
@Input() metadata: MetadatumViewModel;
/**
* Emits whether or not this field is currently editable
*/
editable: Observable<boolean>;
/**
* Emits whether or not this field is currently valid
*/
valid: Observable<boolean>;
/**
* The current suggestions for the metadatafield when editing
*/
metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]);
constructor(
private registryService: RegistryService,
private objectUpdatesService: ObjectUpdatesService,
) {
}
/**
* Sets up an observable that keeps track of the current editable and valid state of this field
*/
ngOnInit(): void {
this.editable = this.objectUpdatesService.isEditable(this.url, this.metadata.uuid);
this.valid = this.objectUpdatesService.isValid(this.url, this.metadata.uuid);
}
/**
* Sends a new change update for this field to the object updates service
*/
update(ngModel?: NgModel) {
this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata));
if (hasValue(ngModel)) {
this.checkValidity(ngModel);
}
}
/**
* Method to check the validity of a form control
* @param ngModel
*/
public checkValidity(ngModel: NgModel) {
ngModel.control.setValue(ngModel.viewModel);
ngModel.control.updateValueAndValidity();
this.objectUpdatesService.setValidFieldUpdate(this.url, this.metadata.uuid, ngModel.control.valid);
}
/**
* Sends a new editable state for this field to the service to change it
* @param editable The new editable state for this field
*/
setEditable(editable: boolean) {
this.objectUpdatesService.setEditableFieldUpdate(this.url, this.metadata.uuid, editable);
}
/**
* Sends a new remove update for this field to the object updates service
*/
remove() {
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, cloneDeep(this.metadata));
}
/**
* Notifies the object updates service that the updates for the current field can be removed
*/
removeChangesFromField() {
this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.metadata.uuid);
}
/**
* Sets the current metadatafield based on the fieldUpdate input field
*/
ngOnChanges(): void {
this.metadata = cloneDeep(this.fieldUpdate.field) as MetadatumViewModel;
}
/**
* Requests all metadata fields that contain the query string in their key
* Then sets all found metadata fields as metadataFieldSuggestions
* Ignores fields from metadata schemas "relation" and "relationship"
* @param query The query to look for
*/
findMetadataFieldSuggestions(query: string) {
if (isNotEmpty(query)) {
return this.registryService.queryMetadataFields(query, null, true, false, followLink('schema')).pipe(
getFirstSucceededRemoteData(),
metadataFieldsToString(),
).subscribe((fieldNames: string[]) => {
this.setInputSuggestions(fieldNames);
});
} else {
this.metadataFieldSuggestions.next([]);
}
}
/**
* Set the list of input suggestion with the given Metadata fields, which all require a resolved MetadataSchema
* @param fields list of Metadata fields, which all require a resolved MetadataSchema
*/
setInputSuggestions(fields: string[]) {
this.metadataFieldSuggestions.next(
fields.map((fieldName: string) => {
return {
displayValue: fieldName.split('.').join('.&#8203;'),
value: fieldName
};
})
);
}
/**
* Check if a user should be allowed to edit this field
* @return an observable that emits true when the user should be able to edit this field and false when they should not
*/
canSetEditable(): Observable<boolean> {
return this.editable.pipe(
map((editable: boolean) => {
if (editable) {
return false;
} else {
return this.fieldUpdate.changeType !== FieldChangeType.REMOVE;
}
})
);
}
/**
* Check if a user should be allowed to disabled editing this field
* @return an observable that emits true when the user should be able to disable editing this field and false when they should not
*/
canSetUneditable(): Observable<boolean> {
return this.editable;
}
/**
* Check if a user should be allowed to remove this field
* @return an observable that emits true when the user should be able to remove this field and false when they should not
*/
canRemove(): Observable<boolean> {
return observableOf(this.fieldUpdate.changeType !== FieldChangeType.REMOVE && this.fieldUpdate.changeType !== FieldChangeType.ADD);
}
/**
* Check if a user should be allowed to undo changes to this field
* @return an observable that emits true when the user should be able to undo changes to this field and false when they should not
*/
canUndo(): Observable<boolean> {
return this.editable.pipe(
map((editable: boolean) => this.fieldUpdate.changeType >= 0 || editable)
);
}
protected isNotEmpty(value): boolean {
return isNotEmpty(value);
}
}

View File

@@ -1,69 +0,0 @@
<div class="item-metadata">
<div class="button-row top d-flex mb-2">
<button class="mr-auto btn btn-success"
(click)="add()"><i
class="fas fa-plus"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.add-button" | translate}}</span>
</button>
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
(click)="submit()"><i
class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
</button>
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
</button>
</div>
<table class="table table-responsive table-striped table-bordered"
*ngIf="((updates$ | async)| dsObjectValues).length > 0">
<thead>
<tr>
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
ds-edit-in-place-field
[fieldUpdate]="updateValue || {}"
[url]="url"
[ngClass]="{
'table-warning': updateValue.changeType === 0,
'table-danger': updateValue.changeType === 2,
'table-success': updateValue.changeType === 1
}">
</tr>
</tbody>
</table>
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
</div>
<div class="button-row bottom">
<div class="mt-2 float-right">
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
</button>
<button class="btn btn-primary mr-0" [disabled]="!(hasChanges() | async)"
(click)="submit()"><i
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
</button>
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
</button>
</div>
</div>
</div>

View File

@@ -1,20 +0,0 @@
.button-row {
.btn {
margin-right: calc(0.5 * var(--bs-spacer));
&:last-child {
margin-right: 0;
}
@media screen and (min-width: map-get($grid-breakpoints, sm)) {
min-width: var(--ds-edit-item-button-min-width);
}
}
&.top .btn {
margin-top: calc(var(--bs-spacer) / 2);
margin-bottom: calc(var(--bs-spacer) / 2);
}
}

View File

@@ -1,290 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { getTestScheduler } from 'jasmine-marbles';
import { ItemMetadataComponent } from './item-metadata.component';
import { TestScheduler } from 'rxjs/testing';
import { SharedModule } from '../../../shared/shared.module';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateModule } from '@ngx-translate/core';
import { ItemDataService } from '../../../core/data/item-data.service';
import { By } from '@angular/platform-browser';
import { INotification, Notification } from '../../../shared/notifications/models/notification.model';
import { NotificationType } from '../../../shared/notifications/models/notification-type';
import { RouterStub } from '../../../shared/testing/router.stub';
import { Item } from '../../../core/shared/item.model';
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
import { MetadatumViewModel } from '../../../core/shared/metadata.models';
import { RegistryService } from '../../../core/registry/registry.service';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { MetadataField } from '../../../core/metadata/metadata-field.model';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { DSOSuccessResponse } from '../../../core/cache/response.models';
import { createPaginatedList } from '../../../shared/testing/utils.test';
let comp: any;
let fixture: ComponentFixture<ItemMetadataComponent>;
let de: DebugElement;
let el: HTMLElement;
let objectUpdatesService;
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
const successNotification: INotification = new Notification('id', NotificationType.Success, 'success');
const date = new Date();
const router = new RouterStub();
let metadataFieldService;
let paginatedMetadataFields;
let routeStub;
let objectCacheService;
const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' });
const mdField1 = Object.assign(new MetadataField(), {
schema: mdSchema,
element: 'contributor',
qualifier: 'author'
});
const mdField2 = Object.assign(new MetadataField(), { schema: mdSchema, element: 'title' });
const mdField3 = Object.assign(new MetadataField(), {
schema: mdSchema,
element: 'description',
qualifier: 'abstract'
});
let itemService;
const notificationsService = jasmine.createSpyObj('notificationsService',
{
info: infoNotification,
warning: warningNotification,
success: successNotification
}
);
const metadatum1 = Object.assign(new MetadatumViewModel(), {
key: 'dc.description.abstract',
value: 'Example abstract',
language: 'en'
});
const metadatum2 = Object.assign(new MetadatumViewModel(), {
key: 'dc.title',
value: 'Title test',
language: 'de'
});
const metadatum3 = Object.assign(new MetadatumViewModel(), {
key: 'dc.contributor.author',
value: 'Shakespeare, William',
});
const url = 'http://test-url.com/test-url';
router.url = url;
const fieldUpdate1 = {
field: metadatum1,
changeType: undefined
};
const fieldUpdate2 = {
field: metadatum2,
changeType: FieldChangeType.REMOVE
};
const fieldUpdate3 = {
field: metadatum3,
changeType: undefined
};
const operation1 = { op: 'remove', path: '/metadata/dc.title/1' };
let scheduler: TestScheduler;
let item;
describe('ItemMetadataComponent', () => {
beforeEach(waitForAsync(() => {
item = Object.assign(new Item(), {
metadata: {
[metadatum1.key]: [metadatum1],
[metadatum2.key]: [metadatum2],
[metadatum3.key]: [metadatum3]
},
_links: {
self: {
href: 'https://rest.api/core/items/a36d8bd2-8e8c-4969-9b1f-a574c2064983'
}
}
},
{
lastModified: date
}
)
;
itemService = jasmine.createSpyObj('itemService', {
update: createSuccessfulRemoteDataObject$(item),
commitUpdates: {},
patch: observableOf(new DSOSuccessResponse(['item-selflink'], 200, 'OK')),
findByHref: createSuccessfulRemoteDataObject$(item)
});
routeStub = {
data: observableOf({}),
parent: {
data: observableOf({ dso: createSuccessfulRemoteDataObject(item) })
}
};
paginatedMetadataFields = createPaginatedList([mdField1, mdField2, mdField3]);
metadataFieldService = jasmine.createSpyObj({
getAllMetadataFields: createSuccessfulRemoteDataObject$(paginatedMetadataFields)
});
scheduler = getTestScheduler();
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
{
getFieldUpdates: observableOf({
[metadatum1.uuid]: fieldUpdate1,
[metadatum2.uuid]: fieldUpdate2,
[metadatum3.uuid]: fieldUpdate3
}),
saveAddFieldUpdate: {},
discardFieldUpdates: {},
reinstateFieldUpdates: observableOf(true),
initialize: {},
getUpdatedFields: observableOf([metadatum1, metadatum2, metadatum3]),
getLastModified: observableOf(date),
hasUpdates: observableOf(true),
isReinstatable: observableOf(false), // should always return something --> its in ngOnInit
isValidPage: observableOf(true),
createPatch: observableOf([
operation1
])
}
);
objectCacheService = jasmine.createSpyObj('objectCacheService', ['addPatch']);
TestBed.configureTestingModule({
imports: [SharedModule, TranslateModule.forRoot()],
declarations: [ItemMetadataComponent],
providers: [
{ provide: ItemDataService, useValue: itemService },
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
{ provide: Router, useValue: router },
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: RegistryService, useValue: metadataFieldService },
{ provide: ObjectCacheService, useValue: objectCacheService },
], schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(ItemMetadataComponent);
comp = fixture.componentInstance; // EditInPlaceFieldComponent test instance
de = fixture.debugElement;
el = de.nativeElement;
comp.url = url;
fixture.detectChanges();
});
describe('add', () => {
const md = new MetadatumViewModel();
beforeEach(() => {
comp.add(md);
});
it('it should call saveAddFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(url, md);
});
});
describe('discard', () => {
beforeEach(() => {
comp.discard();
});
it('it should call discardFieldUpdates on the objectUpdatesService with the correct url and notification', () => {
expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(url, infoNotification);
});
});
describe('reinstate', () => {
beforeEach(() => {
comp.reinstate();
});
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url', () => {
expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(url);
});
});
describe('submit', () => {
beforeEach(() => {
comp.submit();
});
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url and metadata', () => {
expect(objectUpdatesService.createPatch).toHaveBeenCalledWith(url);
expect(itemService.patch).toHaveBeenCalledWith(comp.item, [operation1]);
expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(url, comp.item.metadataAsList);
});
});
describe('hasChanges', () => {
describe('when the objectUpdatesService\'s hasUpdated method returns true', () => {
beforeEach(() => {
objectUpdatesService.hasUpdates.and.returnValue(observableOf(true));
});
it('should return an observable that emits true', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.hasChanges()).toBe(expected, { a: true });
});
});
describe('when the objectUpdatesService\'s hasUpdated method returns false', () => {
beforeEach(() => {
objectUpdatesService.hasUpdates.and.returnValue(observableOf(false));
});
it('should return an observable that emits false', () => {
const expected = '(a|)';
scheduler.expectObservable(comp.hasChanges()).toBe(expected, { a: false });
});
});
});
describe('changeType is UPDATE', () => {
beforeEach(() => {
fieldUpdate1.changeType = FieldChangeType.UPDATE;
fixture.detectChanges();
});
it('the div should have class table-warning', () => {
const element = de.queryAll(By.css('tr'))[1].nativeElement;
expect(element.classList).toContain('table-warning');
});
});
describe('changeType is ADD', () => {
beforeEach(() => {
fieldUpdate1.changeType = FieldChangeType.ADD;
fixture.detectChanges();
});
it('the div should have class table-success', () => {
const element = de.queryAll(By.css('tr'))[1].nativeElement;
expect(element.classList).toContain('table-success');
});
});
describe('changeType is REMOVE', () => {
beforeEach(() => {
fieldUpdate1.changeType = FieldChangeType.REMOVE;
fixture.detectChanges();
});
it('the div should have class table-danger', () => {
const element = de.queryAll(By.css('tr'))[1].nativeElement;
expect(element.classList).toContain('table-danger');
});
});
});

View File

@@ -1,135 +0,0 @@
import { Component, Input } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { ActivatedRoute, Router } from '@angular/router';
import { cloneDeep } from 'lodash';
import { first, switchMap } from 'rxjs/operators';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { RemoteData } from '../../../core/data/remote-data';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { UpdateDataService } from '../../../core/data/update-data.service';
import { hasNoValue, hasValue } from '../../../shared/empty.util';
import { AlertType } from '../../../shared/alert/aletr-type';
import { Operation } from 'fast-json-patch';
import { MetadataPatchOperationService } from '../../../core/data/object-updates/patch-operation-service/metadata-patch-operation.service';
@Component({
selector: 'ds-item-metadata',
styleUrls: ['./item-metadata.component.scss'],
templateUrl: './item-metadata.component.html',
})
/**
* Component for displaying an item's metadata edit page
*/
export class ItemMetadataComponent extends AbstractItemUpdateComponent {
/**
* The AlertType enumeration
* @type {AlertType}
*/
public AlertTypeEnum = AlertType;
/**
* A custom update service to use for adding and committing patches
* This will default to the ItemDataService
*/
@Input() updateService: UpdateDataService<Item>;
constructor(
public itemService: ItemDataService,
public objectUpdatesService: ObjectUpdatesService,
public router: Router,
public notificationsService: NotificationsService,
public translateService: TranslateService,
public route: ActivatedRoute,
) {
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
}
/**
* Set up and initialize all fields
*/
ngOnInit(): void {
super.ngOnInit();
if (hasNoValue(this.updateService)) {
this.updateService = this.itemService;
}
}
/**
* Initialize the values and updates of the current item's metadata fields
*/
public initializeUpdates(): void {
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList);
}
/**
* Initialize the prefix for notification messages
*/
public initializeNotificationsPrefix(): void {
this.notificationsPrefix = 'item.edit.metadata.notifications.';
}
/**
* Sends a new add update for a field to the object updates service
* @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum
*/
add(metadata: MetadatumViewModel = new MetadatumViewModel()) {
this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata);
}
/**
* Sends all initial values of this item to the object updates service
*/
public initializeOriginalFields() {
this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified, MetadataPatchOperationService);
}
/**
* Requests all current metadata for this item and requests the item service to update the item
* Makes sure the new version of the item is rendered on the page
*/
public submit() {
this.isValid().pipe(first()).subscribe((isValid) => {
if (isValid) {
this.objectUpdatesService.createPatch(this.url).pipe(
first(),
switchMap((patch: Operation[]) => {
return this.updateService.patch(this.item, patch).pipe(
getFirstCompletedRemoteData()
);
})
).subscribe(
(rd: RemoteData<Item>) => {
if (rd.hasFailed) {
this.notificationsService.error(this.getNotificationTitle('error'), rd.errorMessage);
} else {
this.item = rd.payload;
this.checkAndFixMetadataUUIDs();
this.initializeOriginalFields();
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList);
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
}
}
);
} else {
this.notificationsService.error(this.getNotificationTitle('invalid'), this.getNotificationContent('invalid'));
}
});
}
/**
* Check for empty metadata UUIDs and fix them (empty UUIDs would break the object-update service)
*/
checkAndFixMetadataUUIDs() {
const metadata = cloneDeep(this.item.metadata);
Object.keys(this.item.metadata).forEach((key: string) => {
metadata[key] = this.item.metadata[key].map((value) => hasValue(value.uuid) ? value : Object.assign(new MetadataValue(), value));
});
this.item.metadata = metadata;
}
}

View File

@@ -1815,6 +1815,8 @@
"item.edit.metadata.headers.value": "Value",
"item.edit.metadata.metadatafield.error": "An error occurred validating the metadata field",
"item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field",
"item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button",
@@ -2270,6 +2272,64 @@
"itemtemplate.edit.metadata.add-button": "Add",
"itemtemplate.edit.metadata.discard-button": "Discard",
"itemtemplate.edit.metadata.edit.buttons.confirm": "Confirm",
"itemtemplate.edit.metadata.edit.buttons.drag": "Drag to reorder",
"itemtemplate.edit.metadata.edit.buttons.edit": "Edit",
"itemtemplate.edit.metadata.edit.buttons.remove": "Remove",
"itemtemplate.edit.metadata.edit.buttons.undo": "Undo changes",
"itemtemplate.edit.metadata.edit.buttons.unedit": "Stop editing",
"itemtemplate.edit.metadata.edit.buttons.virtual": "This is a virtual metadata value, i.e. a value inherited from a related entity. It cant be modified directly. Add or remove the corresponding relationship in the \"Relationships\" tab",
"itemtemplate.edit.metadata.empty": "The item template currently doesn't contain any metadata. Click Add to start adding a metadata value.",
"itemtemplate.edit.metadata.headers.edit": "Edit",
"itemtemplate.edit.metadata.headers.field": "Field",
"itemtemplate.edit.metadata.headers.language": "Lang",
"itemtemplate.edit.metadata.headers.value": "Value",
"itemtemplate.edit.metadata.metadatafield.error": "An error occurred validating the metadata field",
"itemtemplate.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field",
"itemtemplate.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button",
"itemtemplate.edit.metadata.notifications.discarded.title": "Changed discarded",
"itemtemplate.edit.metadata.notifications.error.title": "An error occurred",
"itemtemplate.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.",
"itemtemplate.edit.metadata.notifications.invalid.title": "Metadata invalid",
"itemtemplate.edit.metadata.notifications.outdated.content": "The item template you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts",
"itemtemplate.edit.metadata.notifications.outdated.title": "Changed outdated",
"itemtemplate.edit.metadata.notifications.saved.content": "Your changes to this item template's metadata were saved.",
"itemtemplate.edit.metadata.notifications.saved.title": "Metadata saved",
"itemtemplate.edit.metadata.reinstate-button": "Undo",
"itemtemplate.edit.metadata.reset-order-button": "Undo reorder",
"itemtemplate.edit.metadata.save-button": "Save",
"journal.listelement.badge": "Journal",
"journal.page.description": "Description",