mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'master' into w2p-60168_Alphabetic-browse-widget
Conflicts: src/app/core/browse/browse.service.ts src/app/shared/shared.module.ts
This commit is contained in:
@@ -374,11 +374,18 @@
|
||||
"title": "DSpace Angular :: Metadata Registry",
|
||||
"head": "Metadata Registry",
|
||||
"description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.",
|
||||
"form": {
|
||||
"create": "Create metadata schema",
|
||||
"edit": "Edit metadata schema",
|
||||
"namespace": "Namespace",
|
||||
"name": "Name"
|
||||
},
|
||||
"schemas": {
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"namespace": "Namespace",
|
||||
"name": "Name"
|
||||
"name": "Name",
|
||||
"delete": "Delete selected"
|
||||
},
|
||||
"no-items": "No metadata schemas to show."
|
||||
}
|
||||
@@ -387,13 +394,40 @@
|
||||
"title": "DSpace Angular :: Metadata Schema Registry",
|
||||
"head": "Metadata Schema",
|
||||
"description": "This is the metadata schema for \"{{namespace}}\".",
|
||||
"return": "Return",
|
||||
"form": {
|
||||
"create": "Create metadata field",
|
||||
"edit": "Edit metadata field",
|
||||
"element": "Element",
|
||||
"qualifier": "Qualifier",
|
||||
"scopenote": "Scope Note"
|
||||
},
|
||||
"fields": {
|
||||
"head": "Schema metadata fields",
|
||||
"table": {
|
||||
"field": "Field",
|
||||
"scopenote": "Scope Note"
|
||||
"scopenote": "Scope Note",
|
||||
"delete": "Delete selected"
|
||||
},
|
||||
"no-items": "No metadata fields to show."
|
||||
},
|
||||
"notification": {
|
||||
"success": "Success",
|
||||
"failure": "Error",
|
||||
"created": "Successfully created metadata schema \"{{prefix}}\"",
|
||||
"edited": "Successfully edited metadata schema \"{{prefix}}\"",
|
||||
"deleted": {
|
||||
"success": "Successfully deleted {{amount}} metadata schemas",
|
||||
"failure": "Failed to delete {{amount}} metadata schemas"
|
||||
},
|
||||
"field": {
|
||||
"created": "Successfully created metadata field \"{{field}}\"",
|
||||
"edited": "Successfully edited metadata field \"{{field}}\"",
|
||||
"deleted": {
|
||||
"success": "Successfully deleted {{amount}} metadata fields",
|
||||
"failure": "Failed to delete {{amount}} metadata fields"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bitstream-formats": {
|
||||
|
@@ -7,6 +7,8 @@ import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { BitstreamFormatsComponent } from './bitstream-formats/bitstream-formats.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { MetadataSchemaFormComponent } from './metadata-registry/metadata-schema-form/metadata-schema-form.component';
|
||||
import {MetadataFieldFormComponent} from './metadata-schema/metadata-field-form/metadata-field-form.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -19,7 +21,12 @@ import { SharedModule } from '../../shared/shared.module';
|
||||
declarations: [
|
||||
MetadataRegistryComponent,
|
||||
MetadataSchemaComponent,
|
||||
BitstreamFormatsComponent
|
||||
BitstreamFormatsComponent,
|
||||
MetadataSchemaFormComponent,
|
||||
MetadataFieldFormComponent
|
||||
],
|
||||
entryComponents: [
|
||||
|
||||
]
|
||||
})
|
||||
export class AdminRegistriesModule {
|
||||
|
@@ -0,0 +1,132 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
* enum object for all of this group's action types.
|
||||
*
|
||||
* The 'type' utility function coerces strings into string
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const MetadataRegistryActionTypes = {
|
||||
|
||||
EDIT_SCHEMA: type('dspace/metadata-registry/EDIT_SCHEMA'),
|
||||
CANCEL_EDIT_SCHEMA: type('dspace/metadata-registry/CANCEL_SCHEMA'),
|
||||
SELECT_SCHEMA: type('dspace/metadata-registry/SELECT_SCHEMA'),
|
||||
DESELECT_SCHEMA: type('dspace/metadata-registry/DESELECT_SCHEMA'),
|
||||
DESELECT_ALL_SCHEMA: type('dspace/metadata-registry/DESELECT_ALL_SCHEMA'),
|
||||
|
||||
EDIT_FIELD: type('dspace/metadata-registry/EDIT_FIELD'),
|
||||
CANCEL_EDIT_FIELD: type('dspace/metadata-registry/CANCEL_FIELD'),
|
||||
SELECT_FIELD: type('dspace/metadata-registry/SELECT_FIELD'),
|
||||
DESELECT_FIELD: type('dspace/metadata-registry/DESELECT_FIELD'),
|
||||
DESELECT_ALL_FIELD: type('dspace/metadata-registry/DESELECT_ALL_FIELD')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to collapse the sidebar
|
||||
*/
|
||||
export class MetadataRegistryEditSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.EDIT_SCHEMA;
|
||||
|
||||
schema: MetadataSchema;
|
||||
|
||||
constructor(registry: MetadataSchema) {
|
||||
this.schema = registry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to expand the sidebar
|
||||
*/
|
||||
export class MetadataRegistryCancelSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.CANCEL_EDIT_SCHEMA;
|
||||
}
|
||||
|
||||
export class MetadataRegistrySelectSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.SELECT_SCHEMA;
|
||||
|
||||
schema: MetadataSchema;
|
||||
|
||||
constructor(registry: MetadataSchema) {
|
||||
this.schema = registry;
|
||||
}
|
||||
}
|
||||
|
||||
export class MetadataRegistryDeselectSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.DESELECT_SCHEMA;
|
||||
|
||||
schema: MetadataSchema;
|
||||
|
||||
constructor(registry: MetadataSchema) {
|
||||
this.schema = registry;
|
||||
}
|
||||
}
|
||||
|
||||
export class MetadataRegistryDeselectAllSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.DESELECT_ALL_SCHEMA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to collapse the sidebar
|
||||
*/
|
||||
export class MetadataRegistryEditFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.EDIT_FIELD;
|
||||
|
||||
field: MetadataField;
|
||||
|
||||
constructor(registry: MetadataField) {
|
||||
this.field = registry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to expand the sidebar
|
||||
*/
|
||||
export class MetadataRegistryCancelFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.CANCEL_EDIT_FIELD;
|
||||
}
|
||||
|
||||
export class MetadataRegistrySelectFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.SELECT_FIELD;
|
||||
|
||||
field: MetadataField;
|
||||
|
||||
constructor(registry: MetadataField) {
|
||||
this.field = registry;
|
||||
}
|
||||
}
|
||||
|
||||
export class MetadataRegistryDeselectFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.DESELECT_FIELD;
|
||||
|
||||
field: MetadataField;
|
||||
|
||||
constructor(registry: MetadataField) {
|
||||
this.field = registry;
|
||||
}
|
||||
}
|
||||
|
||||
export class MetadataRegistryDeselectAllFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.DESELECT_ALL_FIELD;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
* so that reducers can easily compose action types
|
||||
*/
|
||||
export type MetadataRegistryAction
|
||||
= MetadataRegistryEditSchemaAction
|
||||
| MetadataRegistryCancelSchemaAction
|
||||
| MetadataRegistrySelectSchemaAction
|
||||
| MetadataRegistryDeselectSchemaAction
|
||||
| MetadataRegistryEditFieldAction
|
||||
| MetadataRegistryCancelFieldAction
|
||||
| MetadataRegistrySelectFieldAction
|
||||
| MetadataRegistryDeselectFieldAction;
|
@@ -1,42 +1,61 @@
|
||||
<div class="container">
|
||||
<div class="metadata-registry row">
|
||||
<div class="col-12">
|
||||
<div class="metadata-registry row">
|
||||
<div class="col-12">
|
||||
|
||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h2>
|
||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h2>
|
||||
|
||||
<p id="description" class="pb-2">{{'admin.registries.metadata.description' | translate}}</p>
|
||||
<p id="description" class="pb-2">{{'admin.registries.metadata.description' | translate}}</p>
|
||||
|
||||
<ds-metadata-schema-form (submitForm)="forceUpdateSchemas()"></ds-metadata-schema-form>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataSchemas | async)?.payload"
|
||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-schemas" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.namespace' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.name' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
[checked]="isSelected(schema) | async"
|
||||
(change)="selectMetadataSchema(schema, $event)"
|
||||
>
|
||||
</label>
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.namespace}}</a></td>
|
||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(metadataSchemas | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.metadata.schemas.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button *ngIf="(metadataSchemas | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteSchemas()">{{'admin.registries.metadata.schemas.table.delete' | translate}}</button>
|
||||
</div>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataSchemas | async)?.payload"
|
||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-schemas" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.namespace' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.name' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page">
|
||||
<td><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
||||
<td><a [routerLink]="[schema.prefix]">{{schema.namespace}}</a></td>
|
||||
<td><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
<div *ngIf="(metadataSchemas | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert">
|
||||
{{'admin.registries.metadata.schemas.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,5 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
|
||||
.selectable-row:hover {
|
||||
cursor: pointer;
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { MetadataRegistryComponent } from './metadata-registry.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
@@ -13,6 +13,10 @@ import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||
import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
|
||||
describe('MetadataRegistryComponent', () => {
|
||||
let comp: MetadataRegistryComponent;
|
||||
@@ -33,9 +37,18 @@ describe('MetadataRegistryComponent', () => {
|
||||
}
|
||||
];
|
||||
const mockSchemas = observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
getSelectedMetadataSchemas: () => observableOf([]),
|
||||
editMetadataSchema: (schema) => {},
|
||||
cancelEditMetadataSchema: () => {},
|
||||
deleteMetadataSchema: () => observableOf(new RestResponse(true, '200')),
|
||||
deselectAllMetadataSchema: () => {},
|
||||
clearMetadataSchemaRequests: () => observableOf(undefined)
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -43,8 +56,12 @@ describe('MetadataRegistryComponent', () => {
|
||||
declarations: [MetadataRegistryComponent, PaginationComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
||||
]
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MetadataRegistryComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
@@ -52,19 +69,66 @@ describe('MetadataRegistryComponent', () => {
|
||||
fixture = TestBed.createComponent(MetadataRegistryComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
registryService = (comp as any).service;
|
||||
});
|
||||
|
||||
beforeEach(inject([RegistryService], (s) => {
|
||||
registryService = s;
|
||||
}));
|
||||
|
||||
it('should contain two schemas', () => {
|
||||
const tbody: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas>tbody')).nativeElement;
|
||||
expect(tbody.children.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should contain the correct schemas', () => {
|
||||
const dcName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(1) td:nth-child(3)')).nativeElement;
|
||||
const dcName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(1) td:nth-child(4)')).nativeElement;
|
||||
expect(dcName.textContent).toBe('dc');
|
||||
|
||||
const mockName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(2) td:nth-child(3)')).nativeElement;
|
||||
const mockName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(2) td:nth-child(4)')).nativeElement;
|
||||
expect(mockName.textContent).toBe('mock');
|
||||
});
|
||||
|
||||
describe('when clicking a metadata schema row', () => {
|
||||
let row: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'editMetadataSchema');
|
||||
row = fixture.debugElement.query(By.css('.selectable-row')).nativeElement;
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should start editing the selected schema', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.editMetadataSchema).toHaveBeenCalledWith(mockSchemasList[0]);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should cancel editing the selected schema when clicked again', async(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0]));
|
||||
spyOn(registryService, 'cancelEditMetadataSchema');
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.cancelEditMetadataSchema).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when deleting metadata schemas', () => {
|
||||
const selectedSchemas = Array(mockSchemasList[0]);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
||||
spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas));
|
||||
comp.deleteSchemas();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call deleteMetadataSchema with the selected id', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.deleteMetadataSchema).toHaveBeenCalledWith(selectedSchemas[0].id);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@@ -1,34 +1,173 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { zip } from 'rxjs/internal/observable/zip';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { Route, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-registry',
|
||||
templateUrl: './metadata-registry.component.html'
|
||||
templateUrl: './metadata-registry.component.html',
|
||||
styleUrls: ['./metadata-registry.component.scss']
|
||||
})
|
||||
/**
|
||||
* A component used for managing all existing metadata schemas within the repository.
|
||||
* The admin can create, edit or delete metadata schemas here.
|
||||
*/
|
||||
export class MetadataRegistryComponent {
|
||||
|
||||
/**
|
||||
* A list of all the current metadata schemas within the repository
|
||||
*/
|
||||
metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>;
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of metadata schemas
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-metadataschemas-pagination',
|
||||
pageSize: 10000
|
||||
pageSize: 25
|
||||
});
|
||||
|
||||
constructor(private registryService: RegistryService) {
|
||||
constructor(private registryService: RegistryService,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private translateService: TranslateService) {
|
||||
this.updateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of schemas by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateSchemas() {
|
||||
this.metadataSchemas = this.registryService.getMetadataSchemas(this.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of schemas by first clearing the cache related to metadata schemas, then performing
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateSchemas() {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.updateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing the selected metadata schema
|
||||
* @param schema
|
||||
*/
|
||||
editSchema(schema: MetadataSchema) {
|
||||
this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => {
|
||||
if (schema === activeSchema) {
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
} else {
|
||||
this.registryService.editMetadataSchema(schema);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given metadata schema is active (being edited)
|
||||
* @param schema
|
||||
*/
|
||||
isActive(schema: MetadataSchema): Observable<boolean> {
|
||||
return this.getActiveSchema().pipe(
|
||||
map((activeSchema) => schema === activeSchema)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active metadata schema (being edited)
|
||||
*/
|
||||
getActiveSchema(): Observable<MetadataSchema> {
|
||||
return this.registryService.getActiveMetadataSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a metadata schema within the list (checkbox)
|
||||
* @param schema
|
||||
* @param event
|
||||
*/
|
||||
selectMetadataSchema(schema: MetadataSchema, event) {
|
||||
event.target.checked ?
|
||||
this.registryService.selectMetadataSchema(schema) :
|
||||
this.registryService.deselectMetadataSchema(schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given metadata schema is selected in the list (checkbox)
|
||||
* @param schema
|
||||
*/
|
||||
isSelected(schema: MetadataSchema): Observable<boolean> {
|
||||
return this.registryService.getSelectedMetadataSchemas().pipe(
|
||||
map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the selected metadata schemas
|
||||
*/
|
||||
deleteSchemas() {
|
||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||
(schemas) => {
|
||||
const tasks$ = [];
|
||||
for (const schema of schemas) {
|
||||
if (hasValue(schema.id)) {
|
||||
tasks$.push(this.registryService.deleteMetadataSchema(schema.id));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((responses: RestResponse[]) => {
|
||||
const successResponses = responses.filter((response: RestResponse) => response.isSuccessful);
|
||||
const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
this.registryService.deselectAllMetadataSchema();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
this.forceUpdateSchemas();
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notifications for an amount of deleted metadata schemas
|
||||
* @param success Whether or not the notification should be a success message (error message when false)
|
||||
* @param amount The amount of deleted metadata schemas
|
||||
*/
|
||||
showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount })
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content)
|
||||
} else {
|
||||
this.notificationsService.error(head, content)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Makes sure that if the user navigates to another route, the sidebar is collapsed
|
||||
*/
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
import { filter, map, tap } from 'rxjs/operators';
|
||||
import { SearchSidebarCollapseAction } from '../../../+search-page/search-sidebar/search-sidebar.actions';
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
import { URLBaser } from '../../../core/url-baser/url-baser';
|
||||
|
||||
@Injectable()
|
||||
export class SearchSidebarEffects {
|
||||
private previousPath: string;
|
||||
@Effect() routeChange$ = this.actions$
|
||||
.pipe(
|
||||
ofType(fromRouter.ROUTER_NAVIGATION),
|
||||
filter((action) => this.previousPath !== this.getBaseUrl(action)),
|
||||
tap((action) => {
|
||||
this.previousPath = this.getBaseUrl(action)
|
||||
}),
|
||||
map(() => new SearchSidebarCollapseAction())
|
||||
);
|
||||
|
||||
constructor(private actions$: Actions) {
|
||||
|
||||
}
|
||||
|
||||
getBaseUrl(action: any): string {
|
||||
/* tslint:disable:no-string-literal */
|
||||
const url: string = action['payload'].routerState.url;
|
||||
return new URLBaser(url).toString();
|
||||
/* tslint:enable:no-string-literal */
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,111 @@
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import {
|
||||
MetadataRegistryAction,
|
||||
MetadataRegistryActionTypes,
|
||||
MetadataRegistryDeselectFieldAction,
|
||||
MetadataRegistryDeselectSchemaAction,
|
||||
MetadataRegistryEditFieldAction,
|
||||
MetadataRegistryEditSchemaAction,
|
||||
MetadataRegistrySelectFieldAction,
|
||||
MetadataRegistrySelectSchemaAction
|
||||
} from './metadata-registry.actions';
|
||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||
|
||||
/**
|
||||
* The auth state.
|
||||
* @interface State
|
||||
*/
|
||||
export interface MetadataRegistryState {
|
||||
editSchema: MetadataSchema;
|
||||
selectedSchemas: MetadataSchema[];
|
||||
editField: MetadataField;
|
||||
selectedFields: MetadataField[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial state.
|
||||
*/
|
||||
const initialState: MetadataRegistryState = {
|
||||
editSchema: null,
|
||||
selectedSchemas: [],
|
||||
editField: null,
|
||||
selectedFields: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer that handles MetadataRegistryActions to modify metadata schema and/or field states
|
||||
* @param state The current MetadataRegistryState
|
||||
* @param action The MetadataRegistryAction to perform on the state
|
||||
*/
|
||||
export function metadataRegistryReducer(state = initialState, action: MetadataRegistryAction): MetadataRegistryState {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case MetadataRegistryActionTypes.EDIT_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
editSchema: (action as MetadataRegistryEditSchemaAction).schema
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.CANCEL_EDIT_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
editSchema: null
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.SELECT_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
selectedSchemas: [...state.selectedSchemas, (action as MetadataRegistrySelectSchemaAction).schema]
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.DESELECT_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
selectedSchemas: state.selectedSchemas.filter(
|
||||
(selectedSchema) => selectedSchema !== (action as MetadataRegistryDeselectSchemaAction).schema
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.DESELECT_ALL_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
selectedSchemas: []
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.EDIT_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
editField: (action as MetadataRegistryEditFieldAction).field
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.CANCEL_EDIT_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
editField: null
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.SELECT_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
selectedFields: [...state.selectedFields, (action as MetadataRegistrySelectFieldAction).field]
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.DESELECT_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
selectedFields: state.selectedFields.filter(
|
||||
(selectedField) => selectedField !== (action as MetadataRegistryDeselectFieldAction).field
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.DESELECT_ALL_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
selectedFields: []
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
<div *ngIf="registryService.getActiveMetadataSchema() | async; then editheader; else createHeader"></div>
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h4>{{messagePrefix + '.create' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h4>{{messagePrefix + '.edit' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
(cancel)="onCancel()"
|
||||
(submitForm)="onSubmit()">
|
||||
|
||||
</ds-form>
|
@@ -0,0 +1,105 @@
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
|
||||
describe('MetadataSchemaFormComponent', () => {
|
||||
let component: MetadataSchemaFormComponent;
|
||||
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
||||
let registryService: RegistryService;
|
||||
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema)
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
return {
|
||||
patchValue: () => {}
|
||||
};
|
||||
}
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [ MetadataSchemaFormComponent, EnumKeysPipe ],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: FormBuilderService, useValue: formBuilderServiceStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MetadataSchemaFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
beforeEach(inject([RegistryService], (s) => {
|
||||
registryService = s;
|
||||
}));
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
const namespace = 'fake namespace';
|
||||
const prefix = 'fake';
|
||||
|
||||
const expected = Object.assign(new MetadataSchema(), {
|
||||
namespace: namespace,
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.name.value = prefix;
|
||||
component.namespace.value = namespace;
|
||||
});
|
||||
|
||||
describe('without an active schema', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new schema using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with an active schema', () => {
|
||||
const expectedWithId = Object.assign(new MetadataSchema(), {
|
||||
id: 1,
|
||||
namespace: namespace,
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should edit the existing schema using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,171 @@
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormLayout,
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-schema-form',
|
||||
templateUrl: './metadata-schema-form.component.html'
|
||||
})
|
||||
/**
|
||||
* A form used for creating and editing metadata schemas
|
||||
*/
|
||||
export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* A unique id used for ds-form
|
||||
*/
|
||||
formId = 'metadata-schema-form';
|
||||
|
||||
/**
|
||||
* The prefix for all messages related to this form
|
||||
*/
|
||||
messagePrefix = 'admin.registries.metadata.form';
|
||||
|
||||
/**
|
||||
* A dynamic input model for the name field
|
||||
*/
|
||||
name: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A dynamic input model for the namespace field
|
||||
*/
|
||||
namespace: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A list of all dynamic input models
|
||||
*/
|
||||
formModel: DynamicFormControlModel[];
|
||||
|
||||
/**
|
||||
* Layout used for structuring the form inputs
|
||||
*/
|
||||
formLayout: DynamicFormLayout = {
|
||||
name: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
},
|
||||
namespace: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
*/
|
||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
combineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.name`),
|
||||
this.translateService.get(`${this.messagePrefix}.namespace`)
|
||||
).subscribe(([name, namespace]) => {
|
||||
this.name = new DynamicInputModel({
|
||||
id: 'name',
|
||||
label: name,
|
||||
name: 'name',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[^ ,_]{1,32}$'
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.namespace = new DynamicInputModel({
|
||||
id: 'namespace',
|
||||
label: namespace,
|
||||
name: 'namespace',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.formModel = [
|
||||
this.namespace,
|
||||
this.name
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.registryService.getActiveMetadataSchema().subscribe((schema) => {
|
||||
this.formGroup.patchValue({
|
||||
name: schema != null ? schema.prefix : '',
|
||||
namespace: schema != null ? schema.namespace : ''
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop editing the currently selected metadata schema
|
||||
*/
|
||||
onCancel() {
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form
|
||||
* When the schema has an id attached -> Edit the schema
|
||||
* When the schema has no id attached -> Create new schema
|
||||
* Emit the updated/created schema using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||
(schema) => {
|
||||
const values = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value
|
||||
};
|
||||
if (schema == null) {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), values)).subscribe((newSchema) => {
|
||||
this.submitForm.emit(newSchema);
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), {
|
||||
id: schema.id,
|
||||
prefix: (values.prefix ? values.prefix : schema.prefix),
|
||||
namespace: (values.namespace ? values.namespace : schema.namespace)
|
||||
})).subscribe((updatedSchema) => {
|
||||
this.submitForm.emit(updatedSchema);
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty
|
||||
*/
|
||||
clearFields() {
|
||||
this.formGroup.patchValue({
|
||||
prefix: '',
|
||||
namespace: ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
<div *ngIf="registryService.getActiveMetadataField() | async; then editheader; else createHeader"></div>
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h4>{{messagePrefix + '.create' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h4>{{messagePrefix + '.edit' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
[formGroup]="formGroup"
|
||||
(cancel)="onCancel()"
|
||||
(submit)="onSubmit()">
|
||||
|
||||
</ds-form>
|
@@ -0,0 +1,119 @@
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MetadataFieldFormComponent } from './metadata-field-form.component';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
|
||||
describe('MetadataFieldFormComponent', () => {
|
||||
let component: MetadataFieldFormComponent;
|
||||
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
||||
let registryService: RegistryService;
|
||||
|
||||
const metadataSchema = Object.assign(new MetadataSchema(), {
|
||||
id: 1,
|
||||
namespace: 'fake schema',
|
||||
prefix: 'fake'
|
||||
});
|
||||
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field)
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
return {
|
||||
patchValue: () => {}
|
||||
};
|
||||
}
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [ MetadataFieldFormComponent, EnumKeysPipe ],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: FormBuilderService, useValue: formBuilderServiceStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MetadataFieldFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.metadataSchema = metadataSchema;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
beforeEach(inject([RegistryService], (s) => {
|
||||
registryService = s;
|
||||
}));
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
const element = 'fakeElement';
|
||||
const qualifier = 'fakeQualifier';
|
||||
const scopeNote = 'fakeScopeNote';
|
||||
|
||||
const expected = Object.assign(new MetadataField(), {
|
||||
schema: metadataSchema,
|
||||
element: element,
|
||||
qualifier: qualifier,
|
||||
scopeNote: scopeNote
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.element.value = element;
|
||||
component.qualifier.value = qualifier;
|
||||
component.scopeNote.value = scopeNote;
|
||||
});
|
||||
|
||||
describe('without an active field', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(undefined));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new field using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with an active field', () => {
|
||||
const expectedWithId = Object.assign(new MetadataField(), {
|
||||
id: 1,
|
||||
schema: metadataSchema,
|
||||
element: element,
|
||||
qualifier: qualifier,
|
||||
scopeNote: scopeNote
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(expectedWithId));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should edit the existing field using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,203 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
import {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormLayout,
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-field-form',
|
||||
templateUrl: './metadata-field-form.component.html'
|
||||
})
|
||||
/**
|
||||
* A form used for creating and editing metadata fields
|
||||
*/
|
||||
export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* A unique id used for ds-form
|
||||
*/
|
||||
formId = 'metadata-field-form';
|
||||
|
||||
/**
|
||||
* The prefix for all messages related to this form
|
||||
*/
|
||||
messagePrefix = 'admin.registries.schema.form';
|
||||
|
||||
/**
|
||||
* The metadata schema this field is attached to
|
||||
*/
|
||||
@Input() metadataSchema: MetadataSchema;
|
||||
|
||||
/**
|
||||
* A dynamic input model for the element field
|
||||
*/
|
||||
element: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A dynamic input model for the qualifier field
|
||||
*/
|
||||
qualifier: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A dynamic input model for the scopeNote field
|
||||
*/
|
||||
scopeNote: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A list of all dynamic input models
|
||||
*/
|
||||
formModel: DynamicFormControlModel[];
|
||||
|
||||
/**
|
||||
* Layout used for structuring the form inputs
|
||||
*/
|
||||
formLayout: DynamicFormLayout = {
|
||||
element: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
},
|
||||
qualifier: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
},
|
||||
scopeNote: {
|
||||
grid: {
|
||||
host: 'col col-sm-12 d-inline-block'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
*/
|
||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
constructor(public registryService: RegistryService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component, setting up the necessary Models for the dynamic form
|
||||
*/
|
||||
ngOnInit() {
|
||||
combineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.element`),
|
||||
this.translateService.get(`${this.messagePrefix}.qualifier`),
|
||||
this.translateService.get(`${this.messagePrefix}.scopenote`)
|
||||
).subscribe(([element, qualifier, scopenote]) => {
|
||||
this.element = new DynamicInputModel({
|
||||
id: 'element',
|
||||
label: element,
|
||||
name: 'element',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.qualifier = new DynamicInputModel({
|
||||
id: 'qualifier',
|
||||
label: qualifier,
|
||||
name: 'qualifier',
|
||||
required: false,
|
||||
});
|
||||
this.scopeNote = new DynamicInputModel({
|
||||
id: 'scopeNote',
|
||||
label: scopenote,
|
||||
name: 'scopeNote',
|
||||
required: false,
|
||||
});
|
||||
this.formModel = [
|
||||
this.element,
|
||||
this.qualifier,
|
||||
this.scopeNote
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.registryService.getActiveMetadataField().subscribe((field) => {
|
||||
this.formGroup.patchValue({
|
||||
element: field != null ? field.element : '',
|
||||
qualifier: field != null ? field.qualifier : '',
|
||||
scopeNote: field != null ? field.scopeNote : ''
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop editing the currently selected metadata field
|
||||
*/
|
||||
onCancel() {
|
||||
this.registryService.cancelEditMetadataField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form
|
||||
* When the field has an id attached -> Edit the field
|
||||
* When the field has no id attached -> Create new field
|
||||
* Emit the updated/created field using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||
(field) => {
|
||||
const values = {
|
||||
schema: this.metadataSchema,
|
||||
element: this.element.value,
|
||||
qualifier: this.qualifier.value,
|
||||
scopeNote: this.scopeNote.value
|
||||
};
|
||||
if (field == null) {
|
||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), values)).subscribe((newField) => {
|
||||
this.submitForm.emit(newField);
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), {
|
||||
id: field.id,
|
||||
schema: this.metadataSchema,
|
||||
element: (values.element ? values.element : field.element),
|
||||
qualifier: (values.qualifier ? values.qualifier : field.qualifier),
|
||||
scopeNote: (values.scopeNote ? values.scopeNote : field.scopeNote)
|
||||
})).subscribe((updatedField) => {
|
||||
this.submitForm.emit(updatedField);
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty
|
||||
*/
|
||||
clearFields() {
|
||||
this.formGroup.patchValue({
|
||||
element: '',
|
||||
qualifier: '',
|
||||
scopeNote: ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
@@ -6,36 +6,56 @@
|
||||
|
||||
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:namespace }}</p>
|
||||
|
||||
<ds-metadata-field-form
|
||||
[metadataSchema]="(metadataSchema | async)?.payload"
|
||||
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
||||
|
||||
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(metadataFields | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataFields | async)?.payload"
|
||||
[collectionSize]="(metadataFields | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-fields" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let field of (metadataFields | async)?.payload?.page">
|
||||
<td>{{(metadataSchema | async)?.payload?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||
<td>{{field.scopeNote}}</td>
|
||||
<tr *ngFor="let field of (metadataFields | async)?.payload?.page"
|
||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
[checked]="isSelected(field) | async"
|
||||
(change)="selectMetadataField(field, $event)">
|
||||
</label>
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{(metadataSchema | async)?.payload?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
<div *ngIf="(metadataFields | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert">
|
||||
|
||||
<div *ngIf="(metadataFields | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.schema.fields.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
||||
<button *ngIf="(metadataFields | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,5 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
|
||||
.selectable-row:hover {
|
||||
cursor: pointer;
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { MetadataSchemaComponent } from './metadata-schema.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
@@ -17,6 +17,10 @@ import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
|
||||
describe('MetadataSchemaComponent', () => {
|
||||
let comp: MetadataSchemaComponent;
|
||||
@@ -38,40 +42,53 @@ describe('MetadataSchemaComponent', () => {
|
||||
];
|
||||
const mockFieldsList = [
|
||||
{
|
||||
id: 1,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8',
|
||||
element: 'contributor',
|
||||
qualifier: 'advisor',
|
||||
scopenote: null,
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9',
|
||||
element: 'contributor',
|
||||
qualifier: 'author',
|
||||
scopenote: null,
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10',
|
||||
element: 'contributor',
|
||||
qualifier: 'editor',
|
||||
scopenote: 'test scope note',
|
||||
scopeNote: 'test scope note',
|
||||
schema: mockSchemasList[1]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11',
|
||||
element: 'contributor',
|
||||
qualifier: 'illustrator',
|
||||
scopenote: null,
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[1]
|
||||
}
|
||||
];
|
||||
const mockSchemas = observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema)))),
|
||||
getMetadataSchemaByName: (schemaName: string) => observableOf(new RemoteData(false, false, true, undefined, mockSchemasList.filter((value) => value.prefix === schemaName)[0]))
|
||||
getMetadataSchemaByName: (schemaName: string) => observableOf(new RemoteData(false, false, true, undefined, mockSchemasList.filter((value) => value.prefix === schemaName)[0])),
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
getSelectedMetadataFields: () => observableOf([]),
|
||||
editMetadataField: (schema) => {},
|
||||
cancelEditMetadataField: () => {},
|
||||
deleteMetadataField: () => observableOf(new RestResponse(true, '200')),
|
||||
deselectAllMetadataField: () => {},
|
||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
const schemaNameParam = 'mock';
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({
|
||||
@@ -87,8 +104,10 @@ describe('MetadataSchemaComponent', () => {
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: Router, useValue: new RouterStub() }
|
||||
]
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
@@ -96,9 +115,12 @@ describe('MetadataSchemaComponent', () => {
|
||||
fixture = TestBed.createComponent(MetadataSchemaComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
registryService = (comp as any).service;
|
||||
});
|
||||
|
||||
beforeEach(inject([RegistryService], (s) => {
|
||||
registryService = s;
|
||||
}));
|
||||
|
||||
it('should contain the schema prefix in the header', () => {
|
||||
const header: HTMLElement = fixture.debugElement.query(By.css('.metadata-schema #header')).nativeElement;
|
||||
expect(header.textContent).toContain('mock');
|
||||
@@ -110,10 +132,54 @@ describe('MetadataSchemaComponent', () => {
|
||||
});
|
||||
|
||||
it('should contain the correct fields', () => {
|
||||
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(1)')).nativeElement;
|
||||
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
||||
expect(editorField.textContent).toBe('mock.contributor.editor');
|
||||
|
||||
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(1)')).nativeElement;
|
||||
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(2)')).nativeElement;
|
||||
expect(illustratorField.textContent).toBe('mock.contributor.illustrator');
|
||||
});
|
||||
|
||||
describe('when clicking a metadata field row', () => {
|
||||
let row: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'editMetadataField');
|
||||
row = fixture.debugElement.query(By.css('.selectable-row')).nativeElement;
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should start editing the selected field', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.editMetadataField).toHaveBeenCalledWith(mockFieldsList[2]);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should cancel editing the selected field when clicked again', async(() => {
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2]));
|
||||
spyOn(registryService, 'cancelEditMetadataField');
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.cancelEditMetadataField).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when deleting metadata fields', () => {
|
||||
const selectedFields = Array(mockFieldsList[2]);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'deleteMetadataField').and.callThrough();
|
||||
spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields));
|
||||
comp.deleteFields();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call deleteMetadataField with the selected id', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.deleteMetadataField).toHaveBeenCalledWith(selectedFields[0].id);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@@ -1,29 +1,59 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { zip } from 'rxjs/internal/observable/zip';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-schema',
|
||||
templateUrl: './metadata-schema.component.html'
|
||||
templateUrl: './metadata-schema.component.html',
|
||||
styleUrls: ['./metadata-schema.component.scss']
|
||||
})
|
||||
/**
|
||||
* A component used for managing all existing metadata fields within the current metadata schema.
|
||||
* The admin can create, edit or delete metadata fields here.
|
||||
*/
|
||||
export class MetadataSchemaComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The namespace of the metadata schema
|
||||
*/
|
||||
namespace;
|
||||
|
||||
/**
|
||||
* The metadata schema
|
||||
*/
|
||||
metadataSchema: Observable<RemoteData<MetadataSchema>>;
|
||||
|
||||
/**
|
||||
* A list of all the fields attached to this metadata schema
|
||||
*/
|
||||
metadataFields: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of metadata fields
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-metadatafields-pagination',
|
||||
pageSize: 10000
|
||||
pageSize: 25,
|
||||
pageSizeOptions: [25, 50, 100, 200]
|
||||
});
|
||||
|
||||
constructor(private registryService: RegistryService, private route: ActivatedRoute) {
|
||||
constructor(private registryService: RegistryService,
|
||||
private route: ActivatedRoute,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private translateService: TranslateService) {
|
||||
|
||||
}
|
||||
|
||||
@@ -33,22 +63,143 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component using the params within the url (schemaName)
|
||||
* @param params
|
||||
*/
|
||||
initialize(params) {
|
||||
this.metadataSchema = this.registryService.getMetadataSchemaByName(params.schemaName);
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of fields by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateFields() {
|
||||
this.metadataSchema.subscribe((schemaData) => {
|
||||
const schema = schemaData.payload;
|
||||
this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, this.config);
|
||||
this.namespace = { namespace: schemaData.payload.namespace };
|
||||
this.namespace = {namespace: schemaData.payload.namespace};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of fields by first clearing the cache related to metadata fields, then performing
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateFields() {
|
||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing the selected metadata field
|
||||
* @param field
|
||||
*/
|
||||
editField(field: MetadataField) {
|
||||
this.getActiveField().pipe(take(1)).subscribe((activeField) => {
|
||||
if (field === activeField) {
|
||||
this.registryService.cancelEditMetadataField();
|
||||
} else {
|
||||
this.registryService.editMetadataField(field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given metadata field is active (being edited)
|
||||
* @param field
|
||||
*/
|
||||
isActive(field: MetadataField): Observable<boolean> {
|
||||
return this.getActiveField().pipe(
|
||||
map((activeField) => field === activeField)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active metadata field (being edited)
|
||||
*/
|
||||
getActiveField(): Observable<MetadataField> {
|
||||
return this.registryService.getActiveMetadataField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a metadata field within the list (checkbox)
|
||||
* @param field
|
||||
* @param event
|
||||
*/
|
||||
selectMetadataField(field: MetadataField, event) {
|
||||
event.target.checked ?
|
||||
this.registryService.selectMetadataField(field) :
|
||||
this.registryService.deselectMetadataField(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given metadata field is selected in the list (checkbox)
|
||||
* @param field
|
||||
*/
|
||||
isSelected(field: MetadataField): Observable<boolean> {
|
||||
return this.registryService.getSelectedMetadataFields().pipe(
|
||||
map((fields) => fields.find((selectedField) => selectedField === field) != null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the selected metadata fields
|
||||
*/
|
||||
deleteFields() {
|
||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
||||
(fields) => {
|
||||
const tasks$ = [];
|
||||
for (const field of fields) {
|
||||
if (hasValue(field.id)) {
|
||||
tasks$.push(this.registryService.deleteMetadataField(field.id));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((responses: RestResponse[]) => {
|
||||
const successResponses = responses.filter((response: RestResponse) => response.isSuccessful);
|
||||
const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
this.registryService.deselectAllMetadataField();
|
||||
this.registryService.cancelEditMetadataField();
|
||||
this.forceUpdateFields();
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notifications for an amount of deleted metadata fields
|
||||
* @param success Whether or not the notification should be a success message (error message when false)
|
||||
* @param amount The amount of deleted metadata fields
|
||||
*/
|
||||
showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount })
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content)
|
||||
} else {
|
||||
this.notificationsService.error(head, content)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,10 @@ import { NgModule } from '@angular/core';
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: 'registries', loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule' }
|
||||
{
|
||||
path: 'registries',
|
||||
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
@@ -78,7 +78,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
|
||||
let lowerLimit = this.config.browseBy.defaultLowerLimit;
|
||||
if (hasValue(firstItemRD.payload)) {
|
||||
const date = firstItemRD.payload.findMetadata(metadataField);
|
||||
const date = firstItemRD.payload.firstMetadataValue(metadataField);
|
||||
if (hasValue(date) && hasValue(+date.split('-')[0])) {
|
||||
lowerLimit = +date.split('-')[0];
|
||||
}
|
||||
|
@@ -18,42 +18,38 @@ describe('SubCommunityList Component', () => {
|
||||
|
||||
const subcommunities = [Object.assign(new Community(), {
|
||||
id: '123456789-1',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'SubCommunity 1'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ language: 'en_US', value: 'SubCommunity 1' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
Object.assign(new Community(), {
|
||||
id: '123456789-2',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'SubCommunity 2'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ language: 'en_US', value: 'SubCommunity 2' }
|
||||
]
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
const emptySubCommunitiesCommunity = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}],
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ language: 'en_US', value: 'Test title' }
|
||||
]
|
||||
},
|
||||
subcommunities: observableOf(new RemoteData(true, true, true,
|
||||
undefined, new PaginatedList(new PageInfo(), [])))
|
||||
});
|
||||
|
||||
const mockCommunity = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}],
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ language: 'en_US', value: 'Test title' }
|
||||
]
|
||||
},
|
||||
subcommunities: observableOf(new RemoteData(true, true, true,
|
||||
undefined, new PaginatedList(new PageInfo(), subcommunities)))
|
||||
})
|
||||
|
@@ -7,10 +7,12 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let metadatum of metadata" class="metadata-row">
|
||||
<td>{{metadatum.key}}</td>
|
||||
<td>{{metadatum.value}}</td>
|
||||
<td>{{metadatum.language}}</td>
|
||||
</tr>
|
||||
<ng-container *ngFor="let mdEntry of metadata | keyvalue">
|
||||
<tr *ngFor="let mdValue of mdEntry.value" class="metadata-row">
|
||||
<td>{{mdEntry.key}}</td>
|
||||
<td>{{mdValue.value}}</td>
|
||||
<td>{{mdValue.language}}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
@@ -11,10 +11,14 @@ const mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
handle: 'fake/handle',
|
||||
lastModified: '2018',
|
||||
metadata: [
|
||||
{key: 'dc.title', value: 'Mock item title', language: 'en'},
|
||||
{key: 'dc.contributor.author', value: 'Mayer, Ed', language: ''}
|
||||
]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ value: 'Mock item title', language: 'en' }
|
||||
],
|
||||
'dc.contributor.author': [
|
||||
{ value: 'Mayer, Ed', language: '' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
describe('ModifyItemOverviewComponent', () => {
|
||||
@@ -37,19 +41,19 @@ describe('ModifyItemOverviewComponent', () => {
|
||||
const metadataRows = fixture.debugElement.queryAll(By.css('tr.metadata-row'));
|
||||
expect(metadataRows.length).toEqual(2);
|
||||
|
||||
const titleRow = metadataRows[0].queryAll(By.css('td'));
|
||||
expect(titleRow.length).toEqual(3);
|
||||
|
||||
expect(titleRow[0].nativeElement.innerHTML).toContain('dc.title');
|
||||
expect(titleRow[1].nativeElement.innerHTML).toContain('Mock item title');
|
||||
expect(titleRow[2].nativeElement.innerHTML).toContain('en');
|
||||
|
||||
const authorRow = metadataRows[1].queryAll(By.css('td'));
|
||||
const authorRow = metadataRows[0].queryAll(By.css('td'));
|
||||
expect(authorRow.length).toEqual(3);
|
||||
|
||||
expect(authorRow[0].nativeElement.innerHTML).toContain('dc.contributor.author');
|
||||
expect(authorRow[1].nativeElement.innerHTML).toContain('Mayer, Ed');
|
||||
expect(authorRow[2].nativeElement.innerHTML).toEqual('');
|
||||
|
||||
const titleRow = metadataRows[1].queryAll(By.css('td'));
|
||||
expect(titleRow.length).toEqual(3);
|
||||
|
||||
expect(titleRow[0].nativeElement.innerHTML).toContain('dc.title');
|
||||
expect(titleRow[1].nativeElement.innerHTML).toContain('Mock item title');
|
||||
expect(titleRow[2].nativeElement.innerHTML).toContain('en');
|
||||
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {Item} from '../../../core/shared/item.model';
|
||||
import {Metadatum} from '../../../core/shared/metadatum.model';
|
||||
import {MetadataMap} from '../../../core/shared/metadata.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-modify-item-overview',
|
||||
@@ -12,7 +12,7 @@ import {Metadatum} from '../../../core/shared/metadatum.model';
|
||||
export class ModifyItemOverviewComponent implements OnInit {
|
||||
|
||||
@Input() item: Item;
|
||||
metadata: Metadatum[];
|
||||
metadata: MetadataMap;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.metadata = this.item.metadata;
|
||||
|
@@ -14,12 +14,14 @@ let collectionsComponent: CollectionsComponent;
|
||||
let fixture: ComponentFixture<CollectionsComponent>;
|
||||
|
||||
const mockCollection1: Collection = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: observableOf(new RemoteData(false, false, true, null, mockCollection1))});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<ds-metadata-field-wrapper [label]="label | translate">
|
||||
<a *ngFor="let metadatum of values; let last=last;" [href]="metadatum.value">
|
||||
{{ linktext || metadatum.value }}<span *ngIf="!last" [innerHTML]="separator"></span>
|
||||
<a *ngFor="let mdValue of mdValues; let last=last;" [href]="mdValue.value">
|
||||
{{ linktext || mdValue.value }}<span *ngIf="!last" [innerHTML]="separator"></span>
|
||||
</a>
|
||||
</ds-metadata-field-wrapper>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
import { MetadataValuesComponent } from '../metadata-values/metadata-values.component';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.interfaces';
|
||||
|
||||
/**
|
||||
* This component renders the configured 'values' into the ds-metadata-field-wrapper component as a link.
|
||||
@@ -18,7 +19,7 @@ export class MetadataUriValuesComponent extends MetadataValuesComponent {
|
||||
|
||||
@Input() linktext: any;
|
||||
|
||||
@Input() values: any;
|
||||
@Input() mdValues: MetadataValue[];
|
||||
|
||||
@Input() separator: string;
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<ds-metadata-field-wrapper [label]="label | translate">
|
||||
<span *ngFor="let metadatum of values; let last=last;">
|
||||
{{metadatum.value}}<span *ngIf="!last" [innerHTML]="separator"></span>
|
||||
<span *ngFor="let mdValue of mdValues; let last=last;">
|
||||
{{mdValue.value}}<span *ngIf="!last" [innerHTML]="separator"></span>
|
||||
</span>
|
||||
</ds-metadata-field-wrapper>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.interfaces';
|
||||
|
||||
/**
|
||||
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
||||
@@ -12,7 +12,7 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||
})
|
||||
export class MetadataValuesComponent {
|
||||
|
||||
@Input() values: Metadatum[];
|
||||
@Input() mdValues: MetadataValue[];
|
||||
|
||||
@Input() separator: string;
|
||||
|
||||
|
@@ -17,7 +17,7 @@
|
||||
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
||||
<dd class="col-md-8">{{file.findMetadata("dc.description")}}</dd>
|
||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
|
@@ -9,11 +9,13 @@
|
||||
</div>
|
||||
<table class="table table-responsive table-striped">
|
||||
<tbody>
|
||||
<tr *ngFor="let metadatum of (metadata$ | async)">
|
||||
<td>{{metadatum.key}}</td>
|
||||
<td>{{metadatum.value}}</td>
|
||||
<td>{{metadatum.language}}</td>
|
||||
</tr>
|
||||
<ng-container *ngFor="let mdEntry of (metadata$ | async) | keyvalue">
|
||||
<tr *ngFor="let mdValue of mdEntry.value">
|
||||
<td>{{mdEntry.key}}</td>
|
||||
<td>{{mdValue.value}}</td>
|
||||
<td>{{mdValue.language}}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
||||
|
@@ -6,7 +6,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ItemPageComponent } from '../simple/item-page.component';
|
||||
import { Metadatum } from '../../core/shared/metadatum.model';
|
||||
import { MetadataMap } from '../../core/shared/metadata.interfaces';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
@@ -34,7 +34,7 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
||||
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
metadata$: Observable<Metadatum[]>;
|
||||
metadata$: Observable<MetadataMap>;
|
||||
|
||||
constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) {
|
||||
super(route, items, metadataService);
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<div class="item-page-specific-field">
|
||||
<ds-metadata-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values>
|
||||
<ds-metadata-values [mdValues]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values>
|
||||
</div>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<h2 class="item-page-title-field">
|
||||
<ds-metadata-values [values]="item?.filterMetadata(fields)"></ds-metadata-values>
|
||||
<ds-metadata-values [mdValues]="item?.allMetadata(fields)"></ds-metadata-values>
|
||||
</h2>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<div class="item-page-specific-field">
|
||||
<ds-metadata-uri-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values>
|
||||
<ds-metadata-uri-values [mdValues]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
import { Metadatum } from '../core/shared/metadatum.model';
|
||||
import { MetadataMap } from '../core/shared/metadata.interfaces';
|
||||
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
||||
|
||||
/**
|
||||
@@ -16,6 +16,6 @@ export class NormalizedSearchResult implements ListableObject {
|
||||
* The metadata that was used to find this item, hithighlighted
|
||||
*/
|
||||
@autoserialize
|
||||
hitHighlights: Metadatum[];
|
||||
hitHighlights: MetadataMap;
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { Metadatum } from '../core/shared/metadatum.model';
|
||||
import { MetadataMap } from '../core/shared/metadata.interfaces';
|
||||
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
||||
|
||||
/**
|
||||
@@ -14,6 +14,6 @@ export class SearchResult<T extends DSpaceObject> implements ListableObject {
|
||||
/**
|
||||
* The metadata that was used to find this item, hithighlighted
|
||||
*/
|
||||
hitHighlights: Metadatum[];
|
||||
hitHighlights: MetadataMap;
|
||||
|
||||
}
|
||||
|
@@ -111,33 +111,38 @@ export const objects = [
|
||||
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||
type: ResourceType.Community,
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description',
|
||||
language: null,
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: null,
|
||||
value: 'This is a test community to hold content for the OR2017 demostration'
|
||||
},
|
||||
{
|
||||
key: 'dc.description.tableofcontents',
|
||||
language: null,
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: 'dc.rights',
|
||||
language: null,
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: null,
|
||||
value: 'OR2017 - Demonstration'
|
||||
}
|
||||
]
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
language: null,
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
language: null,
|
||||
value: 'This is a test community to hold content for the OR2017 demostration'
|
||||
}
|
||||
],
|
||||
'dc.description.tableofcontents': [
|
||||
{
|
||||
language: null,
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
'dc.rights': [
|
||||
{
|
||||
language: null,
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'OR2017 - Demonstration'
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
Object.assign(new Community(),
|
||||
{
|
||||
@@ -160,33 +165,38 @@ export const objects = [
|
||||
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||
type: ResourceType.Community,
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description',
|
||||
language: null,
|
||||
value: '<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\'http://www.duraspace.org/\'>DuraSpace</a> logo).</p>'
|
||||
},
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: null,
|
||||
value: 'This is a sample top-level community'
|
||||
},
|
||||
{
|
||||
key: 'dc.description.tableofcontents',
|
||||
language: null,
|
||||
value: '<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>'
|
||||
},
|
||||
{
|
||||
key: 'dc.rights',
|
||||
language: null,
|
||||
value: '<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>'
|
||||
},
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: null,
|
||||
value: 'Sample Community'
|
||||
}
|
||||
]
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
language: null,
|
||||
value: '<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\'http://www.duraspace.org/\'>DuraSpace</a> logo).</p>'
|
||||
}
|
||||
],
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
language: null,
|
||||
value: 'This is a sample top-level community'
|
||||
}
|
||||
],
|
||||
'dc.description.tableofcontents': [
|
||||
{
|
||||
language: null,
|
||||
value: '<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>'
|
||||
}
|
||||
],
|
||||
'dc.rights': [
|
||||
{
|
||||
language: null,
|
||||
value: '<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>'
|
||||
}
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'Sample Community'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
];
|
||||
|
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||
|
||||
const ITEM_MODULE_PATH = 'items';
|
||||
export function getItemModulePath() {
|
||||
@@ -17,7 +18,7 @@ export function getItemModulePath() {
|
||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
||||
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
|
||||
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||
|
@@ -15,6 +15,10 @@ import {
|
||||
NotificationsState
|
||||
} from './shared/notifications/notifications.reducers';
|
||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||
import {
|
||||
metadataRegistryReducer,
|
||||
MetadataRegistryState
|
||||
} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
||||
import { hasValue } from './shared/empty.util';
|
||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
||||
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
||||
@@ -23,6 +27,7 @@ export interface AppState {
|
||||
router: fromRouter.RouterReducerState;
|
||||
hostWindow: HostWindowState;
|
||||
forms: FormState;
|
||||
metadataRegistry: MetadataRegistryState;
|
||||
notifications: NotificationsState;
|
||||
searchSidebar: SearchSidebarState;
|
||||
searchFilter: SearchFiltersState;
|
||||
@@ -35,6 +40,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
||||
router: fromRouter.routerReducer,
|
||||
hostWindow: hostWindowReducer,
|
||||
forms: formReducer,
|
||||
metadataRegistry: metadataRegistryReducer,
|
||||
notifications: notificationsReducer,
|
||||
searchSidebar: sidebarReducer,
|
||||
searchFilter: filterReducer,
|
||||
|
@@ -60,23 +60,26 @@ describe('AuthResponseParsingService', () => {
|
||||
handle: null,
|
||||
id: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
|
||||
lastActive: '2018-05-14T17:03:31.277+0000',
|
||||
metadata: [
|
||||
{
|
||||
key: 'eperson.firstname',
|
||||
language: null,
|
||||
value: 'User'
|
||||
},
|
||||
{
|
||||
key: 'eperson.lastname',
|
||||
language: null,
|
||||
value: 'Test'
|
||||
},
|
||||
{
|
||||
key: 'eperson.language',
|
||||
language: null,
|
||||
value: 'en'
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
language: null,
|
||||
value: 'User'
|
||||
}
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
language: null,
|
||||
value: 'Test'
|
||||
}
|
||||
],
|
||||
'eperson.language': [
|
||||
{
|
||||
language: null,
|
||||
value: 'en'
|
||||
}
|
||||
]
|
||||
},
|
||||
name: 'User Test',
|
||||
netid: 'myself@testshib.org',
|
||||
requireCertificate: false,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import {Observable, of, of as observableOf} from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
|
@@ -3,7 +3,7 @@ import {take} from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
// reducers
|
||||
|
@@ -220,44 +220,44 @@ describe('BrowseService', () => {
|
||||
}}));
|
||||
});
|
||||
|
||||
it('should return the URL for the given metadatumKey and linkPath', () => {
|
||||
const metadatumKey = 'dc.date.issued';
|
||||
it('should return the URL for the given metadataKey and linkPath', () => {
|
||||
const metadataKey = 'dc.date.issued';
|
||||
const linkPath = 'items';
|
||||
const expectedURL = browseDefinitions[0]._links[linkPath];
|
||||
|
||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should work when the definition uses a wildcard in the metadatumKey', () => {
|
||||
const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
|
||||
it('should work when the definition uses a wildcard in the metadataKey', () => {
|
||||
const metadataKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
|
||||
const linkPath = 'items';
|
||||
const expectedURL = browseDefinitions[1]._links[linkPath];
|
||||
|
||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should throw an error when the key doesn\'t match', () => {
|
||||
const metadatumKey = 'dc.title'; // isn't in the definitions
|
||||
const metadataKey = 'dc.title'; // isn't in the definitions
|
||||
const linkPath = 'items';
|
||||
|
||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
|
||||
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`));
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should throw an error when the link doesn\'t match', () => {
|
||||
const metadatumKey = 'dc.date.issued';
|
||||
const metadataKey = 'dc.date.issued';
|
||||
const linkPath = 'collections'; // isn't in the definitions
|
||||
|
||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
|
||||
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`));
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
@@ -272,10 +272,10 @@ describe('BrowseService', () => {
|
||||
spyOn(service, 'getBrowseDefinitions').and
|
||||
.returnValue(hot('----'));
|
||||
|
||||
const metadatumKey = 'dc.date.issued';
|
||||
const metadataKey = 'dc.date.issued';
|
||||
const linkPath = 'items';
|
||||
|
||||
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||
const result = service.getBrowseURLFor(metadataKey, linkPath);
|
||||
const expected = cold('b---', { b: undefined });
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
@@ -41,8 +41,8 @@ import { RequestEntry } from '../data/request.reducer';
|
||||
export class BrowseService {
|
||||
protected linkPath = 'browses';
|
||||
|
||||
private static toSearchKeyArray(metadatumKey: string): string[] {
|
||||
const keyParts = metadatumKey.split('.');
|
||||
private static toSearchKeyArray(metadataKey: string): string[] {
|
||||
const keyParts = metadataKey.split('.');
|
||||
const searchFor = [];
|
||||
searchFor.push('*');
|
||||
for (let i = 0; i < keyParts.length - 1; i++) {
|
||||
@@ -50,7 +50,7 @@ export class BrowseService {
|
||||
const nextPart = [...prevParts, '*'].join('.');
|
||||
searchFor.push(nextPart);
|
||||
}
|
||||
searchFor.push(metadatumKey);
|
||||
searchFor.push(metadataKey);
|
||||
return searchFor;
|
||||
}
|
||||
|
||||
@@ -233,8 +233,8 @@ export class BrowseService {
|
||||
* @param metadatumKey
|
||||
* @param linkPath
|
||||
*/
|
||||
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
||||
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
||||
getBrowseURLFor(metadataKey: string, linkPath: string): Observable<string> {
|
||||
const searchKeyArray = BrowseService.toSearchKeyArray(metadataKey);
|
||||
return this.getBrowseDefinitions().pipe(
|
||||
getRemoteDataPayload(),
|
||||
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||
@@ -245,7 +245,7 @@ export class BrowseService {
|
||||
),
|
||||
map((def: BrowseDefinition) => {
|
||||
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
|
||||
throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`);
|
||||
throw new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`);
|
||||
} else {
|
||||
return def._links[linkPath];
|
||||
}
|
||||
|
@@ -8,20 +8,24 @@ import { of as observableOf } from 'rxjs';
|
||||
const pageInfo = new PageInfo();
|
||||
const array = [
|
||||
Object.assign(new Item(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Item nr 1'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Item nr 1'
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
Object.assign(new Item(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Item nr 2'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Item nr 2'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
];
|
||||
const paginatedList = new PaginatedList(pageInfo, array);
|
||||
|
@@ -91,8 +91,8 @@ export class RemoteDataBuildService {
|
||||
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, payload$: Observable<T>) {
|
||||
return observableCombineLatest(requestEntry$, payload$).pipe(
|
||||
map(([reqEntry, payload]) => {
|
||||
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
||||
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
||||
const requestPending = hasValue(reqEntry) && hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
||||
const responsePending = hasValue(reqEntry) && hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
||||
let isSuccessful: boolean;
|
||||
let error: RemoteDataError;
|
||||
if (hasValue(reqEntry) && hasValue(reqEntry.response)) {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize';
|
||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||
|
||||
import { Metadatum } from '../../shared/metadatum.model';
|
||||
import { MetadataMap } from '../../shared/metadata.interfaces';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { mapsTo } from '../builders/build-decorators';
|
||||
import { NormalizedObject } from './normalized-object.model';
|
||||
@@ -46,10 +45,10 @@ export class NormalizedDSpaceObject extends NormalizedObject {
|
||||
type: ResourceType;
|
||||
|
||||
/**
|
||||
* An array containing all metadata of this DSpaceObject
|
||||
* All metadata of this DSpaceObject
|
||||
*/
|
||||
@autoserializeAs(Metadatum)
|
||||
metadata: Metadatum[];
|
||||
@autoserialize
|
||||
metadata: MetadataMap;
|
||||
|
||||
/**
|
||||
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
||||
|
@@ -10,6 +10,7 @@ import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
|
||||
import { NormalizedResourcePolicy } from './normalized-resource-policy.model';
|
||||
import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model';
|
||||
import { NormalizedGroup } from '../../eperson/models/normalized-group.model';
|
||||
import { NormalizedMetadataSchema } from '../../metadata/normalized-metadata-schema.model';
|
||||
|
||||
export class NormalizedObjectFactory {
|
||||
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
|
||||
@@ -41,6 +42,12 @@ export class NormalizedObjectFactory {
|
||||
case ResourceType.Group: {
|
||||
return NormalizedGroup
|
||||
}
|
||||
case ResourceType.MetadataSchema: {
|
||||
return NormalizedMetadataSchema
|
||||
}
|
||||
case ResourceType.MetadataField: {
|
||||
return NormalizedGroup
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
|
28
src/app/core/cache/response.models.ts
vendored
28
src/app/core/cache/response.models.ts
vendored
@@ -6,11 +6,11 @@ import { FacetValue } from '../../+search-page/search-service/facet-value.model'
|
||||
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
|
||||
import { IntegrationModel } from '../integration/models/integration.model';
|
||||
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
|
||||
import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
||||
import { AuthStatus } from '../auth/models/auth-status.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||
import { MetadataField } from '../metadata/metadatafield.model';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class RestResponse {
|
||||
@@ -33,6 +33,9 @@ export class DSOSuccessResponse extends RestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A successful response containing a list of MetadataSchemas wrapped in a RegistryMetadataschemasResponse
|
||||
*/
|
||||
export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadataschemasResponse: RegistryMetadataschemasResponse,
|
||||
@@ -43,6 +46,9 @@ export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A successful response containing a list of MetadataFields wrapped in a RegistryMetadatafieldsResponse
|
||||
*/
|
||||
export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadatafieldsResponse: RegistryMetadatafieldsResponse,
|
||||
@@ -53,6 +59,9 @@ export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A successful response containing a list of BitstreamFormats wrapped in a RegistryBitstreamformatsResponse
|
||||
*/
|
||||
export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public bitstreamformatsResponse: RegistryBitstreamformatsResponse,
|
||||
@@ -63,6 +72,9 @@ export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A successful response containing exactly one MetadataSchema
|
||||
*/
|
||||
export class MetadataschemaSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadataschema: MetadataSchema,
|
||||
@@ -72,6 +84,18 @@ export class MetadataschemaSuccessResponse extends RestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A successful response containing exactly one MetadataField
|
||||
*/
|
||||
export class MetadatafieldSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadatafield: MetadataField,
|
||||
public statusCode: string
|
||||
) {
|
||||
super(true, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public results: SearchQueryResponse,
|
||||
|
@@ -56,13 +56,13 @@ import { FacetValueMapResponseParsingService } from './data/facet-value-map-resp
|
||||
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
|
||||
import { RegistryService } from './registry/registry.service';
|
||||
import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service';
|
||||
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
|
||||
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
|
||||
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
|
||||
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
||||
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
|
||||
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service';
|
||||
@@ -113,7 +113,6 @@ const PROVIDERS = [
|
||||
RegistryMetadataschemasResponseParsingService,
|
||||
RegistryMetadatafieldsResponseParsingService,
|
||||
RegistryBitstreamformatsResponseParsingService,
|
||||
MetadataschemaParsingService,
|
||||
DebugResponseParsingService,
|
||||
SearchResponseParsingService,
|
||||
ServerResponseService,
|
||||
@@ -128,6 +127,7 @@ const PROVIDERS = [
|
||||
SubmissionSectionsConfigService,
|
||||
AuthorityService,
|
||||
IntegrationResponseParsingService,
|
||||
MetadataschemaParsingService,
|
||||
UploaderService,
|
||||
UUIDService,
|
||||
DSpaceObjectDataService,
|
||||
|
@@ -24,13 +24,14 @@ describe('BrowseItemsResponseParsingService', () => {
|
||||
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
||||
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
|
||||
handle: '10986/17472',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.creator',
|
||||
value: 'World Bank',
|
||||
language: null
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
'dc.creator': [
|
||||
{
|
||||
value: 'World Bank',
|
||||
language: null
|
||||
}
|
||||
]
|
||||
},
|
||||
inArchive: true,
|
||||
discoverable: true,
|
||||
withdrawn: false,
|
||||
@@ -56,13 +57,14 @@ describe('BrowseItemsResponseParsingService', () => {
|
||||
uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
|
||||
name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India',
|
||||
handle: '10986/17475',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.creator',
|
||||
value: 'World Bank',
|
||||
language: null
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
'dc.creator': [
|
||||
{
|
||||
value: 'World Bank',
|
||||
language: null
|
||||
}
|
||||
]
|
||||
},
|
||||
inArchive: true,
|
||||
discoverable: true,
|
||||
withdrawn: false,
|
||||
@@ -115,13 +117,14 @@ describe('BrowseItemsResponseParsingService', () => {
|
||||
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
|
||||
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
|
||||
handle: '10986/17472',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.creator',
|
||||
value: 'World Bank',
|
||||
language: null
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
'dc.creator': [
|
||||
{
|
||||
value: 'World Bank',
|
||||
language: null
|
||||
}
|
||||
]
|
||||
},
|
||||
inArchive: true,
|
||||
discoverable: true,
|
||||
withdrawn: false,
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
import {distinctUntilChanged, map, filter} from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -14,14 +13,13 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { DataService } from './data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { FindAllOptions, PatchRequest, RestRequest } from './request.models';
|
||||
import { DeleteRequest, FindAllOptions, PatchRequest, RestRequest } from './request.models';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { configureRequest, getResponseFromEntry } from '../shared/operators';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
import { configureRequest, getRequestFromRequestHref } from '../shared/operators';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Injectable()
|
||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||
@@ -93,9 +91,7 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
||||
),
|
||||
configureRequest(this.requestService),
|
||||
map((request: RestRequest) => request.href),
|
||||
getRequestFromRequestHref(this.requestService),
|
||||
map((requestEntry: RequestEntry) => requestEntry.response)
|
||||
getResponseFromEntry()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,9 +110,7 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
||||
),
|
||||
configureRequest(this.requestService),
|
||||
map((request: RestRequest) => request.href),
|
||||
getRequestFromRequestHref(this.requestService),
|
||||
map((requestEntry: RequestEntry) => requestEntry.response)
|
||||
getResponseFromEntry()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
51
src/app/core/data/metadata-schema-data.service.ts
Normal file
51
src/app/core/data/metadata-schema-data.service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BrowseService } from '../browse/browse.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
|
||||
import { DataService } from './data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { FindAllOptions, GetRequest, RestRequest } from './request.models';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||
import { NormalizedMetadataSchema } from '../metadata/normalized-metadata-schema.model';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ChangeAnalyzer } from './change-analyzer';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
|
||||
/**
|
||||
* A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint
|
||||
*/
|
||||
@Injectable()
|
||||
export class MetadataSchemaDataService extends DataService<NormalizedMetadataSchema, MetadataSchema> {
|
||||
protected linkPath = 'metadataschemas';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
private bs: BrowseService,
|
||||
protected halService: HALEndpointService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected dataBuildService: NormalizedObjectBuildService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: ChangeAnalyzer<NormalizedMetadataSchema>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing metadataschemas
|
||||
* @param {FindAllOptions} options
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
22
src/app/core/data/metadatafield-parsing.service.ts
Normal file
22
src/app/core/data/metadatafield-parsing.service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { RestRequest } from './request.models';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MetadatafieldSuccessResponse, MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { MetadataField } from '../metadata/metadatafield.model';
|
||||
|
||||
/**
|
||||
* A service responsible for parsing DSpaceRESTV2Response data related to a single MetadataField to a valid RestResponse
|
||||
*/
|
||||
@Injectable()
|
||||
export class MetadatafieldParsingService implements ResponseParsingService {
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload;
|
||||
|
||||
const deserialized = new DSpaceRESTv2Serializer(MetadataField).deserialize(payload);
|
||||
return new MetadatafieldSuccessResponse(deserialized, data.statusCode);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import {
|
||||
RegistryBitstreamformatsSuccessResponse
|
||||
} from '../cache/response.models';
|
||||
import { RegistryBitstreamformatsResponseParsingService } from './registry-bitstreamformats-response-parsing.service';
|
||||
|
||||
describe('RegistryBitstreamformatsResponseParsingService', () => {
|
||||
let service: RegistryBitstreamformatsResponseParsingService;
|
||||
|
||||
const mockDSOParser = Object.assign({
|
||||
processPageInfo: () => new PageInfo()
|
||||
}) as DSOResponseParsingService;
|
||||
|
||||
const data = Object.assign({
|
||||
payload: {
|
||||
_embedded: {
|
||||
bitstreamformats: [
|
||||
{
|
||||
uuid: 'uuid-1',
|
||||
description: 'a description'
|
||||
},
|
||||
{
|
||||
uuid: 'uuid-2',
|
||||
description: 'another description'
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RegistryBitstreamformatsResponseParsingService(mockDSOParser);
|
||||
});
|
||||
|
||||
it('should parse the data correctly', () => {
|
||||
const response = service.parse(null, data);
|
||||
expect(response.constructor).toBe(RegistryBitstreamformatsSuccessResponse);
|
||||
});
|
||||
});
|
@@ -0,0 +1,68 @@
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import {
|
||||
RegistryMetadatafieldsSuccessResponse
|
||||
} from '../cache/response.models';
|
||||
import { RegistryMetadatafieldsResponseParsingService } from './registry-metadatafields-response-parsing.service';
|
||||
|
||||
describe('RegistryMetadatafieldsResponseParsingService', () => {
|
||||
let service: RegistryMetadatafieldsResponseParsingService;
|
||||
|
||||
const mockDSOParser = Object.assign({
|
||||
processPageInfo: () => new PageInfo()
|
||||
}) as DSOResponseParsingService;
|
||||
|
||||
const data = Object.assign({
|
||||
payload: {
|
||||
_embedded: {
|
||||
metadatafields: [
|
||||
{
|
||||
id: 1,
|
||||
element: 'element',
|
||||
qualifier: 'qualifier',
|
||||
scopeNote: 'a scope note',
|
||||
_embedded: {
|
||||
schema: {
|
||||
id: 1,
|
||||
prefix: 'test',
|
||||
namespace: 'test namespace'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
element: 'secondelement',
|
||||
qualifier: 'secondqualifier',
|
||||
scopeNote: 'a second scope note',
|
||||
_embedded: {
|
||||
schema: {
|
||||
id: 1,
|
||||
prefix: 'test',
|
||||
namespace: 'test namespace'
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
const emptyData = Object.assign({
|
||||
payload: {}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RegistryMetadatafieldsResponseParsingService(mockDSOParser);
|
||||
});
|
||||
|
||||
it('should parse the data correctly', () => {
|
||||
const response = service.parse(null, data);
|
||||
expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse);
|
||||
});
|
||||
|
||||
it('should not produce an error and parse the data correctly when the data is empty', () => {
|
||||
const response = service.parse(null, emptyData);
|
||||
expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse);
|
||||
});
|
||||
});
|
@@ -9,6 +9,7 @@ import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.seriali
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
|
||||
@Injectable()
|
||||
export class RegistryMetadatafieldsResponseParsingService implements ResponseParsingService {
|
||||
@@ -18,15 +19,19 @@ export class RegistryMetadatafieldsResponseParsingService implements ResponsePar
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload;
|
||||
|
||||
const metadatafields = payload._embedded.metadatafields;
|
||||
metadatafields.forEach((field) => {
|
||||
field.schema = field._embedded.schema;
|
||||
});
|
||||
let metadatafields = [];
|
||||
|
||||
if (hasValue(payload._embedded)) {
|
||||
metadatafields = payload._embedded.metadatafields;
|
||||
metadatafields.forEach((field) => {
|
||||
field.schema = field._embedded.schema;
|
||||
});
|
||||
}
|
||||
|
||||
payload.metadatafields = metadatafields;
|
||||
|
||||
const deserialized = new DSpaceRESTv2Serializer(RegistryMetadatafieldsResponse).deserialize(payload);
|
||||
return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload.page));
|
||||
return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,50 @@
|
||||
import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { RegistryMetadataschemasSuccessResponse } from '../cache/response.models';
|
||||
|
||||
describe('RegistryMetadataschemasResponseParsingService', () => {
|
||||
let service: RegistryMetadataschemasResponseParsingService;
|
||||
|
||||
const mockDSOParser = Object.assign({
|
||||
processPageInfo: () => new PageInfo()
|
||||
}) as DSOResponseParsingService;
|
||||
|
||||
const data = Object.assign({
|
||||
payload: {
|
||||
_embedded: {
|
||||
metadataschemas: [
|
||||
{
|
||||
id: 1,
|
||||
prefix: 'test',
|
||||
namespace: 'test namespace'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
prefix: 'second',
|
||||
namespace: 'second test namespace'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
const emptyData = Object.assign({
|
||||
payload: {}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RegistryMetadataschemasResponseParsingService(mockDSOParser);
|
||||
});
|
||||
|
||||
it('should parse the data correctly', () => {
|
||||
const response = service.parse(null, data);
|
||||
expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse);
|
||||
});
|
||||
|
||||
it('should not produce an error and parse the data correctly when the data is empty', () => {
|
||||
const response = service.parse(null, emptyData);
|
||||
expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse);
|
||||
});
|
||||
});
|
@@ -6,6 +6,7 @@ import { RegistryMetadataschemasResponse } from '../registry/registry-metadatasc
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
|
||||
@Injectable()
|
||||
export class RegistryMetadataschemasResponseParsingService implements ResponseParsingService {
|
||||
@@ -15,11 +16,14 @@ export class RegistryMetadataschemasResponseParsingService implements ResponsePa
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload;
|
||||
|
||||
const metadataschemas = payload._embedded.metadataschemas;
|
||||
let metadataschemas = [];
|
||||
if (hasValue(payload._embedded)) {
|
||||
metadataschemas = payload._embedded.metadataschemas;
|
||||
}
|
||||
payload.metadataschemas = metadataschemas;
|
||||
|
||||
const deserialized = new DSpaceRESTv2Serializer(RegistryMetadataschemasResponse).deserialize(payload);
|
||||
return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload.page));
|
||||
return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -10,7 +10,8 @@ export const RequestActionTypes = {
|
||||
CONFIGURE: type('dspace/core/data/request/CONFIGURE'),
|
||||
EXECUTE: type('dspace/core/data/request/EXECUTE'),
|
||||
COMPLETE: type('dspace/core/data/request/COMPLETE'),
|
||||
RESET_TIMESTAMPS: type('dspace/core/data/request/RESET_TIMESTAMPS')
|
||||
RESET_TIMESTAMPS: type('dspace/core/data/request/RESET_TIMESTAMPS'),
|
||||
REMOVE: type('dspace/core/data/request/REMOVE')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@@ -82,6 +83,24 @@ export class ResetResponseTimestampsAction implements Action {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to remove a cached request
|
||||
*/
|
||||
export class RequestRemoveAction implements Action {
|
||||
type = RequestActionTypes.REMOVE;
|
||||
uuid: string;
|
||||
|
||||
/**
|
||||
* Create a new RequestRemoveAction
|
||||
*
|
||||
* @param uuid
|
||||
* the request's uuid
|
||||
*/
|
||||
constructor(uuid: string) {
|
||||
this.uuid = uuid
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
@@ -91,4 +110,5 @@ export type RequestAction
|
||||
= RequestConfigureAction
|
||||
| RequestExecuteAction
|
||||
| RequestCompleteAction
|
||||
| ResetResponseTimestampsAction;
|
||||
| ResetResponseTimestampsAction
|
||||
| RequestRemoveAction;
|
||||
|
@@ -11,6 +11,9 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
|
||||
import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service';
|
||||
import { MetadataschemaParsingService } from './metadataschema-parsing.service';
|
||||
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
@@ -217,6 +220,58 @@ export class IntegrationRequest extends GetRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to create a MetadataSchema
|
||||
*/
|
||||
export class CreateMetadataSchemaRequest extends PostRequest {
|
||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||
super(uuid, href, body, options);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return MetadataschemaParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to update a MetadataSchema
|
||||
*/
|
||||
export class UpdateMetadataSchemaRequest extends PutRequest {
|
||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||
super(uuid, href, body, options);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return MetadataschemaParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to create a MetadataField
|
||||
*/
|
||||
export class CreateMetadataFieldRequest extends PostRequest {
|
||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||
super(uuid, href, body, options);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return MetadatafieldParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to update a MetadataField
|
||||
*/
|
||||
export class UpdateMetadataFieldRequest extends PutRequest {
|
||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||
super(uuid, href, body, options);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return MetadatafieldParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateRequest extends PostRequest {
|
||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||
super(uuid, href, body, options);
|
||||
|
@@ -4,7 +4,7 @@ import { requestReducer, RequestState } from './request.reducer';
|
||||
import {
|
||||
RequestCompleteAction,
|
||||
RequestConfigureAction,
|
||||
RequestExecuteAction, ResetResponseTimestampsAction
|
||||
RequestExecuteAction, RequestRemoveAction, ResetResponseTimestampsAction
|
||||
} from './request.actions';
|
||||
import { GetRequest } from './request.models';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
@@ -110,4 +110,13 @@ describe('requestReducer', () => {
|
||||
expect(newState[id1].response.statusCode).toEqual(response.statusCode);
|
||||
expect(newState[id1].response.timeAdded).toBe(timeStamp);
|
||||
});
|
||||
|
||||
it('should remove the correct request, in response to a REMOVE action', () => {
|
||||
const state = testState;
|
||||
|
||||
const action = new RequestRemoveAction(id1);
|
||||
const newState = requestReducer(state, action);
|
||||
|
||||
expect(newState[id1]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
RequestActionTypes, RequestAction, RequestConfigureAction,
|
||||
RequestExecuteAction, RequestCompleteAction, ResetResponseTimestampsAction
|
||||
RequestExecuteAction, RequestCompleteAction, ResetResponseTimestampsAction, RequestRemoveAction
|
||||
} from './request.actions';
|
||||
import { RestRequest } from './request.models';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
@@ -38,6 +38,10 @@ export function requestReducer(state = initialState, action: RequestAction): Req
|
||||
return resetResponseTimestamps(state, action as ResetResponseTimestampsAction);
|
||||
}
|
||||
|
||||
case RequestActionTypes.REMOVE: {
|
||||
return removeRequest(state, action as RequestRemoveAction);
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
@@ -105,3 +109,18 @@ function resetResponseTimestamps(state: RequestState, action: ResetResponseTimes
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a request from the RequestState
|
||||
* @param state The current RequestState
|
||||
* @param action The RequestRemoveAction to perform
|
||||
*/
|
||||
function removeRequest(state: RequestState, action: RequestRemoveAction): RequestState {
|
||||
const newState = Object.create(null);
|
||||
for (const value in state) {
|
||||
if (value !== action.uuid) {
|
||||
newState[value] = state[value];
|
||||
}
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
@@ -21,6 +21,8 @@ import { RequestService } from './request.service';
|
||||
import { ActionsSubject, Store } from '@ngrx/store';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { MockStore } from '../../shared/testing/mock-store';
|
||||
import { IndexState } from '../index/index.reducer';
|
||||
|
||||
describe('RequestService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
@@ -59,7 +61,8 @@ describe('RequestService', () => {
|
||||
service = new RequestService(
|
||||
objectCache,
|
||||
uuidService,
|
||||
store
|
||||
store,
|
||||
undefined
|
||||
);
|
||||
serviceAsAny = service as any;
|
||||
});
|
||||
|
@@ -15,23 +15,23 @@ import {
|
||||
import { race as observableRace } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { hasNoValue, hasValue, isNotUndefined } from '../../shared/empty.util';
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { hasNoValue, hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { coreSelector, CoreState } from '../core.reducers';
|
||||
import { IndexName } from '../index/index.reducer';
|
||||
import { IndexName, IndexState } from '../index/index.reducer';
|
||||
import { pathSelector } from '../shared/selectors';
|
||||
import { UUIDService } from '../shared/uuid.service';
|
||||
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
|
||||
import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions';
|
||||
import { GetRequest, RestRequest } from './request.models';
|
||||
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
import { AddToIndexAction } from '../index/index.actions';
|
||||
import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions';
|
||||
|
||||
@Injectable()
|
||||
export class RequestService {
|
||||
@@ -39,7 +39,8 @@ export class RequestService {
|
||||
|
||||
constructor(private objectCache: ObjectCacheService,
|
||||
private uuidService: UUIDService,
|
||||
private store: Store<CoreState>) {
|
||||
private store: Store<CoreState>,
|
||||
private indexStore: Store<IndexState>) {
|
||||
}
|
||||
|
||||
private entryFromUUIDSelector(uuid: string): MemoizedSelector<CoreState, RequestEntry> {
|
||||
@@ -54,6 +55,38 @@ export class RequestService {
|
||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.UUID_MAPPING, uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
|
||||
* contains a given substring
|
||||
* @param selector MemoizedSelector to start from
|
||||
* @param name The name of the index substate we're fetching request UUIDs from
|
||||
* @param href Substring that the request's href should contain
|
||||
*/
|
||||
private uuidsFromHrefSubstringSelector(selector: MemoizedSelector<any, IndexState>, name: string, href: string): MemoizedSelector<any, string[]> {
|
||||
return createSelector(selector, (state: IndexState) => this.getUuidsFromHrefSubstring(state, name, href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a list of request UUIDs from a given index substate of which the request href contains a given substring
|
||||
* @param state The IndexState
|
||||
* @param name The name of the index substate we're fetching request UUIDs from
|
||||
* @param href Substring that the request's href should contain
|
||||
*/
|
||||
private getUuidsFromHrefSubstring(state: IndexState, name: string, href: string): string[] {
|
||||
let result = [];
|
||||
if (isNotEmpty(state)) {
|
||||
const subState = state[name];
|
||||
if (isNotEmpty(subState)) {
|
||||
for (const value in subState) {
|
||||
if (value.indexOf(href) > -1) {
|
||||
result = [...result, subState[value]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
generateRequestId(): string {
|
||||
return `client/${this.uuidService.generate()}`;
|
||||
}
|
||||
@@ -119,6 +152,32 @@ export class RequestService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all request cache providing (part of) the href
|
||||
* This also includes href-to-uuid index cache
|
||||
* @param href A substring of the request(s) href
|
||||
*/
|
||||
removeByHrefSubstring(href: string) {
|
||||
this.store.pipe(
|
||||
select(this.uuidsFromHrefSubstringSelector(pathSelector<CoreState, IndexState>(coreSelector, 'index'), IndexName.REQUEST, href)),
|
||||
take(1)
|
||||
).subscribe((uuids: string[]) => {
|
||||
for (const uuid of uuids) {
|
||||
this.removeByUuid(uuid);
|
||||
}
|
||||
});
|
||||
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
|
||||
this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove request cache using the request's UUID
|
||||
* @param uuid
|
||||
*/
|
||||
removeByUuid(uuid: string) {
|
||||
this.store.dispatch(new RequestRemoveAction(uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a request is in the cache or if it's still pending
|
||||
* @param {GetRequest} request The request to check
|
||||
|
@@ -7,7 +7,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
||||
import { Metadatum } from '../shared/metadatum.model';
|
||||
import { MetadataMap, MetadataValue } from '../shared/metadata.interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class SearchResponseParsingService implements ResponseParsingService {
|
||||
@@ -16,17 +16,17 @@ export class SearchResponseParsingService implements ResponseParsingService {
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload._embedded.searchResult;
|
||||
const hitHighlights = payload._embedded.objects
|
||||
const hitHighlights: MetadataMap[] = payload._embedded.objects
|
||||
.map((object) => object.hitHighlights)
|
||||
.map((hhObject) => {
|
||||
const mdMap: MetadataMap = {};
|
||||
if (hhObject) {
|
||||
return Object.keys(hhObject).map((key) => Object.assign(new Metadatum(), {
|
||||
key: key,
|
||||
value: hhObject[key].join('...')
|
||||
}))
|
||||
} else {
|
||||
return [];
|
||||
for (const key of Object.keys(hhObject)) {
|
||||
const value: MetadataValue = { value: hhObject[key].join('...'), language: null };
|
||||
mdMap[key] = [ value ];
|
||||
}
|
||||
}
|
||||
return mdMap;
|
||||
});
|
||||
|
||||
const dsoSelfLinks = payload._embedded.objects
|
||||
|
@@ -91,9 +91,9 @@ export class DSpaceRESTv2Service {
|
||||
const form: FormData = new FormData();
|
||||
form.append('name', dso.name);
|
||||
if (dso.metadata) {
|
||||
for (const i of Object.keys(dso.metadata)) {
|
||||
if (isNotEmpty(dso.metadata[i].value)) {
|
||||
form.append(dso.metadata[i].key, dso.metadata[i].value);
|
||||
for (const key of Object.keys(dso.metadata)) {
|
||||
for (const value of dso.allMetadataValues(key)) {
|
||||
form.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,8 @@ import { IndexName } from './index.reducer';
|
||||
*/
|
||||
export const IndexActionTypes = {
|
||||
ADD: type('dspace/core/index/ADD'),
|
||||
REMOVE_BY_VALUE: type('dspace/core/index/REMOVE_BY_VALUE')
|
||||
REMOVE_BY_VALUE: type('dspace/core/index/REMOVE_BY_VALUE'),
|
||||
REMOVE_BY_SUBSTRING: type('dspace/core/index/REMOVE_BY_SUBSTRING')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@@ -60,6 +61,30 @@ export class RemoveFromIndexByValueAction implements Action {
|
||||
this.payload = { name, value };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to remove multiple values from the index by substring
|
||||
*/
|
||||
export class RemoveFromIndexBySubstringAction implements Action {
|
||||
type = IndexActionTypes.REMOVE_BY_SUBSTRING;
|
||||
payload: {
|
||||
name: IndexName,
|
||||
value: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new RemoveFromIndexByValueAction
|
||||
*
|
||||
* @param name
|
||||
* the name of the index to remove from
|
||||
* @param value
|
||||
* the value to remove the UUID for
|
||||
*/
|
||||
constructor(name: IndexName, value: string) {
|
||||
this.payload = { name, value };
|
||||
}
|
||||
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import { IndexName, indexReducer, IndexState } from './index.reducer';
|
||||
import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions';
|
||||
import { AddToIndexAction, RemoveFromIndexBySubstringAction, RemoveFromIndexByValueAction } from './index.actions';
|
||||
|
||||
class NullAction extends AddToIndexAction {
|
||||
type = null;
|
||||
@@ -59,4 +59,13 @@ describe('requestReducer', () => {
|
||||
|
||||
expect(newState[IndexName.OBJECT][key1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove the given \'value\' from its corresponding \'key\' in the correct substate, in response to a REMOVE_BY_SUBSTRING action', () => {
|
||||
const state = testState;
|
||||
|
||||
const action = new RemoveFromIndexBySubstringAction(IndexName.OBJECT, key1);
|
||||
const newState = indexReducer(state, action);
|
||||
|
||||
expect(newState[IndexName.OBJECT][key1]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@@ -2,7 +2,7 @@ import {
|
||||
IndexAction,
|
||||
IndexActionTypes,
|
||||
AddToIndexAction,
|
||||
RemoveFromIndexByValueAction
|
||||
RemoveFromIndexByValueAction, RemoveFromIndexBySubstringAction
|
||||
} from './index.actions';
|
||||
|
||||
export enum IndexName {
|
||||
@@ -31,6 +31,10 @@ export function indexReducer(state = initialState, action: IndexAction): IndexSt
|
||||
return removeFromIndexByValue(state, action as RemoveFromIndexByValueAction)
|
||||
}
|
||||
|
||||
case IndexActionTypes.REMOVE_BY_SUBSTRING: {
|
||||
return removeFromIndexBySubstring(state, action as RemoveFromIndexBySubstringAction)
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
@@ -60,3 +64,21 @@ function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValu
|
||||
[action.payload.name]: newSubState
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove values from the IndexState's substate that contain a given substring
|
||||
* @param state The IndexState to remove values from
|
||||
* @param action The RemoveFromIndexByValueAction containing the necessary information to remove the values
|
||||
*/
|
||||
function removeFromIndexBySubstring(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
|
||||
const subState = state[action.payload.name];
|
||||
const newSubState = Object.create(null);
|
||||
for (const value in subState) {
|
||||
if (value.indexOf(action.payload.value) < 0) {
|
||||
newSubState[value] = subState[value];
|
||||
}
|
||||
}
|
||||
return Object.assign({}, state, {
|
||||
[action.payload.name]: newSubState
|
||||
});
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { EmptyError } from 'rxjs/internal-compatibility';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
import { MetadataValue } from '../shared/metadata.interfaces';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@Component({
|
||||
@@ -88,7 +89,7 @@ describe('MetadataService', () => {
|
||||
|
||||
objectCacheService = new ObjectCacheService(store);
|
||||
uuidService = new UUIDService();
|
||||
requestService = new RequestService(objectCacheService, uuidService, store);
|
||||
requestService = new RequestService(objectCacheService, uuidService, store, undefined);
|
||||
remoteDataBuildService = new RemoteDataBuildService(objectCacheService, requestService);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
@@ -152,7 +153,7 @@ describe('MetadataService', () => {
|
||||
expect(title.getTitle()).toEqual('Test PowerPoint Document');
|
||||
expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document');
|
||||
expect(tagStore.get('citation_author')[0].content).toEqual('Doe, Jane');
|
||||
expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26T19:58:25Z');
|
||||
expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26');
|
||||
expect(tagStore.get('citation_issn')[0].content).toEqual('123456789');
|
||||
expect(tagStore.get('citation_language')[0].content).toEqual('en');
|
||||
expect(tagStore.get('citation_keywords')[0].content).toEqual('keyword1; keyword2; keyword3');
|
||||
@@ -216,22 +217,18 @@ describe('MetadataService', () => {
|
||||
|
||||
const mockType = (mockItem: Item, type: string): Item => {
|
||||
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
for (const metadatum of typedMockItem.metadata) {
|
||||
if (metadatum.key === 'dc.type') {
|
||||
metadatum.value = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
typedMockItem.metadata['dc.type'] = [ { value: type } ] as MetadataValue[];
|
||||
return typedMockItem;
|
||||
}
|
||||
|
||||
const mockPublisher = (mockItem: Item): Item => {
|
||||
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
publishedMockItem.metadata.push({
|
||||
key: 'dc.publisher',
|
||||
language: 'en_US',
|
||||
value: 'Mock Publisher'
|
||||
});
|
||||
publishedMockItem.metadata['dc.publisher'] = [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Mock Publisher'
|
||||
}
|
||||
] as MetadataValue[];
|
||||
return publishedMockItem;
|
||||
}
|
||||
|
||||
|
@@ -294,6 +294,10 @@ export class MetadataService {
|
||||
}
|
||||
}
|
||||
|
||||
private hasType(value: string): boolean {
|
||||
return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this._item is a dissertation
|
||||
*
|
||||
@@ -301,14 +305,7 @@ export class MetadataService {
|
||||
* true if this._item has a dc.type equal to 'Thesis'
|
||||
*/
|
||||
private isDissertation(): boolean {
|
||||
let isDissertation = false;
|
||||
for (const metadatum of this.currentObject.value.metadata) {
|
||||
if (metadatum.key === 'dc.type') {
|
||||
isDissertation = metadatum.value.toLowerCase() === 'thesis';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isDissertation;
|
||||
return this.hasType('thesis');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,40 +315,15 @@ export class MetadataService {
|
||||
* true if this._item has a dc.type equal to 'Technical Report'
|
||||
*/
|
||||
private isTechReport(): boolean {
|
||||
let isTechReport = false;
|
||||
for (const metadatum of this.currentObject.value.metadata) {
|
||||
if (metadatum.key === 'dc.type') {
|
||||
isTechReport = metadatum.value.toLowerCase() === 'technical report';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isTechReport;
|
||||
return this.hasType('technical report');
|
||||
}
|
||||
|
||||
private getMetaTagValue(key: string): string {
|
||||
let value: string;
|
||||
for (const metadatum of this.currentObject.value.metadata) {
|
||||
if (metadatum.key === key) {
|
||||
value = metadatum.value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
return this.currentObject.value.firstMetadataValue(key);
|
||||
}
|
||||
|
||||
private getFirstMetaTagValue(keys: string[]): string {
|
||||
let value: string;
|
||||
for (const metadatum of this.currentObject.value.metadata) {
|
||||
for (const key of keys) {
|
||||
if (key === metadatum.key) {
|
||||
value = metadatum.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (value !== undefined) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
return this.currentObject.value.firstMetadataValue(keys);
|
||||
}
|
||||
|
||||
private getMetaTagValuesAndCombine(key: string): string {
|
||||
@@ -359,15 +331,7 @@ export class MetadataService {
|
||||
}
|
||||
|
||||
private getMetaTagValues(keys: string[]): string[] {
|
||||
const values: string[] = [];
|
||||
for (const metadatum of this.currentObject.value.metadata) {
|
||||
for (const key of keys) {
|
||||
if (key === metadatum.key) {
|
||||
values.push(metadatum.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
return this.currentObject.value.allMetadataValues(keys);
|
||||
}
|
||||
|
||||
private addMetaTag(property: string, content: string): void {
|
||||
|
@@ -3,6 +3,9 @@ import { autoserialize } from 'cerialize';
|
||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||
|
||||
export class MetadataField implements ListableObject {
|
||||
@autoserialize
|
||||
id: number;
|
||||
|
||||
@autoserialize
|
||||
self: string;
|
||||
|
||||
|
36
src/app/core/metadata/normalized-metadata-schema.model.ts
Normal file
36
src/app/core/metadata/normalized-metadata-schema.model.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { mapsTo } from '../cache/builders/build-decorators';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||
import { MetadataSchema } from './metadataschema.model';
|
||||
|
||||
/**
|
||||
* Normalized class for a DSpace MetadataSchema
|
||||
*/
|
||||
@mapsTo(MetadataSchema)
|
||||
export class NormalizedMetadataSchema extends NormalizedObject implements CacheableObject, ListableObject {
|
||||
/**
|
||||
* The unique identifier for this schema
|
||||
*/
|
||||
@autoserialize
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The REST link to itself
|
||||
*/
|
||||
@autoserialize
|
||||
self: string;
|
||||
|
||||
/**
|
||||
* A unique prefix that defines this schema
|
||||
*/
|
||||
@autoserialize
|
||||
prefix: string;
|
||||
|
||||
/**
|
||||
* The namespace for this schema
|
||||
*/
|
||||
@autoserialize
|
||||
namespace: string;
|
||||
}
|
@@ -14,13 +14,30 @@ import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import {
|
||||
RegistryBitstreamformatsSuccessResponse,
|
||||
RegistryMetadatafieldsSuccessResponse,
|
||||
RegistryMetadataschemasSuccessResponse
|
||||
RegistryMetadataschemasSuccessResponse, RestResponse
|
||||
} from '../cache/response.models';
|
||||
import { Component } from '@angular/core';
|
||||
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
||||
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
||||
import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { MockStore } from '../../shared/testing/mock-store';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
MetadataRegistryCancelFieldAction,
|
||||
MetadataRegistryCancelSchemaAction, MetadataRegistryDeselectAllFieldAction,
|
||||
MetadataRegistryDeselectAllSchemaAction, MetadataRegistryDeselectFieldAction,
|
||||
MetadataRegistryDeselectSchemaAction,
|
||||
MetadataRegistryEditFieldAction,
|
||||
MetadataRegistryEditSchemaAction, MetadataRegistrySelectFieldAction,
|
||||
MetadataRegistrySelectSchemaAction
|
||||
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
|
||||
import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||
import { MetadataField } from '../metadata/metadatafield.model';
|
||||
|
||||
@Component({ template: '' })
|
||||
class DummyComponent {
|
||||
@@ -49,31 +66,35 @@ describe('RegistryService', () => {
|
||||
];
|
||||
const mockFieldsList = [
|
||||
{
|
||||
id: 1,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8',
|
||||
element: 'contributor',
|
||||
qualifier: 'advisor',
|
||||
scopenote: null,
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9',
|
||||
element: 'contributor',
|
||||
qualifier: 'author',
|
||||
scopenote: null,
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10',
|
||||
element: 'contributor',
|
||||
qualifier: 'editor',
|
||||
scopenote: 'test scope note',
|
||||
scopeNote: 'test scope note',
|
||||
schema: mockSchemasList[1]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11',
|
||||
element: 'contributor',
|
||||
qualifier: 'illustrator',
|
||||
scopenote: null,
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[1]
|
||||
}
|
||||
];
|
||||
@@ -118,6 +139,7 @@ describe('RegistryService', () => {
|
||||
|
||||
const endpoint = 'path';
|
||||
const endpointWithParams = `${endpoint}?size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`;
|
||||
const fieldEndpointWithParams = `${endpoint}?schema=${mockSchemasList[0].prefix}&size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`;
|
||||
|
||||
const halServiceStub = {
|
||||
getEndpoint: (link: string) => observableOf(endpoint)
|
||||
@@ -136,9 +158,11 @@ describe('RegistryService', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const mockStore = new MockStore<AppState>(Object.create(null));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, TranslateModule.forRoot()],
|
||||
declarations: [
|
||||
DummyComponent
|
||||
],
|
||||
@@ -146,6 +170,8 @@ describe('RegistryService', () => {
|
||||
{ provide: RequestService, useValue: getMockRequestService() },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbStub },
|
||||
{ provide: HALEndpointService, useValue: halServiceStub },
|
||||
{ provide: Store, useValue: mockStore },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
RegistryService
|
||||
]
|
||||
});
|
||||
@@ -237,7 +263,7 @@ describe('RegistryService', () => {
|
||||
});
|
||||
|
||||
it('should call getByHref on the request service with the correct request url', () => {
|
||||
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams);
|
||||
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(fieldEndpointWithParams);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -269,4 +295,186 @@ describe('RegistryService', () => {
|
||||
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dispatching to the store', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(mockStore, 'dispatch');
|
||||
});
|
||||
|
||||
describe('when calling editMetadataSchema', () => {
|
||||
beforeEach(() => {
|
||||
registryService.editMetadataSchema(mockSchemasList[0]);
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistryEditSchemaAction with the correct schema', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditSchemaAction(mockSchemasList[0]));
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling cancelEditMetadataSchema', () => {
|
||||
beforeEach(() => {
|
||||
registryService.cancelEditMetadataSchema();
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistryCancelSchemaAction', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelSchemaAction());
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling selectMetadataSchema', () => {
|
||||
beforeEach(() => {
|
||||
registryService.selectMetadataSchema(mockSchemasList[0]);
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistrySelectSchemaAction with the correct schema', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectSchemaAction(mockSchemasList[0]));
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling deselectMetadataSchema', () => {
|
||||
beforeEach(() => {
|
||||
registryService.deselectMetadataSchema(mockSchemasList[0]);
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistryDeselectSchemaAction with the correct schema', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectSchemaAction(mockSchemasList[0]));
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling deselectAllMetadataSchema', () => {
|
||||
beforeEach(() => {
|
||||
registryService.deselectAllMetadataSchema();
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistryDeselectAllSchemaAction', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllSchemaAction());
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling editMetadataField', () => {
|
||||
beforeEach(() => {
|
||||
registryService.editMetadataField(mockFieldsList[0]);
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistryEditFieldAction with the correct Field', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditFieldAction(mockFieldsList[0]));
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling cancelEditMetadataField', () => {
|
||||
beforeEach(() => {
|
||||
registryService.cancelEditMetadataField();
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistryCancelFieldAction', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelFieldAction());
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling selectMetadataField', () => {
|
||||
beforeEach(() => {
|
||||
registryService.selectMetadataField(mockFieldsList[0]);
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistrySelectFieldAction with the correct Field', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectFieldAction(mockFieldsList[0]));
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling deselectMetadataField', () => {
|
||||
beforeEach(() => {
|
||||
registryService.deselectMetadataField(mockFieldsList[0]);
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistryDeselectFieldAction with the correct Field', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectFieldAction(mockFieldsList[0]));
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling deselectAllMetadataField', () => {
|
||||
beforeEach(() => {
|
||||
registryService.deselectAllMetadataField();
|
||||
});
|
||||
|
||||
it('should dispatch a MetadataRegistryDeselectAllFieldAction', () => {
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllFieldAction());
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('when createOrUpdateMetadataSchema is called', () => {
|
||||
let result: Observable<MetadataSchema>;
|
||||
|
||||
beforeEach(() => {
|
||||
result = registryService.createOrUpdateMetadataSchema(mockSchemasList[0]);
|
||||
});
|
||||
|
||||
it('should return the created/updated metadata schema', () => {
|
||||
result.subscribe((schema: MetadataSchema) => {
|
||||
expect(schema).toEqual(mockSchemasList[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when createOrUpdateMetadataField is called', () => {
|
||||
let result: Observable<MetadataField>;
|
||||
|
||||
beforeEach(() => {
|
||||
result = registryService.createOrUpdateMetadataField(mockFieldsList[0]);
|
||||
});
|
||||
|
||||
it('should return the created/updated metadata field', () => {
|
||||
result.subscribe((field: MetadataField) => {
|
||||
expect(field).toEqual(mockFieldsList[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when deleteMetadataSchema is called', () => {
|
||||
let result: Observable<RestResponse>;
|
||||
|
||||
beforeEach(() => {
|
||||
result = registryService.deleteMetadataSchema(mockSchemasList[0].id);
|
||||
});
|
||||
|
||||
it('should return a successful response', () => {
|
||||
result.subscribe((response: RestResponse) => {
|
||||
expect(response.isSuccessful).toBe(true);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('when deleteMetadataField is called', () => {
|
||||
let result: Observable<RestResponse>;
|
||||
|
||||
beforeEach(() => {
|
||||
result = registryService.deleteMetadataField(mockFieldsList[0].id);
|
||||
});
|
||||
|
||||
it('should return a successful response', () => {
|
||||
result.subscribe((response: RestResponse) => {
|
||||
expect(response.isSuccessful).toBe(true);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('when clearMetadataSchemaRequests is called', () => {
|
||||
beforeEach(() => {
|
||||
registryService.clearMetadataSchemaRequests().subscribe();
|
||||
});
|
||||
|
||||
it('should remove the requests related to metadata schemas from cache', () => {
|
||||
expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clearMetadataFieldRequests is called', () => {
|
||||
beforeEach(() => {
|
||||
registryService.clearMetadataFieldRequests().subscribe();
|
||||
});
|
||||
|
||||
it('should remove the requests related to metadata fields from cache', () => {
|
||||
expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -6,8 +6,14 @@ import { PageInfo } from '../shared/page-info.model';
|
||||
import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||
import { MetadataField } from '../metadata/metadatafield.model';
|
||||
import { BitstreamFormat } from './mock-bitstream-format.model';
|
||||
import { filter, flatMap, map, tap } from 'rxjs/operators';
|
||||
import { GetRequest, RestRequest } from '../data/request.models';
|
||||
import {
|
||||
CreateMetadataFieldRequest,
|
||||
CreateMetadataSchemaRequest,
|
||||
DeleteRequest,
|
||||
GetRequest,
|
||||
RestRequest, UpdateMetadataFieldRequest,
|
||||
UpdateMetadataSchemaRequest
|
||||
} from '../data/request.models';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { ResponseParsingService } from '../data/parsing.service';
|
||||
import { RegistryMetadataschemasResponseParsingService } from '../data/registry-metadataschemas-response-parsing.service';
|
||||
@@ -15,20 +21,49 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
||||
import {
|
||||
ErrorResponse, MetadatafieldSuccessResponse, MetadataschemaSuccessResponse,
|
||||
RegistryBitstreamformatsSuccessResponse,
|
||||
RegistryMetadatafieldsSuccessResponse,
|
||||
RegistryMetadataschemasSuccessResponse
|
||||
RegistryMetadataschemasSuccessResponse, RestResponse
|
||||
} from '../cache/response.models';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service';
|
||||
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service';
|
||||
import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
import { configureRequest, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
||||
import { createSelector, select, Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
||||
import {
|
||||
MetadataRegistryCancelFieldAction,
|
||||
MetadataRegistryCancelSchemaAction, MetadataRegistryDeselectAllFieldAction, MetadataRegistryDeselectAllSchemaAction,
|
||||
MetadataRegistryDeselectFieldAction,
|
||||
MetadataRegistryDeselectSchemaAction,
|
||||
MetadataRegistryEditFieldAction,
|
||||
MetadataRegistryEditSchemaAction,
|
||||
MetadataRegistrySelectFieldAction,
|
||||
MetadataRegistrySelectSchemaAction
|
||||
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
|
||||
import { distinctUntilChanged, flatMap, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
|
||||
import { ResourceType } from '../shared/resource-type';
|
||||
import { NormalizedMetadataSchema } from '../metadata/normalized-metadata-schema.model';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
||||
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
||||
const selectedMetadataSchemasSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.selectedSchemas);
|
||||
const editMetadataFieldSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editField);
|
||||
const selectedMetadataFieldsSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.selectedFields);
|
||||
|
||||
@Injectable()
|
||||
export class RegistryService {
|
||||
@@ -39,7 +74,10 @@ export class RegistryService {
|
||||
|
||||
constructor(protected requestService: RequestService,
|
||||
private rdb: RemoteDataBuildService,
|
||||
private halService: HALEndpointService) {
|
||||
private halService: HALEndpointService,
|
||||
private store: Store<AppState>,
|
||||
private notificationsService: NotificationsService,
|
||||
private translateService: TranslateService) {
|
||||
|
||||
}
|
||||
|
||||
@@ -99,7 +137,7 @@ export class RegistryService {
|
||||
}
|
||||
|
||||
public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||
const requestObs = this.getMetadataFieldsRequestObs(pagination);
|
||||
const requestObs = this.getMetadataFieldsBySchemaRequestObs(pagination, schema);
|
||||
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
@@ -111,8 +149,7 @@ export class RegistryService {
|
||||
);
|
||||
|
||||
const metadatafieldsObs: Observable<MetadataField[]> = rmrObs.pipe(
|
||||
map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields),
|
||||
map((metadataFields: MetadataField[]) => metadataFields.filter((field) => field.schema.id === schema.id))
|
||||
map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields)
|
||||
);
|
||||
|
||||
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
||||
@@ -160,7 +197,7 @@ export class RegistryService {
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||
}
|
||||
|
||||
private getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
||||
public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
||||
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
||||
map((url: string) => {
|
||||
const args: string[] = [];
|
||||
@@ -180,10 +217,12 @@ export class RegistryService {
|
||||
);
|
||||
}
|
||||
|
||||
private getMetadataFieldsRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
||||
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
||||
private getMetadataFieldsBySchemaRequestObs(pagination: PaginationComponentOptions, schema: MetadataSchema): Observable<RestRequest> {
|
||||
return this.halService.getEndpoint(this.metadataFieldsPath + '/search/bySchema').pipe(
|
||||
// return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
||||
map((url: string) => {
|
||||
const args: string[] = [];
|
||||
args.push(`schema=${schema.prefix}`);
|
||||
args.push(`size=${pagination.pageSize}`);
|
||||
args.push(`page=${pagination.currentPage - 1}`);
|
||||
if (isNotEmpty(args)) {
|
||||
@@ -220,4 +259,240 @@ export class RegistryService {
|
||||
);
|
||||
}
|
||||
|
||||
public editMetadataSchema(schema: MetadataSchema) {
|
||||
this.store.dispatch(new MetadataRegistryEditSchemaAction(schema));
|
||||
}
|
||||
|
||||
public cancelEditMetadataSchema() {
|
||||
this.store.dispatch(new MetadataRegistryCancelSchemaAction());
|
||||
}
|
||||
|
||||
public getActiveMetadataSchema(): Observable<MetadataSchema> {
|
||||
return this.store.pipe(select(editMetadataSchemaSelector));
|
||||
}
|
||||
|
||||
public selectMetadataSchema(schema: MetadataSchema) {
|
||||
this.store.dispatch(new MetadataRegistrySelectSchemaAction(schema))
|
||||
}
|
||||
|
||||
public deselectMetadataSchema(schema: MetadataSchema) {
|
||||
this.store.dispatch(new MetadataRegistryDeselectSchemaAction(schema))
|
||||
}
|
||||
|
||||
public deselectAllMetadataSchema() {
|
||||
this.store.dispatch(new MetadataRegistryDeselectAllSchemaAction())
|
||||
}
|
||||
|
||||
public getSelectedMetadataSchemas(): Observable<MetadataSchema[]> {
|
||||
return this.store.pipe(select(selectedMetadataSchemasSelector));
|
||||
}
|
||||
|
||||
public editMetadataField(field: MetadataField) {
|
||||
this.store.dispatch(new MetadataRegistryEditFieldAction(field));
|
||||
}
|
||||
|
||||
public cancelEditMetadataField() {
|
||||
this.store.dispatch(new MetadataRegistryCancelFieldAction());
|
||||
}
|
||||
|
||||
public getActiveMetadataField(): Observable<MetadataField> {
|
||||
return this.store.pipe(select(editMetadataFieldSelector));
|
||||
}
|
||||
|
||||
public selectMetadataField(field: MetadataField) {
|
||||
this.store.dispatch(new MetadataRegistrySelectFieldAction(field))
|
||||
}
|
||||
|
||||
public deselectMetadataField(field: MetadataField) {
|
||||
this.store.dispatch(new MetadataRegistryDeselectFieldAction(field))
|
||||
}
|
||||
|
||||
public deselectAllMetadataField() {
|
||||
this.store.dispatch(new MetadataRegistryDeselectAllFieldAction())
|
||||
}
|
||||
|
||||
public getSelectedMetadataFields(): Observable<MetadataField[]> {
|
||||
return this.store.pipe(select(selectedMetadataFieldsSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or Update a MetadataSchema
|
||||
* If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead
|
||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||
* - On creation, a CreateMetadataSchemaRequest is used
|
||||
* - On update, a UpdateMetadataSchemaRequest is used
|
||||
* @param schema The MetadataSchema to create or update
|
||||
*/
|
||||
public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<MetadataSchema> {
|
||||
const isUpdate = hasValue(schema.id);
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
||||
isNotEmptyOperator(),
|
||||
map((endpoint: string) => (isUpdate ? `${endpoint}/${schema.id}` : endpoint)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
const serializedSchema = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(ResourceType.MetadataSchema)).serialize(schema as NormalizedMetadataSchema);
|
||||
|
||||
const request$ = endpoint$.pipe(
|
||||
take(1),
|
||||
map((endpoint: string) => {
|
||||
if (isUpdate) {
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'application/json');
|
||||
options.headers = headers;
|
||||
return new UpdateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema), options);
|
||||
} else {
|
||||
return new CreateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Execute the post/put request
|
||||
request$.pipe(
|
||||
configureRequest(this.requestService)
|
||||
).subscribe();
|
||||
|
||||
// Return created/updated schema
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: RestResponse) => {
|
||||
if (!response.isSuccessful) {
|
||||
if (hasValue((response as any).errorMessage)) {
|
||||
this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1));
|
||||
}
|
||||
} else {
|
||||
this.showNotifications(true, isUpdate, false, { prefix: schema.prefix });
|
||||
return response;
|
||||
}
|
||||
}),
|
||||
isNotEmptyOperator(),
|
||||
map((response: MetadataschemaSuccessResponse) => {
|
||||
if (isNotEmpty(response.metadataschema)) {
|
||||
return response.metadataschema;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public deleteMetadataSchema(id: number): Observable<RestResponse> {
|
||||
return this.delete(this.metadataSchemasPath, id);
|
||||
}
|
||||
|
||||
public clearMetadataSchemaRequests(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
||||
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or Update a MetadataField
|
||||
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||
* - On creation, a CreateMetadataFieldRequest is used
|
||||
* - On update, a UpdateMetadataFieldRequest is used
|
||||
* @param field The MetadataField to create or update
|
||||
*/
|
||||
public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> {
|
||||
const isUpdate = hasValue(field.id);
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
||||
isNotEmptyOperator(),
|
||||
map((endpoint: string) => (isUpdate ? `${endpoint}/${field.id}` : `${endpoint}?schemaId=${field.schema.id}`)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
const request$ = endpoint$.pipe(
|
||||
take(1),
|
||||
map((endpoint: string) => {
|
||||
if (isUpdate) {
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'application/json');
|
||||
options.headers = headers;
|
||||
return new UpdateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field), options);
|
||||
} else {
|
||||
return new CreateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Execute the post/put request
|
||||
request$.pipe(
|
||||
configureRequest(this.requestService)
|
||||
).subscribe();
|
||||
|
||||
// Return created/updated field
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: RestResponse) => {
|
||||
if (!response.isSuccessful) {
|
||||
if (hasValue((response as any).errorMessage)) {
|
||||
this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1));
|
||||
}
|
||||
} else {
|
||||
const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`;
|
||||
this.showNotifications(true, isUpdate, true, { field: fieldString });
|
||||
return response;
|
||||
}
|
||||
}),
|
||||
isNotEmptyOperator(),
|
||||
map((response: MetadatafieldSuccessResponse) => {
|
||||
if (isNotEmpty(response.metadatafield)) {
|
||||
return response.metadatafield;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public deleteMetadataField(id: number): Observable<RestResponse> {
|
||||
return this.delete(this.metadataFieldsPath, id);
|
||||
}
|
||||
|
||||
public clearMetadataFieldRequests(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
||||
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||
)
|
||||
}
|
||||
|
||||
private delete(path: string, id: number): Observable<RestResponse> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.halService.getEndpoint(path).pipe(
|
||||
isNotEmptyOperator(),
|
||||
map((endpoint: string) => `${endpoint}/${id}`),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
const request$ = endpoint$.pipe(
|
||||
take(1),
|
||||
map((endpoint: string) => new DeleteRequest(requestId, endpoint))
|
||||
);
|
||||
|
||||
// Execute the delete request
|
||||
request$.pipe(
|
||||
configureRequest(this.requestService)
|
||||
).subscribe();
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
getResponseFromEntry()
|
||||
);
|
||||
}
|
||||
|
||||
private showNotifications(success: boolean, edited: boolean, isField: boolean, options: any) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const editedString = edited ? 'edited' : 'created';
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}${isField ? '.field' : ''}.${editedString}`, options)
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content)
|
||||
} else {
|
||||
this.notificationsService.error(head, content)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ export class Collection extends DSpaceObject {
|
||||
* Corresponds to the metadata field dc.description
|
||||
*/
|
||||
get introductoryText(): string {
|
||||
return this.findMetadata('dc.description');
|
||||
return this.firstMetadataValue('dc.description');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,7 +24,7 @@ export class Collection extends DSpaceObject {
|
||||
* Corresponds to the metadata field dc.description.abstract
|
||||
*/
|
||||
get shortDescription(): string {
|
||||
return this.findMetadata('dc.description.abstract');
|
||||
return this.firstMetadataValue('dc.description.abstract');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +32,7 @@ export class Collection extends DSpaceObject {
|
||||
* Corresponds to the metadata field dc.rights
|
||||
*/
|
||||
get copyrightText(): string {
|
||||
return this.findMetadata('dc.rights');
|
||||
return this.firstMetadataValue('dc.rights');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@ export class Collection extends DSpaceObject {
|
||||
* Corresponds to the metadata field dc.rights.license
|
||||
*/
|
||||
get license(): string {
|
||||
return this.findMetadata('dc.rights.license');
|
||||
return this.firstMetadataValue('dc.rights.license');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,7 +48,7 @@ export class Collection extends DSpaceObject {
|
||||
* Corresponds to the metadata field dc.description.tableofcontents
|
||||
*/
|
||||
get sidebarText(): string {
|
||||
return this.findMetadata('dc.description.tableofcontents');
|
||||
return this.firstMetadataValue('dc.description.tableofcontents');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -17,7 +17,7 @@ export class Community extends DSpaceObject {
|
||||
* Corresponds to the metadata field dc.description
|
||||
*/
|
||||
get introductoryText(): string {
|
||||
return this.findMetadata('dc.description');
|
||||
return this.firstMetadataValue('dc.description');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,7 +25,7 @@ export class Community extends DSpaceObject {
|
||||
* Corresponds to the metadata field dc.description.abstract
|
||||
*/
|
||||
get shortDescription(): string {
|
||||
return this.findMetadata('dc.description.abstract');
|
||||
return this.firstMetadataValue('dc.description.abstract');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ export class Community extends DSpaceObject {
|
||||
* Corresponds to the metadata field dc.rights
|
||||
*/
|
||||
get copyrightText(): string {
|
||||
return this.findMetadata('dc.rights');
|
||||
return this.firstMetadataValue('dc.rights');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +41,7 @@ export class Community extends DSpaceObject {
|
||||
* Corresponds to the metadata field dc.description.tableofcontents
|
||||
*/
|
||||
get sidebarText(): string {
|
||||
return this.findMetadata('dc.description.tableofcontents');
|
||||
return this.firstMetadataValue('dc.description.tableofcontents');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Metadatum } from './metadatum.model'
|
||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces';
|
||||
import { Metadata } from './metadata.model';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { ResourceType } from './resource-type';
|
||||
@@ -35,14 +35,14 @@ export class DSpaceObject implements CacheableObject, ListableObject {
|
||||
* The name for this DSpaceObject
|
||||
*/
|
||||
get name(): string {
|
||||
return this.findMetadata('dc.title');
|
||||
return this.firstMetadataValue('dc.title');
|
||||
}
|
||||
|
||||
/**
|
||||
* An array containing all metadata of this DSpaceObject
|
||||
* All metadata of this DSpaceObject
|
||||
*/
|
||||
@autoserialize
|
||||
metadata: Metadatum[] = [];
|
||||
metadata: MetadataMap;
|
||||
|
||||
/**
|
||||
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
||||
@@ -55,41 +55,58 @@ export class DSpaceObject implements CacheableObject, ListableObject {
|
||||
owner: Observable<RemoteData<DSpaceObject>>;
|
||||
|
||||
/**
|
||||
* Find a metadata field by key and language
|
||||
* Gets all matching metadata in this DSpaceObject.
|
||||
*
|
||||
* This method returns the value of the first element
|
||||
* in the metadata array that matches the provided
|
||||
* key and language
|
||||
*
|
||||
* @param key
|
||||
* @param language
|
||||
* @return string
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {MetadataValue[]} the matching values or an empty array.
|
||||
*/
|
||||
findMetadata(key: string, language?: string): string {
|
||||
const metadatum = this.metadata.find((m: Metadatum) => {
|
||||
return m.key === key && (isEmpty(language) || m.language === language)
|
||||
});
|
||||
if (isNotEmpty(metadatum)) {
|
||||
return metadatum.value;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
allMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue[] {
|
||||
return Metadata.all(this.metadata, keyOrKeys, valueFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find metadata by an array of keys
|
||||
* Like [[allMetadata]], but only returns string values.
|
||||
*
|
||||
* This method returns the values of the element
|
||||
* in the metadata array that match the provided
|
||||
* key(s)
|
||||
*
|
||||
* @param key(s)
|
||||
* @return Array<Metadatum>
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {string[]} the matching string values or an empty array.
|
||||
*/
|
||||
filterMetadata(keys: string[]): Metadatum[] {
|
||||
return this.metadata.filter((metadatum: Metadatum) => {
|
||||
return keys.some((key) => key === metadatum.key);
|
||||
});
|
||||
allMetadataValues(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string[] {
|
||||
return Metadata.allValues(this.metadata, keyOrKeys, valueFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first matching MetadataValue object in this DSpaceObject, or `undefined`.
|
||||
*
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {MetadataValue} the first matching value, or `undefined`.
|
||||
*/
|
||||
firstMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue {
|
||||
return Metadata.first(this.metadata, keyOrKeys, valueFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [[firstMetadata]], but only returns a string value, or `undefined`.
|
||||
*
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {string} the first matching string value, or `undefined`.
|
||||
*/
|
||||
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
|
||||
return Metadata.firstValue(this.metadata, keyOrKeys, valueFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for a matching metadata value in this DSpaceObject.
|
||||
*
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {boolean} whether a match is found.
|
||||
*/
|
||||
hasMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): boolean {
|
||||
return Metadata.has(this.metadata, keyOrKeys, valueFilter);
|
||||
}
|
||||
|
||||
}
|
||||
|
30
src/app/core/shared/metadata.interfaces.ts
Normal file
30
src/app/core/shared/metadata.interfaces.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/** A map of metadata keys to an ordered list of MetadataValue objects. */
|
||||
export interface MetadataMap {
|
||||
[ key: string ]: MetadataValue[];
|
||||
}
|
||||
|
||||
/** A single metadata value and its properties. */
|
||||
export interface MetadataValue {
|
||||
|
||||
/** The language. */
|
||||
language: string;
|
||||
|
||||
/** The string value. */
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** Constraints for matching metadata values. */
|
||||
export interface MetadataValueFilter {
|
||||
|
||||
/** The language constraint. */
|
||||
language?: string;
|
||||
|
||||
/** The value constraint. */
|
||||
value?: string;
|
||||
|
||||
/** Whether the value constraint should match without regard to case. */
|
||||
ignoreCase?: boolean;
|
||||
|
||||
/** Whether the value constraint should match as a substring. */
|
||||
substring?: boolean;
|
||||
}
|
175
src/app/core/shared/metadata.model.spec.ts
Normal file
175
src/app/core/shared/metadata.model.spec.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { isUndefined } from '../../shared/empty.util';
|
||||
import { MetadataValue, MetadataValueFilter } from './metadata.interfaces';
|
||||
import { Metadata } from './metadata.model';
|
||||
|
||||
const mdValue = (value: string, language?: string): MetadataValue => {
|
||||
return { value: value, language: isUndefined(language) ? null : language };
|
||||
}
|
||||
|
||||
const dcDescription = mdValue('Some description');
|
||||
const dcAbstract = mdValue('Some abstract');
|
||||
const dcTitle0 = mdValue('Title 0');
|
||||
const dcTitle1 = mdValue('Title 1');
|
||||
const dcTitle2 = mdValue('Title 2', 'en_US');
|
||||
const bar = mdValue('Bar');
|
||||
|
||||
const singleMap = { 'dc.title': [ dcTitle0 ] };
|
||||
|
||||
const multiMap = {
|
||||
'dc.description': [ dcDescription ],
|
||||
'dc.description.abstract': [ dcAbstract ],
|
||||
'dc.title': [ dcTitle1, dcTitle2 ],
|
||||
'foo': [ bar ]
|
||||
};
|
||||
|
||||
const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?) => {
|
||||
const keys = keyOrKeys instanceof Array ? keyOrKeys : [ keyOrKeys ];
|
||||
describe('and key' + (keys.length === 1 ? (' ' + keys[0]) : ('s ' + JSON.stringify(keys)))
|
||||
+ ' with ' + (isUndefined(filter) ? 'no filter' : 'filter ' + JSON.stringify(filter)), () => {
|
||||
const result = fn(mapOrMaps, keys, filter);
|
||||
let shouldReturn;
|
||||
if (resultKind === 'boolean') {
|
||||
shouldReturn = expected;
|
||||
} else if (isUndefined(expected)) {
|
||||
shouldReturn = 'undefined';
|
||||
} else if (expected instanceof Array) {
|
||||
shouldReturn = 'an array with ' + expected.length + ' ' + (expected.length > 1 ? 'ordered ' : '')
|
||||
+ resultKind + (expected.length !== 1 ? 's' : '');
|
||||
} else {
|
||||
shouldReturn = 'a ' + resultKind;
|
||||
}
|
||||
it('should return ' + shouldReturn, () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
describe('Metadata', () => {
|
||||
|
||||
describe('all method', () => {
|
||||
|
||||
const testAll = (mapOrMaps, keyOrKeys, expected, filter?: MetadataValueFilter) =>
|
||||
testMethod(Metadata.all, 'value', mapOrMaps, keyOrKeys, expected, filter);
|
||||
|
||||
describe('with emptyMap', () => {
|
||||
testAll({}, 'foo', []);
|
||||
testAll({}, '*', []);
|
||||
});
|
||||
describe('with singleMap', () => {
|
||||
testAll(singleMap, 'foo', []);
|
||||
testAll(singleMap, '*', [ dcTitle0 ]);
|
||||
testAll(singleMap, '*', [], { value: 'baz' });
|
||||
testAll(singleMap, 'dc.title', [ dcTitle0 ]);
|
||||
testAll(singleMap, 'dc.*', [ dcTitle0 ]);
|
||||
});
|
||||
describe('with multiMap', () => {
|
||||
testAll(multiMap, 'foo', [ bar ]);
|
||||
testAll(multiMap, '*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2, bar ]);
|
||||
testAll(multiMap, 'dc.title', [ dcTitle1, dcTitle2 ]);
|
||||
testAll(multiMap, 'dc.*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2 ]);
|
||||
testAll(multiMap, [ 'dc.title', 'dc.*' ], [ dcTitle1, dcTitle2, dcDescription, dcAbstract ]);
|
||||
});
|
||||
describe('with [ singleMap, multiMap ]', () => {
|
||||
testAll([ singleMap, multiMap ], 'foo', [ bar ]);
|
||||
testAll([ singleMap, multiMap ], '*', [ dcTitle0 ]);
|
||||
testAll([ singleMap, multiMap ], 'dc.title', [ dcTitle0 ]);
|
||||
testAll([ singleMap, multiMap ], 'dc.*', [ dcTitle0 ]);
|
||||
});
|
||||
describe('with [ multiMap, singleMap ]', () => {
|
||||
testAll([ multiMap, singleMap ], 'foo', [ bar ]);
|
||||
testAll([ multiMap, singleMap ], '*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2, bar ]);
|
||||
testAll([ multiMap, singleMap ], 'dc.title', [ dcTitle1, dcTitle2 ]);
|
||||
testAll([ multiMap, singleMap ], 'dc.*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2 ]);
|
||||
testAll([ multiMap, singleMap ], [ 'dc.title', 'dc.*' ], [ dcTitle1, dcTitle2, dcDescription, dcAbstract ]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('allValues method', () => {
|
||||
|
||||
const testAllValues = (mapOrMaps, keyOrKeys, expected) =>
|
||||
testMethod(Metadata.allValues, 'string', mapOrMaps, keyOrKeys, expected);
|
||||
|
||||
describe('with emptyMap', () => {
|
||||
testAllValues({}, '*', []);
|
||||
});
|
||||
describe('with singleMap', () => {
|
||||
testAllValues([ singleMap, multiMap ], '*', [ dcTitle0.value ]);
|
||||
});
|
||||
describe('with [ multiMap, singleMap ]', () => {
|
||||
testAllValues([ multiMap, singleMap ], '*', [ dcDescription.value, dcAbstract.value, dcTitle1.value, dcTitle2.value, bar.value ]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('first method', () => {
|
||||
|
||||
const testFirst = (mapOrMaps, keyOrKeys, expected) =>
|
||||
testMethod(Metadata.first, 'value', mapOrMaps, keyOrKeys, expected);
|
||||
|
||||
describe('with emptyMap', () => {
|
||||
testFirst({}, '*', undefined);
|
||||
});
|
||||
describe('with singleMap', () => {
|
||||
testFirst(singleMap, '*', dcTitle0);
|
||||
});
|
||||
describe('with [ multiMap, singleMap ]', () => {
|
||||
testFirst([ multiMap, singleMap ], '*', dcDescription);
|
||||
});
|
||||
});
|
||||
|
||||
describe('firstValue method', () => {
|
||||
|
||||
const testFirstValue = (mapOrMaps, keyOrKeys, expected) =>
|
||||
testMethod(Metadata.firstValue, 'value', mapOrMaps, keyOrKeys, expected);
|
||||
|
||||
describe('with emptyMap', () => {
|
||||
testFirstValue({}, '*', undefined);
|
||||
});
|
||||
describe('with singleMap', () => {
|
||||
testFirstValue(singleMap, '*', dcTitle0.value);
|
||||
});
|
||||
describe('with [ multiMap, singleMap ]', () => {
|
||||
testFirstValue([ multiMap, singleMap ], '*', dcDescription.value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('has method', () => {
|
||||
|
||||
const testHas = (mapOrMaps, keyOrKeys, expected, filter?: MetadataValueFilter) =>
|
||||
testMethod(Metadata.has, 'boolean', mapOrMaps, keyOrKeys, expected, filter);
|
||||
|
||||
describe('with emptyMap', () => {
|
||||
testHas({}, '*', false);
|
||||
});
|
||||
describe('with singleMap', () => {
|
||||
testHas(singleMap, '*', true);
|
||||
testHas(singleMap, '*', false, { value: 'baz' });
|
||||
});
|
||||
describe('with [ multiMap, singleMap ]', () => {
|
||||
testHas([ multiMap, singleMap ], '*', true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('valueMatches method', () => {
|
||||
|
||||
const testValueMatches = (value: MetadataValue, expected: boolean, filter?: MetadataValueFilter) => {
|
||||
describe('with value ' + JSON.stringify(value) + ' and filter '
|
||||
+ (isUndefined(filter) ? 'undefined' : JSON.stringify(filter)), () => {
|
||||
const result = Metadata.valueMatches(value, filter);
|
||||
it('should return ' + expected, () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
testValueMatches(mdValue('a'), true);
|
||||
testValueMatches(mdValue('a'), true, { value: 'a' });
|
||||
testValueMatches(mdValue('a'), false, { value: 'A' });
|
||||
testValueMatches(mdValue('a'), true, { value: 'A', ignoreCase: true });
|
||||
testValueMatches(mdValue('ab'), false, { value: 'b' });
|
||||
testValueMatches(mdValue('ab'), true, { value: 'b', substring: true });
|
||||
testValueMatches(mdValue('a'), true, { language: null });
|
||||
testValueMatches(mdValue('a'), false, { language: 'en_US' });
|
||||
testValueMatches(mdValue('a', 'en_US'), true, { language: 'en_US' });
|
||||
});
|
||||
|
||||
});
|
163
src/app/core/shared/metadata.model.ts
Normal file
163
src/app/core/shared/metadata.model.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { isEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util';
|
||||
import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces';
|
||||
|
||||
/**
|
||||
* Utility class for working with DSpace object metadata.
|
||||
*
|
||||
* When specifying metadata keys, wildcards are supported, so `'*'` will match all keys, `'dc.date.*'` will
|
||||
* match all qualified dc dates, and so on. Exact keys will be evaluated (and matches returned) in the order
|
||||
* they are given.
|
||||
*
|
||||
* When multiple keys in a map match a given wildcard, they are evaluated in the order they are stored in
|
||||
* the map (alphanumeric if obtained from the REST api). If duplicate or overlapping keys are specified, the
|
||||
* first one takes precedence. For example, specifying `['dc.date', 'dc.*', '*']` will cause any `dc.date`
|
||||
* values to be evaluated (and returned, if matched) first, followed by any other `dc` metadata values,
|
||||
* followed by any other (non-dc) metadata values.
|
||||
*/
|
||||
export class Metadata {
|
||||
|
||||
/**
|
||||
* Gets all matching metadata in the map(s).
|
||||
*
|
||||
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). When multiple maps are given, they will be
|
||||
* checked in order, and only values from the first with at least one match will be returned.
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {MetadataValue[]} the matching values or an empty array.
|
||||
*/
|
||||
public static all(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||
filter?: MetadataValueFilter): MetadataValue[] {
|
||||
const mdMaps: MetadataMap[] = mapOrMaps instanceof Array ? mapOrMaps : [ mapOrMaps ];
|
||||
const matches: MetadataValue[] = [];
|
||||
for (const mdMap of mdMaps) {
|
||||
for (const mdKey of Metadata.resolveKeys(mdMap, keyOrKeys)) {
|
||||
const candidates = mdMap[mdKey];
|
||||
if (candidates) {
|
||||
for (const candidate of candidates) {
|
||||
if (Metadata.valueMatches(candidate, filter)) {
|
||||
matches.push(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isEmpty(matches)) {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [[Metadata.all]], but only returns string values.
|
||||
*
|
||||
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). When multiple maps are given, they will be
|
||||
* checked in order, and only values from the first with at least one match will be returned.
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {string[]} the matching string values or an empty array.
|
||||
*/
|
||||
public static allValues(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||
filter?: MetadataValueFilter): string[] {
|
||||
return Metadata.all(mapOrMaps, keyOrKeys, filter).map((mdValue) => mdValue.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first matching MetadataValue object in the map(s), or `undefined`.
|
||||
*
|
||||
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {MetadataValue} the first matching value, or `undefined`.
|
||||
*/
|
||||
public static first(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||
filter?: MetadataValueFilter): MetadataValue {
|
||||
const mdMaps: MetadataMap[] = mdMapOrMaps instanceof Array ? mdMapOrMaps : [ mdMapOrMaps ];
|
||||
for (const mdMap of mdMaps) {
|
||||
for (const key of Metadata.resolveKeys(mdMap, keyOrKeys)) {
|
||||
const values: MetadataValue[] = mdMap[key];
|
||||
if (values) {
|
||||
return values.find((value: MetadataValue) => Metadata.valueMatches(value, filter));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [[Metadata.first]], but only returns a string value, or `undefined`.
|
||||
*
|
||||
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {string} the first matching string value, or `undefined`.
|
||||
*/
|
||||
public static firstValue(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||
filter?: MetadataValueFilter): string {
|
||||
const value = Metadata.first(mdMapOrMaps, keyOrKeys, filter);
|
||||
return isUndefined(value) ? undefined : value.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for a matching metadata value in the given map(s).
|
||||
*
|
||||
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
|
||||
* @returns {boolean} whether a match is found.
|
||||
*/
|
||||
public static has(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
|
||||
filter?: MetadataValueFilter): boolean {
|
||||
return isNotUndefined(Metadata.first(mdMapOrMaps, keyOrKeys, filter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value matches a filter.
|
||||
*
|
||||
* @param {MetadataValue} mdValue the value to check.
|
||||
* @param {MetadataValueFilter} filter the filter to use.
|
||||
* @returns {boolean} whether the filter matches, or true if no filter is given.
|
||||
*/
|
||||
public static valueMatches(mdValue: MetadataValue, filter: MetadataValueFilter) {
|
||||
if (!filter) {
|
||||
return true;
|
||||
} else if (filter.language && filter.language !== mdValue.language) {
|
||||
return false;
|
||||
} else if (filter.value) {
|
||||
let fValue = filter.value;
|
||||
let mValue = mdValue.value;
|
||||
if (filter.ignoreCase) {
|
||||
fValue = filter.value.toLowerCase();
|
||||
mValue = mdValue.value.toLowerCase();
|
||||
}
|
||||
if (filter.substring) {
|
||||
return mValue.includes(fValue);
|
||||
} else {
|
||||
return mValue === fValue;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of keys in the map limited by, and in the order given by `keyOrKeys`.
|
||||
*
|
||||
* @param {MetadataMap} mdMap The source map.
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
|
||||
*/
|
||||
private static resolveKeys(mdMap: MetadataMap, keyOrKeys: string | string[]): string[] {
|
||||
const inputKeys: string[] = keyOrKeys instanceof Array ? keyOrKeys : [ keyOrKeys ];
|
||||
const outputKeys: string[] = [];
|
||||
for (const inputKey of inputKeys) {
|
||||
if (inputKey.includes('*')) {
|
||||
const inputKeyRegex = new RegExp('^' + inputKey.replace('.', '\.').replace('*', '.*') + '$');
|
||||
for (const mapKey of Object.keys(mdMap)) {
|
||||
if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) {
|
||||
outputKeys.push(mapKey);
|
||||
}
|
||||
}
|
||||
} else if (mdMap.hasOwnProperty(inputKey) && !outputKeys.includes(inputKey)) {
|
||||
outputKeys.push(inputKey);
|
||||
}
|
||||
}
|
||||
return outputKeys;
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
|
||||
export class Metadatum {
|
||||
|
||||
/**
|
||||
* The metadata field of this Metadatum
|
||||
*/
|
||||
@autoserialize
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* The language of this Metadatum
|
||||
*/
|
||||
@autoserialize
|
||||
language: string;
|
||||
|
||||
/**
|
||||
* The value of this Metadatum
|
||||
*/
|
||||
@autoserialize
|
||||
value: string;
|
||||
|
||||
}
|
@@ -60,7 +60,7 @@ export const getRemoteDataPayload = () =>
|
||||
|
||||
export const getSucceededRemoteData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded));
|
||||
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded), hasValueOperator());
|
||||
|
||||
export const getFinishedRemoteData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
|
@@ -8,5 +8,7 @@ export enum ResourceType {
|
||||
Community = 'community',
|
||||
EPerson = 'eperson',
|
||||
Group = 'group',
|
||||
ResourcePolicy = 'resourcePolicy'
|
||||
ResourcePolicy = 'resourcePolicy',
|
||||
MetadataSchema = 'metadataschema',
|
||||
MetadataField = 'metadatafield'
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@ import { ResourceType } from '../../../core/shared/resource-type';
|
||||
import { ComColFormComponent } from './comcol-form.component';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { hasValue } from '../../empty.util';
|
||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||
|
||||
describe('ComColFormComponent', () => {
|
||||
let comp: ComColFormComponent<DSpaceObject>;
|
||||
@@ -29,23 +28,24 @@ describe('ComColFormComponent', () => {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const titleMD = { key: 'dc.title', value: 'Community Title' } as Metadatum;
|
||||
const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' } as Metadatum;
|
||||
const abstractMD = {
|
||||
key: 'dc.description.abstract',
|
||||
value: 'Community description'
|
||||
} as Metadatum;
|
||||
const newTitleMD = { key: 'dc.title', value: 'New Community Title' } as Metadatum;
|
||||
const dcTitle = 'dc.title';
|
||||
const dcRandom = 'dc.random';
|
||||
const dcAbstract = 'dc.description.abstract';
|
||||
|
||||
const titleMD = { [dcTitle]: [ { value: 'Community Title', language: null } ] };
|
||||
const randomMD = { [dcRandom]: [ { value: 'Random metadata excluded from form', language: null } ] };
|
||||
const abstractMD = { [dcAbstract]: [ { value: 'Community description', language: null } ] };
|
||||
const newTitleMD = { [dcTitle]: [ { value: 'New Community Title', language: null } ] };
|
||||
const formModel = [
|
||||
new DynamicInputModel({
|
||||
id: 'title',
|
||||
name: newTitleMD.key,
|
||||
value: 'New Community Title'
|
||||
name: dcTitle,
|
||||
value: newTitleMD[dcTitle][0].value
|
||||
}),
|
||||
new DynamicInputModel({
|
||||
id: 'abstract',
|
||||
name: abstractMD.key,
|
||||
value: abstractMD.value
|
||||
name: dcAbstract,
|
||||
value: abstractMD[dcAbstract][0].value
|
||||
})
|
||||
];
|
||||
|
||||
@@ -87,10 +87,10 @@ describe('ComColFormComponent', () => {
|
||||
comp.dso = Object.assign(
|
||||
new Community(),
|
||||
{
|
||||
metadata: [
|
||||
titleMD,
|
||||
randomMD
|
||||
]
|
||||
metadata: {
|
||||
...titleMD,
|
||||
...randomMD
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -101,11 +101,11 @@ describe('ComColFormComponent', () => {
|
||||
{},
|
||||
new Community(),
|
||||
{
|
||||
metadata: [
|
||||
randomMD,
|
||||
newTitleMD,
|
||||
abstractMD
|
||||
],
|
||||
metadata: {
|
||||
...newTitleMD,
|
||||
...randomMD,
|
||||
...abstractMD
|
||||
},
|
||||
type: ResourceType.Community
|
||||
},
|
||||
)
|
||||
|
@@ -8,6 +8,7 @@ import { FormGroup } from '@angular/forms';
|
||||
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.interfaces';
|
||||
import { isNotEmpty } from '../../empty.util';
|
||||
import { ResourceType } from '../../../core/shared/resource-type';
|
||||
|
||||
@@ -64,7 +65,7 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.formModel.forEach(
|
||||
(fieldModel: DynamicInputModel) => {
|
||||
fieldModel.value = this.dso.findMetadata(fieldModel.name);
|
||||
fieldModel.value = this.dso.firstMetadataValue(fieldModel.name);
|
||||
}
|
||||
);
|
||||
this.formGroup = this.formService.createFormGroup(this.formModel);
|
||||
@@ -77,20 +78,24 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks which new fields where added and sends the updated version of the DSO to the parent component
|
||||
* Checks which new fields were added and sends the updated version of the DSO to the parent component
|
||||
*/
|
||||
onSubmit() {
|
||||
const metadata = this.formModel.map(
|
||||
(fieldModel: DynamicInputModel) => {
|
||||
return { key: fieldModel.name, value: fieldModel.value }
|
||||
const formMetadata = new Object() as MetadataMap;
|
||||
this.formModel.forEach((fieldModel: DynamicInputModel) => {
|
||||
const value: MetadataValue = { value: fieldModel.value as string, language: null };
|
||||
if (formMetadata.hasOwnProperty(fieldModel.name)) {
|
||||
formMetadata[fieldModel.name].push(value);
|
||||
} else {
|
||||
formMetadata[fieldModel.name] = [ value ];
|
||||
}
|
||||
);
|
||||
const filteredOldMetadata = this.dso.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key));
|
||||
const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value));
|
||||
});
|
||||
|
||||
const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata];
|
||||
const updatedDSO = Object.assign({}, this.dso, {
|
||||
metadata: newMetadata,
|
||||
metadata: {
|
||||
...this.dso.metadata,
|
||||
...formMetadata
|
||||
},
|
||||
type: ResourceType.Community
|
||||
});
|
||||
this.submitForm.emit(updatedDSO);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="container-fluid">
|
||||
<form [formGroup]="formGroup">
|
||||
<form class="form-horizontal" [formGroup]="formGroup">
|
||||
|
||||
<ds-dynamic-form
|
||||
[formId]="formId"
|
||||
|
@@ -69,6 +69,12 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
@Output() addArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output() removeArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
|
||||
/**
|
||||
* An event fired when form is valid and submitted .
|
||||
* Event's payload equals to the form content.
|
||||
*/
|
||||
@Output() cancel: EventEmitter<Observable<any>> = new EventEmitter<Observable<any>>();
|
||||
|
||||
/**
|
||||
* An event fired when form is valid and submitted .
|
||||
* Event's payload equals to the form content.
|
||||
@@ -130,7 +136,9 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
|
||||
} else {
|
||||
this.formModel.forEach((model) => {
|
||||
this.formBuilderService.addFormGroupControl(this.formGroup, this.parentFormModel, model);
|
||||
if (this.parentFormModel) {
|
||||
this.formBuilderService.addFormGroupControl(this.formGroup, this.parentFormModel, model);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -275,6 +283,7 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
reset(): void {
|
||||
this.formGroup.reset();
|
||||
this.cancel.emit();
|
||||
}
|
||||
|
||||
isItemReadOnly(arrayContext: DynamicFormArrayModel, index: number): boolean {
|
||||
|
@@ -51,13 +51,14 @@ export const MockItem: Item = Object.assign(new Item(), {
|
||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
type: 'bitstream',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: null,
|
||||
value: 'test_word.docx'
|
||||
}
|
||||
]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
sizeBytes: 31302,
|
||||
@@ -85,13 +86,14 @@ export const MockItem: Item = Object.assign(new Item(), {
|
||||
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||
uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||
type: 'bitstream',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: null,
|
||||
value: 'test_pdf.pdf'
|
||||
}
|
||||
]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_pdf.pdf'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -100,98 +102,106 @@ export const MockItem: Item = Object.assign(new Item(), {
|
||||
id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||
uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||
type: 'item',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.creator',
|
||||
language: 'en_US',
|
||||
value: 'Doe, Jane'
|
||||
},
|
||||
{
|
||||
key: 'dc.date.accessioned',
|
||||
language: null,
|
||||
value: '1650-06-26T19:58:25Z'
|
||||
},
|
||||
{
|
||||
key: 'dc.date.available',
|
||||
language: null,
|
||||
value: '1650-06-26T19:58:25Z'
|
||||
},
|
||||
{
|
||||
key: 'dc.date.issued',
|
||||
language: null,
|
||||
value: '1650-06-26'
|
||||
},
|
||||
{
|
||||
key: 'dc.identifier.issn',
|
||||
language: 'en_US',
|
||||
value: '123456789'
|
||||
},
|
||||
{
|
||||
key: 'dc.identifier.uri',
|
||||
language: null,
|
||||
value: 'http://dspace7.4science.it/xmlui/handle/10673/6'
|
||||
},
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!'
|
||||
},
|
||||
{
|
||||
key: 'dc.description.provenance',
|
||||
language: 'en',
|
||||
value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)'
|
||||
},
|
||||
{
|
||||
key: 'dc.description.provenance',
|
||||
language: 'en',
|
||||
value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).'
|
||||
},
|
||||
{
|
||||
key: 'dc.description.provenance',
|
||||
language: 'en',
|
||||
value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).'
|
||||
},
|
||||
{
|
||||
key: 'dc.description.provenance',
|
||||
language: 'en',
|
||||
value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).'
|
||||
},
|
||||
{
|
||||
key: 'dc.language',
|
||||
language: 'en_US',
|
||||
value: 'en'
|
||||
},
|
||||
{
|
||||
key: 'dc.rights',
|
||||
language: 'en_US',
|
||||
value: '© Jane Doe'
|
||||
},
|
||||
{
|
||||
key: 'dc.subject',
|
||||
language: 'en_US',
|
||||
value: 'keyword1'
|
||||
},
|
||||
{
|
||||
key: 'dc.subject',
|
||||
language: 'en_US',
|
||||
value: 'keyword2'
|
||||
},
|
||||
{
|
||||
key: 'dc.subject',
|
||||
language: 'en_US',
|
||||
value: 'keyword3'
|
||||
},
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test PowerPoint Document'
|
||||
},
|
||||
{
|
||||
key: 'dc.type',
|
||||
language: 'en_US',
|
||||
value: 'text'
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
'dc.creator': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Doe, Jane'
|
||||
}
|
||||
],
|
||||
'dc.date.accessioned': [
|
||||
{
|
||||
language: null,
|
||||
value: '1650-06-26T19:58:25Z'
|
||||
}
|
||||
],
|
||||
'dc.date.available': [
|
||||
{
|
||||
language: null,
|
||||
value: '1650-06-26T19:58:25Z'
|
||||
}
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
language: null,
|
||||
value: '1650-06-26'
|
||||
}
|
||||
],
|
||||
'dc.identifier.issn': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: '123456789'
|
||||
}
|
||||
],
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
language: null,
|
||||
value: 'http://dspace7.4science.it/xmlui/handle/10673/6'
|
||||
}
|
||||
],
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!'
|
||||
}
|
||||
],
|
||||
'dc.description.provenance': [
|
||||
{
|
||||
language: 'en',
|
||||
value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)'
|
||||
},
|
||||
{
|
||||
language: 'en',
|
||||
value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).'
|
||||
},
|
||||
{
|
||||
language: 'en',
|
||||
value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).'
|
||||
},
|
||||
{
|
||||
language: 'en',
|
||||
value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).'
|
||||
}
|
||||
],
|
||||
'dc.language': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'en'
|
||||
}
|
||||
],
|
||||
'dc.rights': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: '© Jane Doe'
|
||||
}
|
||||
],
|
||||
'dc.subject': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'keyword1'
|
||||
},
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'keyword2'
|
||||
},
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'keyword3'
|
||||
}
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Test PowerPoint Document'
|
||||
}
|
||||
],
|
||||
'dc.type': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'text'
|
||||
}
|
||||
]
|
||||
},
|
||||
owningCollection: observableOf({
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb',
|
||||
requestPending: false,
|
||||
|
@@ -7,6 +7,9 @@ export function getMockRequestService(requestEntry$: Observable<RequestEntry> =
|
||||
configure: false,
|
||||
generateRequestId: 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
|
||||
getByHref: requestEntry$,
|
||||
getByUUID: requestEntry$
|
||||
getByUUID: requestEntry$,
|
||||
/* tslint:disable:no-empty */
|
||||
removeByHrefSubstring: () => {}
|
||||
/* tslint:enable:no-empty */
|
||||
});
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { ListableObject } from '../listable-object.model';
|
||||
import { hasValue } from '../../../empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-abstract-object-element',
|
||||
@@ -11,8 +10,4 @@ export class AbstractListableElementComponent <T extends ListableObject> {
|
||||
public constructor(@Inject('objectElementProvider') public listableObject: ListableObject) {
|
||||
this.object = listableObject as T;
|
||||
}
|
||||
|
||||
hasValue(data) {
|
||||
return hasValue(data);
|
||||
}
|
||||
}
|
||||
|
@@ -8,21 +8,25 @@ let collectionGridElementComponent: CollectionGridElementComponent;
|
||||
let fixture: ComponentFixture<CollectionGridElementComponent>;
|
||||
|
||||
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
describe('CollectionGridElementComponent', () => {
|
||||
|
@@ -8,21 +8,25 @@ let communityGridElementComponent: CommunityGridElementComponent;
|
||||
let fixture: ComponentFixture<CommunityGridElementComponent>;
|
||||
|
||||
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
describe('CommunityGridElementComponent', () => {
|
||||
|
@@ -5,19 +5,19 @@
|
||||
</ds-grid-thumbnail>
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
|
||||
<h4 class="card-title">{{object.firstMetadataValue('dc.title')}}</h4>
|
||||
|
||||
<ds-truncatable-part [id]="object.id" [minLines]="2">
|
||||
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
|
||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||
<p *ngIf="object.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])" class="item-authors card-text text-muted">
|
||||
<span *ngFor="let author of object.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{author}}
|
||||
<span *ngIf="!last">; </span>
|
||||
</span>
|
||||
<span *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
|
||||
<span *ngIf="object.hasMetadata('dc.date.issued')" class="item-date">{{object.firstMetadataValue("dc.date.issued")}}</span>
|
||||
</p>
|
||||
</ds-truncatable-part>
|
||||
|
||||
<ds-truncatable-part [id]="object.id" [minLines]="5">
|
||||
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") }}</p>
|
||||
<p *ngIf="object.hasMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.firstMetadataValue("dc.description.abstract")}}</p>
|
||||
</ds-truncatable-part>
|
||||
|
||||
<div class="text-center pt-2">
|
||||
|
@@ -11,31 +11,37 @@ let fixture: ComponentFixture<ItemGridElementComponent>;
|
||||
|
||||
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.contributor.author',
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
},
|
||||
{
|
||||
key: 'dc.date.issued',
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.contributor.author': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
}
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
},
|
||||
{
|
||||
key: 'dc.type',
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
}
|
||||
],
|
||||
'dc.type': [
|
||||
{
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
describe('ItemGridElementComponent', () => {
|
||||
|
@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
|
||||
};
|
||||
|
||||
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||
mockCollectionWithAbstract.hitHighlights = [];
|
||||
mockCollectionWithAbstract.hitHighlights = {};
|
||||
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
} ]
|
||||
metadata: {
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||
mockCollectionWithoutAbstract.hitHighlights = [];
|
||||
mockCollectionWithoutAbstract.hitHighlights = {};
|
||||
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
} ]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
describe('CollectionSearchResultGridElementComponent', () => {
|
||||
|
@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
|
||||
};
|
||||
|
||||
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||
mockCommunityWithAbstract.hitHighlights = [];
|
||||
mockCommunityWithAbstract.hitHighlights = {};
|
||||
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
} ]
|
||||
metadata: {
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||
mockCommunityWithoutAbstract.hitHighlights = [];
|
||||
mockCommunityWithoutAbstract.hitHighlights = {};
|
||||
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
} ]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
describe('CommunitySearchResultGridElementComponent', () => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user