forked from hazza/dspace-angular
97742: Removing old item-metadata component & adding support for itemtemplate dso-edit-metadata
This commit is contained in:
@@ -15,6 +15,7 @@ import { StatisticsModule } from '../statistics/statistics.module';
|
|||||||
import { CollectionFormModule } from './collection-form/collection-form.module';
|
import { CollectionFormModule } from './collection-form/collection-form.module';
|
||||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||||
import { ComcolModule } from '../shared/comcol/comcol.module';
|
import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||||
|
import { DsoSharedModule } from '../dso-shared/dso-shared.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -24,7 +25,8 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
|
|||||||
StatisticsModule.forRoot(),
|
StatisticsModule.forRoot(),
|
||||||
EditItemPageModule,
|
EditItemPageModule,
|
||||||
CollectionFormModule,
|
CollectionFormModule,
|
||||||
ComcolModule
|
ComcolModule,
|
||||||
|
DsoSharedModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CollectionPageComponent,
|
CollectionPageComponent,
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<ng-container *ngIf="itemRD?.hasSucceeded">
|
<ng-container *ngIf="itemRD?.hasSucceeded">
|
||||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
<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>
|
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ds-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-loading>
|
<ds-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-loading>
|
||||||
|
@@ -58,7 +58,7 @@
|
|||||||
</ds-dso-edit-metadata-field-values>
|
</ds-dso-edit-metadata-field-values>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isEmpty">
|
<div *ngIf="isEmpty && !form.newValue">
|
||||||
<ds-alert [content]="dsoType + '.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
<ds-alert [content]="dsoType + '.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row bottom d-inline-block w-100">
|
<div class="button-row bottom d-inline-block w-100">
|
||||||
|
@@ -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)
|
* Resolved update data-service for the given DSpaceObject (depending on its type, e.g. ItemDataService for an Item)
|
||||||
* Used to send the PATCH request
|
* Used to send the PATCH request
|
||||||
*/
|
*/
|
||||||
updateDataService: UpdateDataService<DSpaceObject>;
|
@Input() updateDataService: UpdateDataService<DSpaceObject>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type of the DSpaceObject in String
|
* Type of the DSpaceObject in String
|
||||||
@@ -143,11 +143,13 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
type = this.dso.type;
|
type = this.dso.type;
|
||||||
}
|
}
|
||||||
const provider = getDataServiceFor(type);
|
if (hasNoValue(this.updateDataService)) {
|
||||||
this.updateDataService = Injector.create({
|
const provider = getDataServiceFor(type);
|
||||||
providers: [],
|
this.updateDataService = Injector.create({
|
||||||
parent: this.parentInjector
|
providers: [],
|
||||||
}).get(provider);
|
parent: this.parentInjector
|
||||||
|
}).get(provider);
|
||||||
|
}
|
||||||
this.dsoType = type.value;
|
this.dsoType = type.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,16 +7,18 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { RegistryService } from '../../../core/registry/registry.service';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.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 { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
|
||||||
describe('MetadataFieldSelectorComponent', () => {
|
describe('MetadataFieldSelectorComponent', () => {
|
||||||
let component: MetadataFieldSelectorComponent;
|
let component: MetadataFieldSelectorComponent;
|
||||||
let fixture: ComponentFixture<MetadataFieldSelectorComponent>;
|
let fixture: ComponentFixture<MetadataFieldSelectorComponent>;
|
||||||
|
|
||||||
let registryService: RegistryService;
|
let registryService: RegistryService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
|
||||||
let metadataSchema: MetadataSchema;
|
let metadataSchema: MetadataSchema;
|
||||||
let metadataFields: MetadataField[];
|
let metadataFields: MetadataField[];
|
||||||
@@ -45,12 +47,14 @@ describe('MetadataFieldSelectorComponent', () => {
|
|||||||
registryService = jasmine.createSpyObj('registryService', {
|
registryService = jasmine.createSpyObj('registryService', {
|
||||||
queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)),
|
queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)),
|
||||||
});
|
});
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [MetadataFieldSelectorComponent, VarDirective],
|
declarations: [MetadataFieldSelectorComponent, VarDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryService },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -99,5 +103,20 @@ describe('MetadataFieldSelectorComponent', () => {
|
|||||||
done();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -12,7 +12,7 @@ import {
|
|||||||
import { switchMap, debounceTime, distinctUntilChanged, map, tap, take } from 'rxjs/operators';
|
import { switchMap, debounceTime, distinctUntilChanged, map, tap, take } from 'rxjs/operators';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData, getFirstSucceededRemoteData,
|
getAllSucceededRemoteData, getFirstCompletedRemoteData, getFirstSucceededRemoteData,
|
||||||
metadataFieldsToString
|
metadataFieldsToString
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
@@ -21,6 +21,9 @@ import { FormControl } from '@angular/forms';
|
|||||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { Subscription } from 'rxjs/internal/Subscription';
|
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({
|
@Component({
|
||||||
selector: 'ds-metadata-field-selector',
|
selector: 'ds-metadata-field-selector',
|
||||||
@@ -97,7 +100,9 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
*/
|
*/
|
||||||
subs: Subscription[] = [];
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
constructor(protected registryService: RegistryService) {
|
constructor(protected registryService: RegistryService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translate: TranslateService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,11 +153,20 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
*/
|
*/
|
||||||
validate(): Observable<boolean> {
|
validate(): Observable<boolean> {
|
||||||
return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe(
|
return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
metadataFieldsToString(),
|
switchMap((rd) => {
|
||||||
take(1),
|
if (rd.hasSucceeded) {
|
||||||
map((fields: string[]) => fields.indexOf(this.mdField) > -1),
|
return of(rd).pipe(
|
||||||
tap((exists: boolean) => this.showInvalid = !exists),
|
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];
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -14,8 +14,6 @@ import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract
|
|||||||
import { ItemPrivateComponent } from './item-private/item-private.component';
|
import { ItemPrivateComponent } from './item-private/item-private.component';
|
||||||
import { ItemPublicComponent } from './item-public/item-public.component';
|
import { ItemPublicComponent } from './item-public/item-public.component';
|
||||||
import { ItemDeleteComponent } from './item-delete/item-delete.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 { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
||||||
import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.component';
|
import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.component';
|
||||||
import { SearchPageModule } from '../../search-page/search-page.module';
|
import { SearchPageModule } from '../../search-page/search-page.module';
|
||||||
@@ -63,15 +61,12 @@ import { DsoSharedModule } from '../../dso-shared/dso-shared.module';
|
|||||||
ItemPublicComponent,
|
ItemPublicComponent,
|
||||||
ItemDeleteComponent,
|
ItemDeleteComponent,
|
||||||
ItemStatusComponent,
|
ItemStatusComponent,
|
||||||
ItemMetadataComponent,
|
|
||||||
ItemRelationshipsComponent,
|
ItemRelationshipsComponent,
|
||||||
ItemBitstreamsComponent,
|
ItemBitstreamsComponent,
|
||||||
ItemVersionHistoryComponent,
|
ItemVersionHistoryComponent,
|
||||||
EditInPlaceFieldComponent,
|
|
||||||
ItemEditBitstreamComponent,
|
ItemEditBitstreamComponent,
|
||||||
ItemEditBitstreamBundleComponent,
|
ItemEditBitstreamBundleComponent,
|
||||||
PaginatedDragAndDropBitstreamListComponent,
|
PaginatedDragAndDropBitstreamListComponent,
|
||||||
EditInPlaceFieldComponent,
|
|
||||||
EditRelationshipComponent,
|
EditRelationshipComponent,
|
||||||
EditRelationshipListComponent,
|
EditRelationshipListComponent,
|
||||||
ItemCollectionMapperComponent,
|
ItemCollectionMapperComponent,
|
||||||
@@ -84,9 +79,6 @@ import { DsoSharedModule } from '../../dso-shared/dso-shared.module';
|
|||||||
BundleDataService,
|
BundleDataService,
|
||||||
ObjectValuesPipe
|
ObjectValuesPipe
|
||||||
],
|
],
|
||||||
exports: [
|
|
||||||
ItemMetadataComponent
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class EditItemPageModule {
|
export class EditItemPageModule {
|
||||||
|
|
||||||
|
@@ -1,71 +0,0 @@
|
|||||||
<td>
|
|
||||||
<div class="metadata-field">
|
|
||||||
<div *ngIf="!(editable | async)">
|
|
||||||
<span >{{metadata?.key?.split('.').join('.​')}}</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>
|
|
@@ -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);
|
|
||||||
}
|
|
@@ -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('.​'),
|
|
||||||
value: ('dc.' + mdField1.toString())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayValue: ('dc.' + mdField2.toString()).split('.').join('.​'),
|
|
||||||
value: ('dc.' + mdField2.toString())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayValue: ('dc.' + mdField3.toString()).split('.').join('.​'),
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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('.​'),
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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"> {{"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"> {{"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"> {{"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"> {{"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>
|
|
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1815,6 +1815,8 @@
|
|||||||
|
|
||||||
"item.edit.metadata.headers.value": "Value",
|
"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.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",
|
"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 can’t 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.listelement.badge": "Journal",
|
||||||
|
|
||||||
"journal.page.description": "Description",
|
"journal.page.description": "Description",
|
||||||
|
Reference in New Issue
Block a user