Merge branch 'master' into w2p-71380_Simplify-bitstream-drag-and-drop

This commit is contained in:
Kristof De Langhe
2020-06-26 14:24:22 +02:00
51 changed files with 921 additions and 1190 deletions

View File

@@ -4,6 +4,7 @@ import { NgModule } from '@angular/core';
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component'; import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { getRegistriesModulePath } from '../admin-routing.module'; import { getRegistriesModulePath } from '../admin-routing.module';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats'; const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
@@ -14,16 +15,28 @@ export function getBitstreamFormatsModulePath() {
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{path: 'metadata', component: MetadataRegistryComponent, data: {title: 'admin.registries.metadata.title'}},
{ {
path: 'metadata/:schemaName', path: 'metadata',
component: MetadataSchemaComponent, resolve: { breadcrumb: I18nBreadcrumbResolver },
data: {title: 'admin.registries.schema.title'} data: {title: 'admin.registries.metadata.title', breadcrumbKey: 'admin.registries.metadata'},
children: [
{
path: '',
component: MetadataRegistryComponent
},
{
path: ':schemaName',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: MetadataSchemaComponent,
data: {title: 'admin.registries.schema.title', breadcrumbKey: 'admin.registries.schema'}
}
]
}, },
{ {
path: BITSTREAMFORMATS_MODULE_PATH, path: BITSTREAMFORMATS_MODULE_PATH,
resolve: { breadcrumb: I18nBreadcrumbResolver },
loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule', loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule',
data: {title: 'admin.registries.bitstream-formats.title'} data: {title: 'admin.registries.bitstream-formats.title', breadcrumbKey: 'admin.registries.bitstream-formats'}
}, },
]) ])
] ]

View File

@@ -4,6 +4,7 @@ import { BitstreamFormatsResolver } from './bitstream-formats.resolver';
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component'; import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
import { BitstreamFormatsComponent } from './bitstream-formats.component'; import { BitstreamFormatsComponent } from './bitstream-formats.component';
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component'; import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
import { I18nBreadcrumbResolver } from '../../../core/breadcrumbs/i18n-breadcrumb.resolver';
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit'; const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
const BITSTREAMFORMAT_ADD_PATH = 'add'; const BITSTREAMFORMAT_ADD_PATH = 'add';
@@ -17,14 +18,18 @@ const BITSTREAMFORMAT_ADD_PATH = 'add';
}, },
{ {
path: BITSTREAMFORMAT_ADD_PATH, path: BITSTREAMFORMAT_ADD_PATH,
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AddBitstreamFormatComponent, component: AddBitstreamFormatComponent,
data: {breadcrumbKey: 'admin.registries.bitstream-formats.create'}
}, },
{ {
path: BITSTREAMFORMAT_EDIT_PATH, path: BITSTREAMFORMAT_EDIT_PATH,
component: EditBitstreamFormatComponent, component: EditBitstreamFormatComponent,
resolve: { resolve: {
bitstreamFormat: BitstreamFormatsResolver bitstreamFormat: BitstreamFormatsResolver,
} breadcrumb: I18nBreadcrumbResolver
},
data: {breadcrumbKey: 'admin.registries.bitstream-formats.edit'}
}, },
]) ])
], ],

View File

@@ -11,7 +11,6 @@
<ds-pagination <ds-pagination
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0" *ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="config"
[pageInfoState]="(metadataSchemas | async)?.payload"
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements" [collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true" [hidePagerWhenSinglePage]="true"

View File

@@ -4,7 +4,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { map, take } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { RestResponse } from '../../../core/cache/response.models'; import { RestResponse } from '../../../core/cache/response.models';
import { zip } from 'rxjs/internal/observable/zip'; import { zip } from 'rxjs/internal/observable/zip';
@@ -12,6 +12,8 @@ import { NotificationsService } from '../../../shared/notifications/notification
import { Route, Router } from '@angular/router'; import { Route, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
@Component({ @Component({
selector: 'ds-metadata-registry', selector: 'ds-metadata-registry',
@@ -37,6 +39,11 @@ export class MetadataRegistryComponent {
pageSize: 25 pageSize: 25
}); });
/**
* Whether or not the list of MetadataSchemas needs an update
*/
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
constructor(private registryService: RegistryService, constructor(private registryService: RegistryService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private router: Router, private router: Router,
@@ -50,14 +57,17 @@ export class MetadataRegistryComponent {
*/ */
onPageChange(event) { onPageChange(event) {
this.config.currentPage = event; this.config.currentPage = event;
this.updateSchemas(); this.forceUpdateSchemas();
} }
/** /**
* Update the list of schemas by fetching it from the rest api or cache * Update the list of schemas by fetching it from the rest api or cache
*/ */
private updateSchemas() { private updateSchemas() {
this.metadataSchemas = this.registryService.getMetadataSchemas(this.config); this.metadataSchemas = this.needsUpdate$.pipe(
filter((update) => update === true),
switchMap(() => this.registryService.getMetadataSchemas(toFindListOptions(this.config)))
);
} }
/** /**
@@ -65,8 +75,7 @@ export class MetadataRegistryComponent {
* a new REST call * a new REST call
*/ */
public forceUpdateSchemas() { public forceUpdateSchemas() {
this.registryService.clearMetadataSchemaRequests().subscribe(); this.needsUpdate$.next(true);
this.updateSchemas();
} }
/** /**
@@ -125,6 +134,7 @@ export class MetadataRegistryComponent {
* Delete all the selected metadata schemas * Delete all the selected metadata schemas
*/ */
deleteSchemas() { deleteSchemas() {
this.registryService.clearMetadataSchemaRequests().subscribe();
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe( this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
(schemas) => { (schemas) => {
const tasks$ = []; const tasks$ = [];

View File

@@ -21,7 +21,8 @@ describe('MetadataSchemaFormComponent', () => {
const registryServiceStub = { const registryServiceStub = {
getActiveMetadataSchema: () => observableOf(undefined), getActiveMetadataSchema: () => observableOf(undefined),
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema), createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
cancelEditMetadataSchema: () => {} cancelEditMetadataSchema: () => {},
clearMetadataSchemaRequests: () => observableOf(undefined)
}; };
const formBuilderServiceStub = { const formBuilderServiceStub = {
createFormGroup: () => { createFormGroup: () => {

View File

@@ -128,6 +128,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
* Emit the updated/created schema using the EventEmitter submitForm * Emit the updated/created schema using the EventEmitter submitForm
*/ */
onSubmit() { onSubmit() {
this.registryService.clearMetadataSchemaRequests().subscribe();
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe( this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
(schema) => { (schema) => {
const values = { const values = {
@@ -139,7 +140,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
this.submitForm.emit(newSchema); this.submitForm.emit(newSchema);
}); });
} else { } else {
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), { this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
id: schema.id, id: schema.id,
prefix: (values.prefix ? values.prefix : schema.prefix), prefix: (values.prefix ? values.prefix : schema.prefix),
namespace: (values.namespace ? values.namespace : schema.namespace) namespace: (values.namespace ? values.namespace : schema.namespace)
@@ -148,6 +149,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
}); });
} }
this.clearFields(); this.clearFields();
this.registryService.cancelEditMetadataSchema();
} }
); );
} }

View File

@@ -30,6 +30,7 @@ describe('MetadataFieldFormComponent', () => {
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field), createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
cancelEditMetadataField: () => {}, cancelEditMetadataField: () => {},
cancelEditMetadataSchema: () => {}, cancelEditMetadataSchema: () => {},
clearMetadataFieldRequests: () => observableOf(undefined)
}; };
const formBuilderServiceStub = { const formBuilderServiceStub = {
createFormGroup: () => { createFormGroup: () => {

View File

@@ -153,6 +153,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
* Emit the updated/created field using the EventEmitter submitForm * Emit the updated/created field using the EventEmitter submitForm
*/ */
onSubmit() { onSubmit() {
this.registryService.clearMetadataFieldRequests().subscribe();
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe( this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
(field) => { (field) => {
const values = { const values = {
@@ -166,7 +167,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
this.submitForm.emit(newField); this.submitForm.emit(newField);
}); });
} else { } else {
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), { this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), field, {
id: field.id, id: field.id,
schema: this.metadataSchema, schema: this.metadataSchema,
element: (values.element ? values.element : field.element), element: (values.element ? values.element : field.element),
@@ -177,6 +178,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
}); });
} }
this.clearFields(); this.clearFields();
this.registryService.cancelEditMetadataField();
} }
); );
} }

View File

@@ -1,36 +1,37 @@
<div class="container"> <div class="container">
<div class="metadata-schema row"> <div class="metadata-schema row">
<div class="col-12"> <div class="col-12" *ngVar="(metadataSchema$ | async) as schema">
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{(metadataSchema | async)?.payload?.prefix}}"</h2> <h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{schema?.prefix}}"</h2>
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:namespace }}</p> <p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:{ namespace: schema?.namespace } }}</p>
<ds-metadata-field-form <ds-metadata-field-form
[metadataSchema]="(metadataSchema | async)?.payload" [metadataSchema]="schema"
(submitForm)="forceUpdateFields()"></ds-metadata-field-form> (submitForm)="forceUpdateFields()"></ds-metadata-field-form>
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3> <h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
<ds-pagination <ng-container *ngVar="(metadataFields$ | async)?.payload as fields">
*ngIf="(metadataFields | async)?.payload?.totalElements > 0" <ds-pagination
[paginationOptions]="config" *ngIf="fields?.totalElements > 0"
[pageInfoState]="(metadataFields | async)?.payload" [paginationOptions]="config"
[collectionSize]="(metadataFields | async)?.payload?.totalElements" [pageInfoState]="fields"
[hideGear]="false" [collectionSize]="fields?.totalElements"
[hidePagerWhenSinglePage]="true" [hideGear]="false"
(pageChange)="onPageChange($event)"> [hidePagerWhenSinglePage]="true"
<div class="table-responsive"> (pageChange)="onPageChange($event)">
<table id="metadata-fields" class="table table-striped table-hover"> <div class="table-responsive">
<thead> <table id="metadata-fields" class="table table-striped table-hover">
<thead>
<tr> <tr>
<th></th> <th></th>
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th> <th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th> <th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let field of (metadataFields | async)?.payload?.page" <tr *ngFor="let field of fields?.page"
[ngClass]="{'table-primary' : isActive(field) | async}"> [ngClass]="{'table-primary' : isActive(field) | async}">
<td> <td>
<label> <label>
@@ -39,22 +40,23 @@
(change)="selectMetadataField(field, $event)"> (change)="selectMetadataField(field, $event)">
</label> </label>
</td> </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)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td> <td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
</ds-pagination>
<div *ngIf="fields?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
{{'admin.registries.schema.fields.no-items' | translate}}
</div> </div>
</ds-pagination>
<div *ngIf="(metadataFields | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert"> <div>
{{'admin.registries.schema.fields.no-items' | translate}} <button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
</div> <button *ngIf="fields?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
</div>
<div> </ng-container>
<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> </div>

View File

@@ -22,6 +22,7 @@ import { RestResponse } from '../../../core/cache/response.models';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { MetadataField } from '../../../core/metadata/metadata-field.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { VarDirective } from '../../../shared/utils/var.directive';
describe('MetadataSchemaComponent', () => { describe('MetadataSchemaComponent', () => {
let comp: MetadataSchemaComponent; let comp: MetadataSchemaComponent;
@@ -124,7 +125,7 @@ describe('MetadataSchemaComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe], declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective],
providers: [ providers: [
{ provide: RegistryService, useValue: registryServiceStub }, { provide: RegistryService, useValue: registryServiceStub },
{ provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: ActivatedRoute, useValue: activatedRouteStub },

View File

@@ -5,7 +5,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { map, take } from 'rxjs/operators'; import { map, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { RestResponse } from '../../../core/cache/response.models'; import { RestResponse } from '../../../core/cache/response.models';
import { zip } from 'rxjs/internal/observable/zip'; import { zip } from 'rxjs/internal/observable/zip';
@@ -13,6 +13,10 @@ import { NotificationsService } from '../../../shared/notifications/notification
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { MetadataField } from '../../../core/metadata/metadata-field.model';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
@Component({ @Component({
selector: 'ds-metadata-schema', selector: 'ds-metadata-schema',
@@ -24,21 +28,15 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
* The admin can create, edit or delete metadata fields here. * The admin can create, edit or delete metadata fields here.
*/ */
export class MetadataSchemaComponent implements OnInit { export class MetadataSchemaComponent implements OnInit {
/**
* The namespace of the metadata schema
*/
namespace;
/** /**
* The metadata schema * The metadata schema
*/ */
metadataSchema: Observable<RemoteData<MetadataSchema>>; metadataSchema$: Observable<MetadataSchema>;
/** /**
* A list of all the fields attached to this metadata schema * A list of all the fields attached to this metadata schema
*/ */
metadataFields: Observable<RemoteData<PaginatedList<MetadataField>>>; metadataFields$: Observable<RemoteData<PaginatedList<MetadataField>>>;
/** /**
* Pagination config used to display the list of metadata fields * Pagination config used to display the list of metadata fields
@@ -49,6 +47,11 @@ export class MetadataSchemaComponent implements OnInit {
pageSizeOptions: [25, 50, 100, 200] pageSizeOptions: [25, 50, 100, 200]
}); });
/**
* Whether or not the list of MetadataFields needs an update
*/
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
constructor(private registryService: RegistryService, constructor(private registryService: RegistryService,
private route: ActivatedRoute, private route: ActivatedRoute,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
@@ -68,7 +71,7 @@ export class MetadataSchemaComponent implements OnInit {
* @param params * @param params
*/ */
initialize(params) { initialize(params) {
this.metadataSchema = this.registryService.getMetadataSchemaByName(params.schemaName); this.metadataSchema$ = this.registryService.getMetadataSchemaByName(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
this.updateFields(); this.updateFields();
} }
@@ -78,18 +81,20 @@ export class MetadataSchemaComponent implements OnInit {
*/ */
onPageChange(event) { onPageChange(event) {
this.config.currentPage = event; this.config.currentPage = event;
this.updateFields(); this.forceUpdateFields();
} }
/** /**
* Update the list of fields by fetching it from the rest api or cache * Update the list of fields by fetching it from the rest api or cache
*/ */
private updateFields() { private updateFields() {
this.metadataSchema.subscribe((schemaData) => { this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
const schema = schemaData.payload; switchMap(([schema, update]: [MetadataSchema, boolean]) => {
this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, this.config); if (update) {
this.namespace = {namespace: schemaData.payload.namespace}; return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config));
}); }
})
);
} }
/** /**
@@ -97,8 +102,7 @@ export class MetadataSchemaComponent implements OnInit {
* a new REST call * a new REST call
*/ */
public forceUpdateFields() { public forceUpdateFields() {
this.registryService.clearMetadataFieldRequests().subscribe(); this.needsUpdate$.next(true);
this.updateFields();
} }
/** /**
@@ -157,6 +161,7 @@ export class MetadataSchemaComponent implements OnInit {
* Delete all the selected metadata fields * Delete all the selected metadata fields
*/ */
deleteFields() { deleteFields() {
this.registryService.clearMetadataFieldRequests().subscribe();
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe( this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
(fields) => { (fields) => {
const tasks$ = []; const tasks$ = [];

View File

@@ -14,7 +14,7 @@ describe('I18nBreadcrumbResolver', () => {
}); });
it('should resolve the breadcrumb config', () => { it('should resolve the breadcrumb config', () => {
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path] } as any, {} as any); const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path], pathFromRoot: [{ url: [path] }] } as any, {} as any);
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path }; const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path };
expect(resolvedConfig).toEqual(expectedConfig); expect(resolvedConfig).toEqual(expectedConfig);
}); });

View File

@@ -25,7 +25,17 @@ export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>>
if (hasNoValue(key)) { if (hasNoValue(key)) {
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data') throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
} }
const fullPath = route.url.join(''); const fullPath = this.getResolvedUrl(route);
return { provider: this.breadcrumbService, key: key, url: fullPath }; return { provider: this.breadcrumbService, key: key, url: fullPath };
} }
/**
* Resolve the full URL of an ActivatedRouteSnapshot
* @param route
*/
getResolvedUrl(route: ActivatedRouteSnapshot): string {
return route.pathFromRoot
.map((v) => v.url.map((segment) => segment.toString()).join('/'))
.join('/');
}
} }

View File

@@ -6,9 +6,6 @@ import { ConfigObject } from '../config/models/config.model';
import { FacetValue } from '../../shared/search/facet-value.model'; import { FacetValue } from '../../shared/search/facet-value.model';
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
import { IntegrationModel } from '../integration/models/integration.model'; import { IntegrationModel } from '../integration/models/integration.model';
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
import { SubmissionObject } from '../submission/models/submission-object.model'; import { SubmissionObject } from '../submission/models/submission-object.model';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
@@ -40,48 +37,6 @@ 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,
public statusCode: number,
public statusText: string,
public pageInfo?: PageInfo
) {
super(true, statusCode, statusText);
}
}
/**
* A successful response containing a list of MetadataFields wrapped in a RegistryMetadatafieldsResponse
*/
export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
constructor(
public metadatafieldsResponse: RegistryMetadatafieldsResponse,
public statusCode: number,
public statusText: string,
public pageInfo?: PageInfo
) {
super(true, statusCode, statusText);
}
}
/**
* A successful response containing a list of BitstreamFormats wrapped in a RegistryBitstreamformatsResponse
*/
export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
constructor(
public bitstreamformatsResponse: RegistryBitstreamformatsResponse,
public statusCode: number,
public statusText: string,
public pageInfo?: PageInfo
) {
super(true, statusCode, statusText);
}
}
/** /**
* A successful response containing exactly one MetadataSchema * A successful response containing exactly one MetadataSchema
*/ */

View File

@@ -69,13 +69,8 @@ import { ItemDataService } from './data/item-data.service';
import { LicenseDataService } from './data/license-data.service'; import { LicenseDataService } from './data/license-data.service';
import { LookupRelationService } from './data/lookup-relation.service'; import { LookupRelationService } from './data/lookup-relation.service';
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
import { MetadatafieldParsingService } from './data/metadatafield-parsing.service';
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service';
import { RelationshipTypeService } from './data/relationship-type.service'; import { RelationshipTypeService } from './data/relationship-type.service';
import { RelationshipService } from './data/relationship.service'; import { RelationshipService } from './data/relationship.service';
import { ResourcePolicyService } from './resource-policy/resource-policy.service'; import { ResourcePolicyService } from './resource-policy/resource-policy.service';
@@ -145,6 +140,8 @@ import { Version } from './shared/version.model';
import { VersionHistory } from './shared/version-history.model'; import { VersionHistory } from './shared/version-history.model';
import { WorkflowActionDataService } from './data/workflow-action-data.service'; import { WorkflowActionDataService } from './data/workflow-action-data.service';
import { WorkflowAction } from './tasks/models/workflow-action-object.model'; import { WorkflowAction } from './tasks/models/workflow-action-object.model';
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
import { MetadataFieldDataService } from './data/metadata-field-data.service';
/** /**
* When not in production, endpoint responses can be mocked for testing purposes * When not in production, endpoint responses can be mocked for testing purposes
@@ -201,9 +198,6 @@ const PROVIDERS = [
FacetValueResponseParsingService, FacetValueResponseParsingService,
FacetValueMapResponseParsingService, FacetValueMapResponseParsingService,
FacetConfigResponseParsingService, FacetConfigResponseParsingService,
RegistryMetadataschemasResponseParsingService,
RegistryMetadatafieldsResponseParsingService,
RegistryBitstreamformatsResponseParsingService,
MappedCollectionsReponseParsingService, MappedCollectionsReponseParsingService,
DebugResponseParsingService, DebugResponseParsingService,
SearchResponseParsingService, SearchResponseParsingService,
@@ -223,8 +217,6 @@ const PROVIDERS = [
JsonPatchOperationsBuilder, JsonPatchOperationsBuilder,
AuthorityService, AuthorityService,
IntegrationResponseParsingService, IntegrationResponseParsingService,
MetadataschemaParsingService,
MetadatafieldParsingService,
UploaderService, UploaderService,
UUIDService, UUIDService,
NotificationsService, NotificationsService,
@@ -264,6 +256,8 @@ const PROVIDERS = [
LicenseDataService, LicenseDataService,
ItemTypeDataService, ItemTypeDataService,
WorkflowActionDataService, WorkflowActionDataService,
MetadataSchemaDataService,
MetadataFieldDataService,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,

View File

@@ -45,11 +45,12 @@ import {
FindListOptions, FindListOptions,
FindListRequest, FindListRequest,
GetRequest, GetRequest,
PatchRequest PatchRequest, PutRequest
} from './request.models'; } from './request.models';
import { RequestEntry } from './request.reducer'; import { RequestEntry } from './request.reducer';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { RestRequestMethod } from './rest-request-method'; import { RestRequestMethod } from './rest-request-method';
import { GenericConstructor } from '../shared/generic-constructor';
export abstract class DataService<T extends CacheableObject> { export abstract class DataService<T extends CacheableObject> {
protected abstract requestService: RequestService; protected abstract requestService: RequestService;
@@ -343,7 +344,9 @@ export abstract class DataService<T extends CacheableObject> {
tap((href: string) => { tap((href: string) => {
this.requestService.removeByHrefSubstring(href); this.requestService.removeByHrefSubstring(href);
const request = new FindListRequest(this.requestService.generateRequestId(), href, options); const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
request.responseMsToLive = 10 * 1000; if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.configure(request); this.requestService.configure(request);
} }
@@ -381,6 +384,28 @@ export abstract class DataService<T extends CacheableObject> {
); );
} }
/**
* Send a PUT request for the specified object
*
* @param object The object to send a put request for.
*/
put(object: T): Observable<RemoteData<T>> {
const requestId = this.requestService.generateRequestId();
const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
const request = new PutRequest(requestId, object._links.self.href, serializedObject);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.configure(request);
return this.requestService.getByUUID(requestId).pipe(
find((re: RequestEntry) => hasValue(re) && re.completed),
switchMap(() => this.findByHref(object._links.self.href))
);
}
/** /**
* Add a new patch to the object cache * Add a new patch to the object cache
* The patch is derived from the differences between the given object and its version in the object cache * The patch is derived from the differences between the given object and its version in the object cache

View File

@@ -0,0 +1,114 @@
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { RestResponse } from '../cache/response.models';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { CreateRequest, FindListOptions, PutRequest } from './request.models';
import { MetadataFieldDataService } from './metadata-field-data.service';
import { MetadataField } from '../metadata/metadata-field.model';
import { MetadataSchema } from '../metadata/metadata-schema.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { RequestParam } from '../cache/models/request-param.model';
describe('MetadataFieldDataService', () => {
let metadataFieldService: MetadataFieldDataService;
let requestService: RequestService;
let halService: HALEndpointService;
let notificationsService: NotificationsService;
let schema: MetadataSchema;
let rdbService: RemoteDataBuildService;
const endpoint = 'api/metadatafield/endpoint';
function init() {
schema = Object.assign(new MetadataSchema(), {
prefix: 'dc',
namespace: 'namespace',
_links: {
self: { href: 'selflink' }
}
});
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
configure: {},
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
removeByHrefSubstring: {}
});
halService = Object.assign(new HALEndpointServiceStub(endpoint));
notificationsService = jasmine.createSpyObj('notificationsService', {
error: {}
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: createSuccessfulRemoteDataObject$(undefined)
});
metadataFieldService = new MetadataFieldDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService);
}
beforeEach(() => {
init();
});
describe('findBySchema', () => {
beforeEach(() => {
spyOn(metadataFieldService, 'searchBy');
});
it('should call searchBy with the correct arguments', () => {
metadataFieldService.findBySchema(schema);
const expectedOptions = Object.assign(new FindListOptions(), {
searchParams: [new RequestParam('schema', schema.prefix)]
});
expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions);
});
});
describe('createOrUpdateMetadataField', () => {
let field: MetadataField;
beforeEach(() => {
field = Object.assign(new MetadataField(), {
element: 'identifier',
qualifier: undefined,
schema: schema,
_links: {
self: { href: 'selflink' }
}
});
});
describe('called with a new metadata field', () => {
it('should send a CreateRequest', (done) => {
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest));
done();
});
});
});
describe('called with an existing metadata field', () => {
beforeEach(() => {
field = Object.assign(field, {
id: 'id-of-existing-field'
});
});
it('should send a PutRequest', (done) => {
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest));
done();
});
});
});
});
describe('clearRequests', () => {
it('should remove requests on the data service\'s endpoint', (done) => {
metadataFieldService.clearRequests().subscribe(() => {
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataFieldService as any).linkPath}`);
done();
});
});
});
});

View File

@@ -0,0 +1,89 @@
import { Injectable } from '@angular/core';
import { dataService } from '../cache/builders/build-decorators';
import { DataService } from './data.service';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { HttpClient } from '@angular/common/http';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
import { MetadataField } from '../metadata/metadata-field.model';
import { MetadataSchema } from '../metadata/metadata-schema.model';
import { FindListOptions, FindListRequest } from './request.models';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { Observable } from 'rxjs/internal/Observable';
import { hasValue } from '../../shared/empty.util';
import { find, skipWhile, switchMap, tap } from 'rxjs/operators';
import { RemoteData } from './remote-data';
import { RequestParam } from '../cache/models/request-param.model';
import { PaginatedList } from './paginated-list';
/**
* A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint
*/
@Injectable()
@dataService(METADATA_FIELD)
export class MetadataFieldDataService extends DataService<MetadataField> {
protected linkPath = 'metadatafields';
protected searchBySchemaLinkPath = 'bySchema';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected halService: HALEndpointService,
protected objectCache: ObjectCacheService,
protected comparator: DefaultChangeAnalyzer<MetadataField>,
protected http: HttpClient,
protected notificationsService: NotificationsService) {
super();
}
/**
* Find metadata fields belonging to a metadata schema
* @param schema The metadata schema to list fields for
* @param options The options info used to retrieve the fields
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>) {
const optionsWithSchema = Object.assign(new FindListOptions(), options, {
searchParams: [new RequestParam('schema', schema.prefix)]
});
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
}
/**
* 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 CreateRequest is used
* - On update, a PutRequest is used
* @param field The MetadataField to create or update
*/
createOrUpdateMetadataField(field: MetadataField): Observable<RemoteData<MetadataField>> {
const isUpdate = hasValue(field.id);
if (isUpdate) {
return this.put(field);
} else {
return this.create(field, new RequestParam('schemaId', field.schema.id));
}
}
/**
* Clear all metadata field requests
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
*/
clearRequests(): Observable<string> {
return this.getBrowseEndpoint().pipe(
tap((href: string) => {
this.requestService.removeByHrefSubstring(href);
})
);
}
}

View File

@@ -0,0 +1,89 @@
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { MetadataSchemaDataService } from './metadata-schema-data.service';
import { of as observableOf } from 'rxjs';
import { RestResponse } from '../cache/response.models';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { MetadataSchema } from '../metadata/metadata-schema.model';
import { CreateRequest, PutRequest } from './request.models';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
describe('MetadataSchemaDataService', () => {
let metadataSchemaService: MetadataSchemaDataService;
let requestService: RequestService;
let halService: HALEndpointService;
let notificationsService: NotificationsService;
let rdbService: RemoteDataBuildService;
const endpoint = 'api/metadataschema/endpoint';
function init() {
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
configure: {},
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
removeByHrefSubstring: {}
});
halService = Object.assign(new HALEndpointServiceStub(endpoint));
notificationsService = jasmine.createSpyObj('notificationsService', {
error: {}
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: createSuccessfulRemoteDataObject$(undefined)
});
metadataSchemaService = new MetadataSchemaDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService);
}
beforeEach(() => {
init();
});
describe('createOrUpdateMetadataSchema', () => {
let schema: MetadataSchema;
beforeEach(() => {
schema = Object.assign(new MetadataSchema(), {
prefix: 'dc',
namespace: 'namespace',
_links: {
self: { href: 'selflink' }
}
});
});
describe('called with a new metadata schema', () => {
it('should send a CreateRequest', (done) => {
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest));
done();
});
});
});
describe('called with an existing metadata schema', () => {
beforeEach(() => {
schema = Object.assign(schema, {
id: 'id-of-existing-schema'
});
});
it('should send a PutRequest', (done) => {
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest));
done();
});
});
});
});
describe('clearRequests', () => {
it('should remove requests on the data service\'s endpoint', (done) => {
metadataSchemaService.clearRequests().subscribe(() => {
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataSchemaService as any).linkPath}`);
done();
});
});
});
});

View File

@@ -9,37 +9,21 @@ import { CoreState } from '../core.reducers';
import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataSchema } from '../metadata/metadata-schema.model';
import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type'; import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ChangeAnalyzer } from './change-analyzer';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { Observable } from 'rxjs/internal/Observable';
/* tslint:disable:max-classes-per-file */ import { hasValue } from '../../shared/empty.util';
class DataServiceImpl extends DataService<MetadataSchema> { import { tap } from 'rxjs/operators';
protected linkPath = 'metadataschemas'; import { RemoteData } from './remote-data';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: ChangeAnalyzer<MetadataSchema>) {
super();
}
}
/** /**
* A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint * A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint
*/ */
@Injectable() @Injectable()
@dataService(METADATA_SCHEMA) @dataService(METADATA_SCHEMA)
export class MetadataSchemaDataService { export class MetadataSchemaDataService extends DataService<MetadataSchema> {
private dataService: DataServiceImpl; protected linkPath = 'metadataschemas';
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
@@ -50,6 +34,35 @@ export class MetadataSchemaDataService {
protected comparator: DefaultChangeAnalyzer<MetadataSchema>, protected comparator: DefaultChangeAnalyzer<MetadataSchema>,
protected http: HttpClient, protected http: HttpClient,
protected notificationsService: NotificationsService) { protected notificationsService: NotificationsService) {
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); super();
} }
/**
* 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 CreateRequest is used
* - On update, a PutRequest is used
* @param schema The MetadataSchema to create or update
*/
createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<RemoteData<MetadataSchema>> {
const isUpdate = hasValue(schema.id);
if (isUpdate) {
return this.put(schema);
} else {
return this.create(schema);
}
}
/**
* Clear all metadata schema requests
* Used for refreshing lists after adding/updating/removing a metadata schema in the registry
*/
clearRequests(): Observable<string> {
return this.getBrowseEndpoint().pipe(
tap((href: string) => this.requestService.removeByHrefSubstring(href))
);
}
} }

View File

@@ -1,22 +0,0 @@
import { Injectable } from '@angular/core';
import { MetadatafieldSuccessResponse, RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
import { MetadataField } from '../metadata/metadata-field.model';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
/**
* 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 DSpaceSerializer(MetadataField).deserialize(payload);
return new MetadatafieldSuccessResponse(deserialized, data.statusCode, data.statusText);
}
}

View File

@@ -1,19 +0,0 @@
import { Injectable } from '@angular/core';
import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
import { MetadataSchema } from '../metadata/metadata-schema.model';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
@Injectable()
export class MetadataschemaParsingService implements ResponseParsingService {
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const payload = data.payload;
const deserialized = new DSpaceSerializer(MetadataSchema).deserialize(payload);
return new MetadataschemaSuccessResponse(deserialized, data.statusCode, data.statusText);
}
}

View File

@@ -1,41 +0,0 @@
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);
});
});

View File

@@ -1,25 +0,0 @@
import { Injectable } from '@angular/core';
import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
@Injectable()
export class RegistryBitstreamformatsResponseParsingService implements ResponseParsingService {
constructor(private dsoParser: DSOResponseParsingService) {
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const payload = data.payload;
const bitstreamformats = payload._embedded.bitstreamformats;
payload.bitstreamformats = bitstreamformats;
const deserialized = new DSpaceSerializer(RegistryBitstreamformatsResponse).deserialize(payload);
return new RegistryBitstreamformatsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload.page));
}
}

View File

@@ -1,68 +0,0 @@
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);
});
});

View File

@@ -1,34 +0,0 @@
import { Injectable } from '@angular/core';
import { hasValue } from '../../shared/empty.util';
import { RegistryMetadatafieldsSuccessResponse, RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
@Injectable()
export class RegistryMetadatafieldsResponseParsingService implements ResponseParsingService {
constructor(private dsoParser: DSOResponseParsingService) {
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const payload = data.payload;
let metadatafields = [];
if (hasValue(payload._embedded)) {
metadatafields = payload._embedded.metadatafields;
metadatafields.forEach((field) => {
field.schema = field._embedded.schema;
});
}
payload.metadatafields = metadatafields;
const deserialized = new DSpaceSerializer(RegistryMetadatafieldsResponse).deserialize(payload);
return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload));
}
}

View File

@@ -1,50 +0,0 @@
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);
});
});

View File

@@ -1,29 +0,0 @@
import { Injectable } from '@angular/core';
import { hasValue } from '../../shared/empty.util';
import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
@Injectable()
export class RegistryMetadataschemasResponseParsingService implements ResponseParsingService {
constructor(private dsoParser: DSOResponseParsingService) {
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const payload = data.payload;
let metadataschemas = [];
if (hasValue(payload._embedded)) {
metadataschemas = payload._embedded.metadataschemas;
}
payload.metadataschemas = metadataschemas;
const deserialized = new DSpaceSerializer(RegistryMetadataschemasResponse).deserialize(payload);
return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload));
}
}

View File

@@ -14,8 +14,6 @@ import { RestRequestMethod } from './rest-request-method';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service'; import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service'; import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
import { MetadataschemaParsingService } from './metadataschema-parsing.service';
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service'; import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service'; import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
@@ -251,58 +249,6 @@ 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;
}
}
/** /**
* Class representing a submission HTTP GET request object * Class representing a submission HTTP GET request object
*/ */

View File

@@ -1,24 +0,0 @@
import { autoserialize, deserialize } from 'cerialize';
import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type';
import { HALLink } from '../shared/hal-link.model';
import { PageInfo } from '../shared/page-info.model';
import { BitstreamFormat } from '../shared/bitstream-format.model';
import { link } from '../cache/builders/build-decorators';
export class RegistryBitstreamformatsResponse {
@autoserialize
page: PageInfo;
/**
* The {@link HALLink}s for this RegistryBitstreamformatsResponse
*/
@deserialize
_links: {
self: HALLink;
bitstreamformats: HALLink;
};
@link(BITSTREAM_FORMAT)
bitstreamformats?: BitstreamFormat[];
}

View File

@@ -1,46 +0,0 @@
import { autoserialize, deserialize } from 'cerialize';
import { typedObject } from '../cache/builders/build-decorators';
import { MetadataField } from '../metadata/metadata-field.model';
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
import { HALLink } from '../shared/hal-link.model';
import { PageInfo } from '../shared/page-info.model';
import { ResourceType } from '../shared/resource-type';
import { excludeFromEquals } from '../utilities/equals.decorators';
/**
* Class that represents a response with a registry's metadata fields
*/
@typedObject
export class RegistryMetadatafieldsResponse {
static type = METADATA_FIELD;
/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
/**
* List of metadata fields in the response
*/
@deserialize
metadatafields: MetadataField[];
/**
* Page info of this response
*/
@autoserialize
page: PageInfo;
/**
* The REST link to this response
*/
@autoserialize
self: string;
@deserialize
_links: {
self: HALLink,
}
}

View File

@@ -1,14 +0,0 @@
import { PageInfo } from '../shared/page-info.model';
import { autoserialize, deserialize } from 'cerialize';
import { MetadataSchema } from '../metadata/metadata-schema.model';
export class RegistryMetadataschemasResponse {
@deserialize
metadataschemas: MetadataSchema[];
@autoserialize
page: PageInfo;
@autoserialize
self: string;
}

View File

@@ -3,8 +3,7 @@ import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { import {
MetadataRegistryCancelFieldAction, MetadataRegistryCancelFieldAction,
MetadataRegistryCancelSchemaAction, MetadataRegistryCancelSchemaAction,
@@ -17,30 +16,20 @@ import {
MetadataRegistrySelectFieldAction, MetadataRegistrySelectFieldAction,
MetadataRegistrySelectSchemaAction MetadataRegistrySelectSchemaAction
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { StoreMock } from '../../shared/testing/store.mock'; import { StoreMock } from '../../shared/testing/store.mock';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { import { RestResponse } from '../cache/response.models';
RegistryMetadatafieldsSuccessResponse,
RegistryMetadataschemasSuccessResponse,
RestResponse
} from '../cache/response.models';
import { RemoteData } from '../data/remote-data';
import { RequestEntry } from '../data/request.reducer';
import { RequestService } from '../data/request.service';
import { MetadataField } from '../metadata/metadata-field.model'; import { MetadataField } from '../metadata/metadata-field.model';
import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataSchema } from '../metadata/metadata-schema.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { PageInfo } from '../shared/page-info.model';
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
import { RegistryService } from './registry.service'; import { RegistryService } from './registry.service';
import { storeModuleConfig } from '../../app.reducer'; import { storeModuleConfig } from '../../app.reducer';
import { FindListOptions } from '../data/request.models';
import { MetadataSchemaDataService } from '../data/metadata-schema-data.service';
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test';
@Component({ template: '' }) @Component({ template: '' })
class DummyComponent { class DummyComponent {
@@ -49,211 +38,169 @@ class DummyComponent {
describe('RegistryService', () => { describe('RegistryService', () => {
let registryService: RegistryService; let registryService: RegistryService;
let mockStore; let mockStore;
const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { let metadataSchemaService: MetadataSchemaDataService;
id: 'registry-service-spec-pagination', let metadataFieldService: MetadataFieldDataService;
pageSize: 20
});
const mockSchemasList = [ let options: FindListOptions;
Object.assign(new MetadataSchema(), { let mockSchemasList: MetadataSchema[];
id: 1, let mockFieldsList: MetadataField[];
_links: {
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1' } function init() {
}, options = Object.assign(new FindListOptions(), {
prefix: 'dc', currentPage: 1,
namespace: 'http://dublincore.org/documents/dcmi-terms/', elementsPerPage: 20
type: MetadataSchema.type });
}),
Object.assign(new MetadataSchema(), { mockSchemasList = [
id: 2, Object.assign(new MetadataSchema(), {
_links: {
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2' }
},
prefix: 'mock',
namespace: 'http://dspace.org/mockschema',
type: MetadataSchema.type
})
];
const mockFieldsList = [
Object.assign(new MetadataField(),
{
id: 1, id: 1,
_links: { _links: {
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8' } self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1' }
}, },
element: 'contributor', prefix: 'dc',
qualifier: 'advisor', namespace: 'http://dublincore.org/documents/dcmi-terms/',
scopeNote: null, type: MetadataSchema.type
schema: mockSchemasList[0],
type: MetadataField.type
}), }),
Object.assign(new MetadataField(), Object.assign(new MetadataSchema(), {
{
id: 2, id: 2,
_links: { _links: {
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9' } self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2' }
}, },
element: 'contributor', prefix: 'mock',
qualifier: 'author', namespace: 'http://dspace.org/mockschema',
scopeNote: null, type: MetadataSchema.type
schema: mockSchemasList[0],
type: MetadataField.type
}),
Object.assign(new MetadataField(),
{
id: 3,
_links: {
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10' }
},
element: 'contributor',
qualifier: 'editor',
scopeNote: 'test scope note',
schema: mockSchemasList[1],
type: MetadataField.type
}),
Object.assign(new MetadataField(),
{
id: 4,
_links: {
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11' }
},
element: 'contributor',
qualifier: 'illustrator',
scopeNote: null,
schema: mockSchemasList[1],
type: MetadataField.type
}) })
]; ];
const pageInfo = new PageInfo(); mockFieldsList = [
pageInfo.elementsPerPage = 20; Object.assign(new MetadataField(),
pageInfo.currentPage = 1; {
id: 1,
const endpoint = 'path'; _links: {
const endpointWithParams = `${endpoint}?size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`; self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8' }
const fieldEndpointWithParams = `${endpoint}?schema=${mockSchemasList[0].prefix}&size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`; },
element: 'contributor',
const halServiceStub = { qualifier: 'advisor',
getEndpoint: (link: string) => observableOf(endpoint) scopeNote: null,
}; schema: mockSchemasList[0],
type: MetadataField.type
const rdbStub = { }),
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => { Object.assign(new MetadataField(),
return observableCombineLatest(requestEntryObs, {
payloadObs).pipe(map(([req, pay]) => { id: 2,
return { req, pay }; _links: {
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9' }
},
element: 'contributor',
qualifier: 'author',
scopeNote: null,
schema: mockSchemasList[0],
type: MetadataField.type
}),
Object.assign(new MetadataField(),
{
id: 3,
_links: {
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10' }
},
element: 'contributor',
qualifier: 'editor',
scopeNote: 'test scope note',
schema: mockSchemasList[1],
type: MetadataField.type
}),
Object.assign(new MetadataField(),
{
id: 4,
_links: {
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11' }
},
element: 'contributor',
qualifier: 'illustrator',
scopeNote: null,
schema: mockSchemasList[1],
type: MetadataField.type
}) })
); ];
},
aggregate: (input: Array<Observable<RemoteData<any>>>): Observable<RemoteData<any[]>> => { metadataSchemaService = jasmine.createSpyObj('metadataSchemaService', {
return createSuccessfulRemoteDataObject$([]); findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)),
} findById: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
}; createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
clearRequests: observableOf('href')
});
metadataFieldService = jasmine.createSpyObj('metadataFieldService', {
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockFieldsList)),
findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
createOrUpdateMetadataField: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
clearRequests: observableOf('href')
});
}
beforeEach(() => { beforeEach(() => {
init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot()], imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot()],
declarations: [ declarations: [
DummyComponent DummyComponent
], ],
providers: [ providers: [
{ provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: rdbStub },
{ provide: HALEndpointService, useValue: halServiceStub },
{ provide: Store, useClass: StoreMock }, { provide: Store, useClass: StoreMock },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: MetadataSchemaDataService, useValue: metadataSchemaService },
{ provide: MetadataFieldDataService, useValue: metadataFieldService },
RegistryService RegistryService
] ]
}); });
registryService = TestBed.get(RegistryService); registryService = TestBed.get(RegistryService);
mockStore = TestBed.get(Store); mockStore = TestBed.get(Store);
spyOn((registryService as any).halService, 'getEndpoint').and.returnValue(observableOf(endpoint));
}); });
describe('when requesting metadataschemas', () => { describe('when requesting metadataschemas', () => {
const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), { let result;
metadataschemas: mockSchemasList,
page: pageInfo
});
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => { beforeEach(() => {
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); result = registryService.getMetadataSchemas(options);
/* tslint:disable:no-empty */ });
registryService.getMetadataSchemas(pagination).subscribe((value) => {
it('should call metadataSchemaService.findAll', (done) => {
result.subscribe(() => {
expect(metadataSchemaService.findAll).toHaveBeenCalled();
done();
}); });
/* tslint:enable:no-empty */
});
it('should call getEndpoint on the halService', () => {
expect((registryService as any).halService.getEndpoint).toHaveBeenCalled();
});
it('should send out the request on the request service', () => {
expect((registryService as any).requestService.configure).toHaveBeenCalled();
});
it('should call getByHref on the request service with the correct request url', () => {
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams);
}); });
}); });
describe('when requesting metadataschema by name', () => { describe('when requesting metadataschema by name', () => {
const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), { let result;
metadataschemas: mockSchemasList,
page: pageInfo
});
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => { beforeEach(() => {
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); result = registryService.getMetadataSchemaByName(mockSchemasList[0].prefix);
/* tslint:disable:no-empty */ });
registryService.getMetadataSchemaByName(mockSchemasList[0].prefix).subscribe((value) => {
it('should call metadataSchemaService.findById with the correct ID', (done) => {
result.subscribe(() => {
expect(metadataSchemaService.findById).toHaveBeenCalledWith(`${mockSchemasList[0].id}`);
done();
}); });
/* tslint:enable:no-empty */
});
it('should call getEndpoint on the halService', () => {
expect((registryService as any).halService.getEndpoint).toHaveBeenCalled();
});
it('should send out the request on the request service', () => {
expect((registryService as any).requestService.configure).toHaveBeenCalled();
});
it('should call getByHref on the request service with the correct request url', () => {
expect((registryService as any).requestService.getByHref.calls.argsFor(0)[0]).toContain(endpoint);
}); });
}); });
describe('when requesting metadatafields', () => { describe('when requesting metadatafields', () => {
const queryResponse = Object.assign(new RegistryMetadatafieldsResponse(), { let result;
metadatafields: mockFieldsList,
page: pageInfo
});
const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, 200, 'OK', pageInfo);
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => { beforeEach(() => {
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); result = registryService.getAllMetadataFields();
/* tslint:disable:no-empty */ });
registryService.getMetadataFieldsBySchema(mockSchemasList[0], pagination).subscribe((value) => {
it('should call metadataFieldService.findAll', (done) => {
result.subscribe(() => {
expect(metadataFieldService.findAll).toHaveBeenCalled();
done();
}); });
/* tslint:enable:no-empty */
});
it('should call getEndpoint on the halService', () => {
expect((registryService as any).halService.getEndpoint).toHaveBeenCalled();
});
it('should send out the request on the request service', () => {
expect((registryService as any).requestService.configure).toHaveBeenCalled();
});
it('should call getByHref on the request service with the correct request url', () => {
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(fieldEndpointWithParams);
}); });
}); });
@@ -370,9 +317,10 @@ describe('RegistryService', () => {
result = registryService.createOrUpdateMetadataSchema(mockSchemasList[0]); result = registryService.createOrUpdateMetadataSchema(mockSchemasList[0]);
}); });
it('should return the created/updated metadata schema', () => { it('should return the created/updated metadata schema', (done) => {
result.subscribe((schema: MetadataSchema) => { result.subscribe((schema: MetadataSchema) => {
expect(schema).toEqual(mockSchemasList[0]); expect(schema).toEqual(mockSchemasList[0]);
done();
}); });
}); });
}); });
@@ -384,9 +332,10 @@ describe('RegistryService', () => {
result = registryService.createOrUpdateMetadataField(mockFieldsList[0]); result = registryService.createOrUpdateMetadataField(mockFieldsList[0]);
}); });
it('should return the created/updated metadata field', () => { it('should return the created/updated metadata field', (done) => {
result.subscribe((field: MetadataField) => { result.subscribe((field: MetadataField) => {
expect(field).toEqual(mockFieldsList[0]); expect(field).toEqual(mockFieldsList[0]);
done();
}); });
}); });
}); });
@@ -425,7 +374,7 @@ describe('RegistryService', () => {
}); });
it('should remove the requests related to metadata schemas from cache', () => { it('should remove the requests related to metadata schemas from cache', () => {
expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled(); expect(metadataSchemaService.clearRequests).toHaveBeenCalled();
}); });
}); });
@@ -435,7 +384,7 @@ describe('RegistryService', () => {
}); });
it('should remove the requests related to metadata fields from cache', () => { it('should remove the requests related to metadata fields from cache', () => {
expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled(); expect(metadataFieldService.clearRequests).toHaveBeenCalled();
}); });
}); });
}); });

View File

@@ -2,37 +2,18 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { import { FindListOptions } from '../data/request.models';
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';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
import { import {
MetadatafieldSuccessResponse, MetadatafieldSuccessResponse,
MetadataschemaSuccessResponse, MetadataschemaSuccessResponse,
RegistryMetadatafieldsSuccessResponse,
RegistryMetadataschemasSuccessResponse,
RestResponse RestResponse
} from '../cache/response.models'; } from '../cache/response.models';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service'; import { hasNoValue, hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../shared/operators';
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { URLCombiner } from '../url-combiner/url-combiner';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { configureRequest, getResponseFromEntry } from '../shared/operators';
import { createSelector, select, Store } from '@ngrx/store'; import { createSelector, select, Store } from '@ngrx/store';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers'; import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
@@ -48,15 +29,14 @@ import {
MetadataRegistrySelectFieldAction, MetadataRegistrySelectFieldAction,
MetadataRegistrySelectSchemaAction MetadataRegistrySelectSchemaAction
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
import { distinctUntilChanged, flatMap, map, take, tap } from 'rxjs/operators'; import { flatMap, map, tap } from 'rxjs/operators';
import { NotificationsService } from '../../shared/notifications/notifications.service'; 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'; import { TranslateService } from '@ngx-translate/core';
import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataSchema } from '../metadata/metadata-schema.model';
import { MetadataField } from '../metadata/metadata-field.model'; import { MetadataField } from '../metadata/metadata-field.model';
import { getClassForType } from '../cache/builders/build-decorators'; import { MetadataSchemaDataService } from '../data/metadata-schema-data.service';
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry; const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema); const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
@@ -70,221 +50,64 @@ const selectedMetadataFieldsSelector = createSelector(metadataRegistryStateSelec
@Injectable() @Injectable()
export class RegistryService { export class RegistryService {
private metadataSchemasPath = 'metadataschemas'; constructor(private store: Store<AppState>,
private metadataFieldsPath = 'metadatafields';
// private bitstreamFormatsPath = 'bitstreamformats';
constructor(protected requestService: RequestService,
private rdb: RemoteDataBuildService,
private halService: HALEndpointService,
private store: Store<AppState>,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private translateService: TranslateService) { private translateService: TranslateService,
private metadataSchemaService: MetadataSchemaDataService,
private metadataFieldService: MetadataFieldDataService) {
} }
/** /**
* Retrieves all metadata schemas * Retrieves all metadata schemas
* @param pagination The pagination info used to retrieve the schemas * @param options The options used to retrieve the schemas
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/ */
public getMetadataSchemas(pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataSchema>>> { public getMetadataSchemas(options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataSchema>>): Observable<RemoteData<PaginatedList<MetadataSchema>>> {
const requestObs = this.getMetadataSchemasRequestObs(pagination); return this.metadataSchemaService.findAll(options, ...linksToFollow);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
const rmrObs: Observable<RegistryMetadataschemasResponse> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse)
);
const metadataschemasObs: Observable<MetadataSchema[]> = rmrObs.pipe(
map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas)
);
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: RegistryMetadataschemasSuccessResponse) => response.pageInfo)
);
const payloadObs = observableCombineLatest(metadataschemasObs, pageInfoObs).pipe(
map(([metadataschemas, pageInfo]) => {
return new PaginatedList(pageInfo, metadataschemas);
})
);
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
} }
/** /**
* Retrieves a metadata schema by its name * Retrieves a metadata schema by its name
* @param schemaName The name of the schema to find * @param schemaName The name of the schema to find
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/ */
public getMetadataSchemaByName(schemaName: string): Observable<RemoteData<MetadataSchema>> { public getMetadataSchemaByName(schemaName: string, ...linksToFollow: Array<FollowLinkConfig<MetadataSchema>>): Observable<RemoteData<MetadataSchema>> {
// Temporary pagination to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema // Temporary options to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema
const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { const options: FindListOptions = Object.assign(new FindListOptions(), {
id: 'all-metadatafields-pagination', elementsPerPage: 10000
pageSize: 10000
}); });
const requestObs = this.getMetadataSchemasRequestObs(pagination); return this.getMetadataSchemas(options).pipe(
getFirstSucceededRemoteDataPayload(),
const requestEntryObs = requestObs.pipe( map((schemas: PaginatedList<MetadataSchema>) => schemas.page),
flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) isNotEmptyOperator(),
map((schemas: MetadataSchema[]) => schemas.filter((schema) => schema.prefix === schemaName)[0]),
flatMap((schema: MetadataSchema) => this.metadataSchemaService.findById(`${schema.id}`, ...linksToFollow))
); );
const rmrObs: Observable<RegistryMetadataschemasResponse> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse)
);
const metadataschemaObs: Observable<MetadataSchema> = rmrObs.pipe(
map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas),
map((metadataSchemas: MetadataSchema[]) => metadataSchemas.filter((value) => value.prefix === schemaName)[0])
);
return this.rdb.toRemoteDataObservable(requestEntryObs, metadataschemaObs);
} }
/** /**
* retrieves all metadata fields that belong to a certain metadata schema * retrieves all metadata fields that belong to a certain metadata schema
* @param schema The schema to filter by * @param schema The schema to filter by
* @param pagination The pagination info used to retrieve the fields * @param options The options info used to retrieve the fields
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/ */
public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> { public getMetadataFieldsBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
const requestObs = this.getMetadataFieldsBySchemaRequestObs(pagination, schema); return this.metadataFieldService.findBySchema(schema, options, ...linksToFollow);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
const rmrObs: Observable<RegistryMetadatafieldsResponse> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse)
);
const metadatafieldsObs: Observable<MetadataField[]> = rmrObs.pipe(
map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields)
);
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo)
);
const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe(
map(([metadatafields, pageInfo]) => {
return new PaginatedList(pageInfo, metadatafields);
})
);
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
} }
/** /**
* Retrieve all existing metadata fields as a paginated list * Retrieve all existing metadata fields as a paginated list
* @param pagination Pagination options to determine which page of metadata fields should be requested * @param options Options to determine which page of metadata fields should be requested
* When no pagination is provided, all metadata fields are requested in one large page * When no options are provided, all metadata fields are requested in one large page
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
* @returns an observable that emits a remote data object with a page of metadata fields * @returns an observable that emits a remote data object with a page of metadata fields
*/ */
public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> { public getAllMetadataFields(options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
if (hasNoValue(pagination)) { if (hasNoValue(options)) {
pagination = {currentPage: 1, pageSize: 10000} as any; options = {currentPage: 1, elementsPerPage: 10000} as any;
} }
const requestObs = this.getMetadataFieldsRequestObs(pagination); return this.metadataFieldService.findAll(options, ...linksToFollow);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
const rmrObs: Observable<RegistryMetadatafieldsResponse> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse)
);
const metadatafieldsObs: Observable<MetadataField[]> = rmrObs.pipe(
map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields),
/* Make sure to explicitly cast this into a MetadataField object, on first page loads this object comes from the object cache created by the server and its prototype is unknown */
map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => Object.assign(new MetadataField(), metadataField)))
);
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo)
);
const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe(
map(([metadatafields, pageInfo]) => {
return new PaginatedList(pageInfo, metadatafields);
})
);
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
}
public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
map((url: string) => {
const args: string[] = [];
args.push(`size=${pagination.pageSize}`);
args.push(`page=${pagination.currentPage - 1}`);
if (isNotEmpty(args)) {
url = new URLCombiner(url, `?${args.join('&')}`).toString();
}
const request = new GetRequest(this.requestService.generateRequestId(), url);
return Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return RegistryMetadataschemasResponseParsingService;
}
});
}),
tap((request: RestRequest) => this.requestService.configure(request)),
);
}
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)) {
url = new URLCombiner(url, `?${args.join('&')}`).toString();
}
const request = new GetRequest(this.requestService.generateRequestId(), url);
return Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return RegistryMetadatafieldsResponseParsingService;
}
});
}),
tap((request: RestRequest) => this.requestService.configure(request)),
);
}
private getMetadataFieldsRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
map((url: string) => {
const args: string[] = [];
args.push(`size=${pagination.pageSize}`);
args.push(`page=${pagination.currentPage - 1}`);
if (isNotEmpty(args)) {
url = new URLCombiner(url, `?${args.join('&')}`).toString();
}
const request = new GetRequest(this.requestService.generateRequestId(), url);
return Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return RegistryMetadatafieldsResponseParsingService;
}
});
}),
tap((request: RestRequest) => this.requestService.configure(request)),
);
} }
public editMetadataSchema(schema: MetadataSchema) { public editMetadataSchema(schema: MetadataSchema) {
@@ -386,59 +209,17 @@ export class RegistryService {
* Create or Update a MetadataSchema * Create or Update a MetadataSchema
* If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead * 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): * 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 creation, a CreateRequest is used
* - On update, a UpdateMetadataSchemaRequest is used * - On update, a PutRequest is used
* @param schema The MetadataSchema to create or update * @param schema The MetadataSchema to create or update
*/ */
public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<MetadataSchema> { public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<MetadataSchema> {
const isUpdate = hasValue(schema.id); const isUpdate = hasValue(schema.id);
const requestId = this.requestService.generateRequestId(); return this.metadataSchemaService.createOrUpdateMetadataSchema(schema).pipe(
const endpoint$ = this.halService.getEndpoint(this.metadataSchemasPath).pipe( getFirstSucceededRemoteDataPayload(),
isNotEmptyOperator(), hasValueOperator(),
map((endpoint: string) => (isUpdate ? `${endpoint}/${schema.id}` : endpoint)), tap(() => {
distinctUntilChanged() this.showNotifications(true, isUpdate, false, {prefix: schema.prefix});
);
const serializedSchema = new DSpaceSerializer(getClassForType(MetadataSchema.type)).serialize(schema);
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;
}
}) })
); );
} }
@@ -448,74 +229,32 @@ export class RegistryService {
* @param id The id of the metadata schema to delete * @param id The id of the metadata schema to delete
*/ */
public deleteMetadataSchema(id: number): Observable<RestResponse> { public deleteMetadataSchema(id: number): Observable<RestResponse> {
return this.delete(this.metadataSchemasPath, id); return this.metadataSchemaService.deleteAndReturnResponse(`${id}`);
} }
/** /**
* Method that clears a cached metadata schema request and returns its REST url * Method that clears a cached metadata schema request and returns its REST url
*/ */
public clearMetadataSchemaRequests(): Observable<string> { public clearMetadataSchemaRequests(): Observable<string> {
return this.halService.getEndpoint(this.metadataSchemasPath).pipe( return this.metadataSchemaService.clearRequests();
tap((href: string) => this.requestService.removeByHrefSubstring(href))
);
} }
/** /**
* Create or Update a MetadataField * Create or Update a MetadataField
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead * 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): * 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 creation, a CreateRequest is used
* - On update, a UpdateMetadataFieldRequest is used * - On update, a PutRequest is used
* @param field The MetadataField to create or update * @param field The MetadataField to create or update
*/ */
public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> { public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> {
const isUpdate = hasValue(field.id); const isUpdate = hasValue(field.id);
const requestId = this.requestService.generateRequestId(); return this.metadataFieldService.createOrUpdateMetadataField(field).pipe(
const endpoint$ = this.halService.getEndpoint(this.metadataFieldsPath).pipe( getFirstSucceededRemoteDataPayload(),
isNotEmptyOperator(), hasValueOperator(),
map((endpoint: string) => (isUpdate ? `${endpoint}/${field.id}` : `${endpoint}?schemaId=${field.schema.id}`)), tap(() => {
distinctUntilChanged() const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`;
); this.showNotifications(true, isUpdate, true, {field: fieldString});
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;
}
}) })
); );
} }
@@ -525,38 +264,13 @@ export class RegistryService {
* @param id The id of the metadata field to delete * @param id The id of the metadata field to delete
*/ */
public deleteMetadataField(id: number): Observable<RestResponse> { public deleteMetadataField(id: number): Observable<RestResponse> {
return this.delete(this.metadataFieldsPath, id); return this.metadataFieldService.deleteAndReturnResponse(`${id}`);
} }
/** /**
* Method that clears a cached metadata field request and returns its REST url * Method that clears a cached metadata field request and returns its REST url
*/ */
public clearMetadataFieldRequests(): Observable<string> { public clearMetadataFieldRequests(): Observable<string> {
return this.halService.getEndpoint(this.metadataFieldsPath).pipe( return this.metadataFieldService.clearRequests();
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) { private showNotifications(success: boolean, edited: boolean, isField: boolean, options: any) {

View File

@@ -22,6 +22,7 @@ import { NotificationsService } from '../../notifications/notifications.service'
import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { VarDirective } from '../../utils/var.directive'; import { VarDirective } from '../../utils/var.directive';
import { ComColFormComponent } from './comcol-form.component'; import { ComColFormComponent } from './comcol-form.component';
import { Operation } from 'fast-json-patch';
describe('ComColFormComponent', () => { describe('ComColFormComponent', () => {
let comp: ComColFormComponent<DSpaceObject>; let comp: ComColFormComponent<DSpaceObject>;
@@ -40,11 +41,8 @@ describe('ComColFormComponent', () => {
} }
}; };
const dcTitle = 'dc.title'; const dcTitle = 'dc.title';
const dcRandom = 'dc.random';
const dcAbstract = 'dc.description.abstract'; 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 abstractMD = { [dcAbstract]: [{ value: 'Community description', language: null }] };
const newTitleMD = { [dcTitle]: [{ value: 'New Community Title', language: null }] }; const newTitleMD = { [dcTitle]: [{ value: 'New Community Title', language: null }] };
const formModel = [ const formModel = [
@@ -112,33 +110,47 @@ describe('ComColFormComponent', () => {
}); });
it('should emit the new version of the community', () => { it('should emit the new version of the community', () => {
comp.dso = Object.assign( comp.dso = new Community();
new Community(),
{
metadata: {
...titleMD,
...randomMD
}
}
);
comp.onSubmit(); comp.onSubmit();
const operations: Operation[] = [
{
op: 'replace',
path: '/metadata/dc.title',
value: {
value: 'New Community Title',
language: null,
},
},
{
op: 'replace',
path: '/metadata/dc.description.abstract',
value: {
value: 'Community description',
language: null,
},
},
];
expect(comp.submitForm.emit).toHaveBeenCalledWith( expect(comp.submitForm.emit).toHaveBeenCalledWith(
{ {
dso: Object.assign( dso: Object.assign({}, comp.dso, {
{},
new Community(),
{
metadata: { metadata: {
...newTitleMD, 'dc.title': [{
...randomMD, value: 'New Community Title',
...abstractMD language: null,
}],
'dc.description.abstract': [{
value: 'Community description',
language: null,
}],
}, },
type: Community.type type: Community.type,
}, }
), ),
uploader: undefined, uploader: undefined,
deleteLogo: false deleteLogo: false,
operations: operations,
} }
); );
}) })
@@ -164,11 +176,6 @@ describe('ComColFormComponent', () => {
it('should emit finish', () => { it('should emit finish', () => {
expect(comp.finish.emit).toHaveBeenCalled(); expect(comp.finish.emit).toHaveBeenCalled();
}); });
it('should remove the object\'s cache', () => {
expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled();
expect(objectCacheStub.remove).toHaveBeenCalled();
});
}); });
describe('onUploadError', () => { describe('onUploadError', () => {
@@ -239,6 +246,11 @@ describe('ComColFormComponent', () => {
it('should display a success notification', () => { it('should display a success notification', () => {
expect(notificationsService.success).toHaveBeenCalled(); expect(notificationsService.success).toHaveBeenCalled();
}); });
it('should remove the object\'s cache', () => {
expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled();
expect(objectCacheStub.remove).toHaveBeenCalled();
});
}); });
describe('when dsoService.deleteLogo returns an error response', () => { describe('when dsoService.deleteLogo returns an error response', () => {

View File

@@ -25,6 +25,7 @@ import { hasValue, isNotEmpty } from '../../empty.util';
import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsService } from '../../notifications/notifications.service';
import { UploaderOptions } from '../../uploader/uploader-options.model'; import { UploaderOptions } from '../../uploader/uploader-options.model';
import { UploaderComponent } from '../../uploader/uploader.component'; import { UploaderComponent } from '../../uploader/uploader.component';
import { Operation } from 'fast-json-patch';
/** /**
* A form for creating and editing Communities or Collections * A form for creating and editing Communities or Collections
@@ -85,7 +86,8 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDe
@Output() submitForm: EventEmitter<{ @Output() submitForm: EventEmitter<{
dso: T, dso: T,
uploader: FileUploader, uploader: FileUploader,
deleteLogo: boolean deleteLogo: boolean,
operations: Operation[],
}> = new EventEmitter(); }> = new EventEmitter();
/** /**
@@ -189,9 +191,9 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDe
const formMetadata = {} as MetadataMap; const formMetadata = {} as MetadataMap;
this.formModel.forEach((fieldModel: DynamicInputModel) => { this.formModel.forEach((fieldModel: DynamicInputModel) => {
const value: MetadataValue = { const value: MetadataValue = {
value: fieldModel.value as string, value: fieldModel.value as string,
language: null language: null
} as any; } as any;
if (formMetadata.hasOwnProperty(fieldModel.name)) { if (formMetadata.hasOwnProperty(fieldModel.name)) {
formMetadata[fieldModel.name].push(value); formMetadata[fieldModel.name].push(value);
} else { } else {
@@ -206,10 +208,26 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDe
}, },
type: Community.type type: Community.type
}); });
const operations: Operation[] = [];
this.formModel.forEach((fieldModel: DynamicInputModel) => {
if (fieldModel.value !== this.dso.firstMetadataValue(fieldModel.name)) {
operations.push({
op: 'replace',
path: `/metadata/${fieldModel.name}`,
value: {
value: fieldModel.value,
language: null,
},
});
}
});
this.submitForm.emit({ this.submitForm.emit({
dso: updatedDSO, dso: updatedDSO,
uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined, uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined,
deleteLogo: this.markLogoForDeletion deleteLogo: this.markLogoForDeletion,
operations: operations,
}); });
} }
@@ -257,7 +275,9 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDe
* The request was successful, display a success notification * The request was successful, display a success notification
*/ */
public onCompleteItem() { public onCompleteItem() {
this.refreshCache(); if (hasValue(this.dso.id)) {
this.refreshCache();
}
this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success')); this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success'));
this.finish.emit(); this.finish.emit();
} }

View File

@@ -77,7 +77,8 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements
const uploader = event.uploader; const uploader = event.uploader;
this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => {
this.dsoDataService.create(dso, new RequestParam('parent', uuid)) const params = uuid ? [new RequestParam('parent', uuid)] : [];
this.dsoDataService.create(dso, ...params)
.pipe(getSucceededRemoteData()) .pipe(getSucceededRemoteData())
.subscribe((dsoRD: RemoteData<TDomain>) => { .subscribe((dsoRD: RemoteData<TDomain>) => {
if (isNotUndefined(dsoRD)) { if (isNotUndefined(dsoRD)) {

View File

@@ -13,13 +13,13 @@ import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NotificationsService } from '../../../notifications/notifications.service'; import { NotificationsService } from '../../../notifications/notifications.service';
import { SharedModule } from '../../../shared.module'; import { SharedModule } from '../../../shared.module';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils';
import { ComcolMetadataComponent } from './comcol-metadata.component'; import { ComcolMetadataComponent } from './comcol-metadata.component';
describe('ComColMetadataComponent', () => { describe('ComColMetadataComponent', () => {
let comp: ComcolMetadataComponent<DSpaceObject>; let comp: ComcolMetadataComponent<DSpaceObject>;
let fixture: ComponentFixture<ComcolMetadataComponent<DSpaceObject>>; let fixture: ComponentFixture<ComcolMetadataComponent<DSpaceObject>>;
let dsoDataService: CommunityDataService; let dsoDataService;
let router: Router; let router: Router;
let community; let community;
@@ -49,6 +49,7 @@ describe('ComColMetadataComponent', () => {
communityDataServiceStub = { communityDataServiceStub = {
update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity),
patch: () => null,
getLogoEndpoint: () => observableOf(logoEndpoint) getLogoEndpoint: () => observableOf(logoEndpoint)
}; };
@@ -95,37 +96,60 @@ describe('ComColMetadataComponent', () => {
describe('with an empty queue in the uploader', () => { describe('with an empty queue in the uploader', () => {
beforeEach(() => { beforeEach(() => {
data = { data = {
dso: Object.assign(new Community(), { operations: [
metadata: [{ {
key: 'dc.title', op: 'replace',
value: 'test' path: '/metadata/dc.title',
}] value: {
}), value: 'test',
language: null,
},
},
],
dso: new Community(),
uploader: { uploader: {
options: { options: {
url: '' url: ''
}, },
queue: [], queue: [],
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
uploadAll: () => {} uploadAll: () => {
}
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
} },
} deleteLogo: false,
};
spyOn(router, 'navigate');
}); });
it('should navigate when successful', () => { describe('when successful', () => {
spyOn(router, 'navigate');
comp.onSubmit(data); beforeEach(() => {
fixture.detectChanges(); spyOn(dsoDataService, 'patch').and.returnValue(observableOf({
expect(router.navigate).toHaveBeenCalled(); isSuccessful: true,
}));
});
it('should navigate', () => {
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).toHaveBeenCalled();
});
}); });
it('should not navigate on failure', () => { describe('on failure', () => {
spyOn(router, 'navigate');
spyOn(dsoDataService, 'update').and.returnValue(createFailedRemoteDataObject$(newCommunity)); beforeEach(() => {
comp.onSubmit(data); spyOn(dsoDataService, 'patch').and.returnValue(observableOf({
fixture.detectChanges(); isSuccessful: false,
expect(router.navigate).not.toHaveBeenCalled(); }));
});
it('should not navigate', () => {
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
});
}); });
}); });

View File

@@ -5,8 +5,7 @@ import { RemoteData } from '../../../../core/data/remote-data';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { first, map, take } from 'rxjs/operators'; import { first, map, take } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../../core/shared/operators'; import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasValue, isNotUndefined } from '../../../empty.util'; import { hasValue, isEmpty } from '../../../empty.util';
import { DataService } from '../../../../core/data/data.service';
import { ResourceType } from '../../../../core/shared/resource-type'; import { ResourceType } from '../../../../core/shared/resource-type';
import { ComColDataService } from '../../../../core/data/comcol-data.service'; import { ComColDataService } from '../../../../core/data/comcol-data.service';
import { NotificationsService } from '../../../notifications/notifications.service'; import { NotificationsService } from '../../../notifications/notifications.service';
@@ -49,26 +48,33 @@ export class ComcolMetadataComponent<TDomain extends DSpaceObject> implements On
* @param event The event returned by the community/collection form. Contains the new dso and logo uploader * @param event The event returned by the community/collection form. Contains the new dso and logo uploader
*/ */
onSubmit(event) { onSubmit(event) {
const dso = event.dso;
const uploader = event.uploader; const uploader = event.uploader;
const deleteLogo = event.deleteLogo; const deleteLogo = event.deleteLogo;
this.dsoDataService.update(dso) const newLogo = hasValue(uploader) && uploader.queue.length > 0;
.pipe(getSucceededRemoteData()) if (newLogo) {
.subscribe((dsoRD: RemoteData<TDomain>) => { this.dsoDataService.getLogoEndpoint(event.dso.uuid).pipe(take(1)).subscribe((href: string) => {
if (isNotUndefined(dsoRD)) { uploader.options.url = href;
const newUUID = dsoRD.payload.uuid; uploader.uploadAll();
if (hasValue(uploader) && uploader.queue.length > 0) {
this.dsoDataService.getLogoEndpoint(newUUID).pipe(take(1)).subscribe((href: string) => {
uploader.options.url = href;
uploader.uploadAll();
});
} else if (!deleteLogo) {
this.router.navigate([this.frontendURL + newUUID]);
}
this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.notifications.success'));
}
}); });
}
if (!isEmpty(event.operations)) {
this.dsoDataService.patch(event.dso, event.operations)
.subscribe(async (response) => {
if (response.isSuccessful) {
if (!newLogo && !deleteLogo) {
await this.router.navigate([this.frontendURL + event.dso.uuid]);
}
this.notificationsService.success(null, this.translate.get(`${this.type.value}.edit.notifications.success`));
} else if (response.statusCode === 403) {
this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.unauthorized`));
} else {
this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.error`));
}
});
}
} }
/** /**

View File

@@ -39,7 +39,15 @@ describe('CreateCollectionParentSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub }, { provide: NgbActiveModal, useValue: modalStub },
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } useValue: {
root: {
snapshot: {
data: {
dso: communityRD,
},
},
}
},
}, },
{ {
provide: Router, useValue: router provide: Router, useValue: router

View File

@@ -14,6 +14,6 @@
</h3> </h3>
<h5 class="px-2">{{'dso-selector.create.community.sub-level' | translate}}</h5> <h5 class="px-2">{{'dso-selector.create.community.sub-level' | translate}}</h5>
<ds-dso-selector [currentDSOId]="(dsoRD$ | async)?.payload.uuid" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector> <ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector>
</div> </div>
</div> </div>

View File

@@ -33,7 +33,15 @@ describe('CreateCommunityParentSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub }, { provide: NgbActiveModal, useValue: modalStub },
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } useValue: {
root: {
snapshot: {
data: {
dso: communityRD,
},
},
}
},
}, },
{ {
provide: Router, useValue: router provide: Router, useValue: router

View File

@@ -32,7 +32,15 @@ describe('CreateItemParentSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub }, { provide: NgbActiveModal, useValue: modalStub },
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } } useValue: {
root: {
snapshot: {
data: {
dso: collectionRD,
},
},
}
},
}, },
{ {
provide: Router, useValue: router provide: Router, useValue: router

View File

@@ -5,6 +5,6 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<ds-dso-selector [currentDSOId]="(dsoRD$ | async)?.payload.uuid ? 'search.resourceid:' + (dsoRD$ | async)?.payload.uuid : null" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector> <ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid ? 'search.resourceid:' + dsoRD?.payload.uuid : null" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector>
</div> </div>
</div> </div>

View File

@@ -41,8 +41,16 @@ describe('DSOSelectorModalWrapperComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub }, { provide: NgbActiveModal, useValue: modalStub },
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } } useValue: {
} root: {
snapshot: {
data: {
dso: itemRD,
},
},
}
}
},
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -63,11 +71,7 @@ describe('DSOSelectorModalWrapperComponent', () => {
}); });
it('should initially set the DSO to the activated route\'s item/collection/community', () => { it('should initially set the DSO to the activated route\'s item/collection/community', () => {
component.dsoRD$ expect(component.dsoRD).toEqual(itemRD);
.pipe(first())
.subscribe((a) => {
expect(a).toEqual(itemRD);
})
}); });
describe('selectObject', () => { describe('selectObject', () => {

View File

@@ -1,11 +1,12 @@
import { Injectable, Input, OnInit } from '@angular/core'; import { Injectable, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { hasValue, isNotEmpty } from '../../empty.util';
export enum SelectorActionType { export enum SelectorActionType {
CREATE = 'create', CREATE = 'create',
@@ -21,7 +22,7 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit {
/** /**
* The current page's DSO * The current page's DSO
*/ */
@Input() dsoRD$: Observable<RemoteData<DSpaceObject>>; @Input() dsoRD: RemoteData<DSpaceObject>;
/** /**
* The type of the DSO that's being edited or created * The type of the DSO that's being edited or created
@@ -45,10 +46,30 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit {
* Get de current page's DSO based on the selectorType * Get de current page's DSO based on the selectorType
*/ */
ngOnInit(): void { ngOnInit(): void {
const typeString = this.selectorType.toString().toLowerCase(); const matchingRoute = this.findRouteData(
this.dsoRD$ = this.route.root.firstChild.firstChild.data.pipe(map((data) => data[typeString])); (route: ActivatedRouteSnapshot) => hasValue(route.data.dso),
this.route.root.snapshot
);
if (hasValue(matchingRoute)) {
this.dsoRD = matchingRoute.data.dso;
}
} }
findRouteData(predicate: (value: ActivatedRouteSnapshot, index?: number, obj?: ActivatedRouteSnapshot[]) => unknown, ...routes: ActivatedRouteSnapshot[]) {
const result = routes.find(predicate);
if (hasValue(result)) {
return result;
} else {
const nextLevelRoutes = routes
.map((route: ActivatedRouteSnapshot) => route.children)
.reduce((combined: ActivatedRouteSnapshot[], current: ActivatedRouteSnapshot[]) => [...combined, ...current]);
if (isNotEmpty(nextLevelRoutes)) {
return this.findRouteData(predicate, ...nextLevelRoutes)
} else {
return undefined;
}
}
}
/** /**
* Method called when an object has been selected * Method called when an object has been selected
* @param dso The selected DSpaceObject * @param dso The selected DSpaceObject

View File

@@ -33,7 +33,15 @@ describe('EditCollectionSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub }, { provide: NgbActiveModal, useValue: modalStub },
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } } useValue: {
root: {
snapshot: {
data: {
dso: collectionRD,
},
},
}
},
}, },
{ {
provide: Router, useValue: router provide: Router, useValue: router

View File

@@ -33,7 +33,15 @@ describe('EditCommunitySelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub }, { provide: NgbActiveModal, useValue: modalStub },
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } useValue: {
root: {
snapshot: {
data: {
dso: communityRD,
},
},
}
},
}, },
{ {
provide: Router, useValue: router provide: Router, useValue: router

View File

@@ -33,7 +33,15 @@ describe('EditItemSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub }, { provide: NgbActiveModal, useValue: modalStub },
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } } useValue: {
root: {
snapshot: {
data: {
dso: itemRD,
},
},
}
},
}, },
{ {
provide: Router, useValue: router provide: Router, useValue: router

View File

@@ -0,0 +1,14 @@
import { PaginationComponentOptions } from './pagination-component-options.model';
import { FindListOptions } from '../../core/data/request.models';
/**
* Transform a PaginationComponentOptions object into a FindListOptions object
* @param pagination The PaginationComponentOptions to transform
* @param original An original FindListOptions object to start from
*/
export function toFindListOptions(pagination: PaginationComponentOptions, original?: FindListOptions): FindListOptions {
return Object.assign(new FindListOptions(), original, {
currentPage: pagination.currentPage,
elementsPerPage: pagination.pageSize
});
}

View File

@@ -8,6 +8,10 @@
"admin.registries.bitstream-formats.breadcrumbs": "Format registry",
"admin.registries.bitstream-formats.create.breadcrumbs": "Bitstream format",
"admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.",
"admin.registries.bitstream-formats.create.failure.head": "Failure", "admin.registries.bitstream-formats.create.failure.head": "Failure",
@@ -30,6 +34,8 @@
"admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.",
"admin.registries.bitstream-formats.edit.breadcrumbs": "Bitstream format",
"admin.registries.bitstream-formats.edit.description.hint": "", "admin.registries.bitstream-formats.edit.description.hint": "",
"admin.registries.bitstream-formats.edit.description.label": "Description", "admin.registries.bitstream-formats.edit.description.label": "Description",
@@ -94,6 +100,8 @@
"admin.registries.metadata.breadcrumbs": "Metadata registry",
"admin.registries.metadata.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.", "admin.registries.metadata.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.",
"admin.registries.metadata.form.create": "Create metadata schema", "admin.registries.metadata.form.create": "Create metadata schema",
@@ -120,6 +128,8 @@
"admin.registries.schema.breadcrumbs": "Metadata schema",
"admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".",
"admin.registries.schema.fields.head": "Schema metadata fields", "admin.registries.schema.fields.head": "Schema metadata fields",
@@ -767,6 +777,10 @@
"community.edit.notifications.success": "Successfully edited the Community", "community.edit.notifications.success": "Successfully edited the Community",
"community.edit.notifications.unauthorized": "You do not have privileges to make this change",
"community.edit.notifications.error": "An error occured while editing the Community",
"community.edit.return": "Return", "community.edit.return": "Return",